Leseprobe

Transcription

Leseprobe
kap01.fm Seite 5 Mittwoch, 5. Dezember 2001 10:40 10
2 C-Theorie
2.1
Typische Wertebereiche für die einzelnen Datentypen
Tabelle 2-1 zeigt die von Borland C für die einzelnen Datentypen verwendeten Bitzahlen und die daraus resultierenden Wertebereiche. Diese Tabelle gilt für viele C-Compiler.
Datentyp-Bezeichnung
Bitanzahl
Wertebereich
unsigned char
8
0 .. 255
char, signed char
8
-128 .. 127
unsigned short, unsigned short int
16
0 .. 65535
short, signed short, short int, signed short int
16
-32768 .. 32767
unsigned, unsigned int
16
0 .. 65535
int, signed, signed int
16
-32768 .. 32767
unsigned long, unsigned long int
32
0 .. 4294967295
long, signed long, long int, signed long int
32
-2147483648 .. 2147483647
float
32
3.4*10-38 .. 3.4*1038
double
64
1.7*10-308 .. 1.7*10308
long double
80
3.4*10-4932 .. 1.1*104932
Tabelle 2-1: Typische Wertebereiche für die einzelnen Datentypen
2.2
Prioritätstabelle für Operatoren
Tabelle 2-2 zeigt die Prioritäten der einzelnen C-Operatoren.
Operator (höchste Priorität oben; niedrigste Priorität unten)
( )
[ ]
->
. (Punktoperator)
Assoziativität
von links her
! ~ ++ -- – (Vorz.) + (Vorz.) * (Zeigerzugriff) & (Adresse) (casting) sizeof
von rechts her
* % /
von links her
Tabelle 2-2: Prioritätstabelle für die C-Operatoren
kap01.fm Seite 6 Mittwoch, 5. Dezember 2001 10:40 10
6
2
C-Theorie
Operator (höchste Priorität oben; niedrigste Priorität unten)
Assoziativität
+ -
von links her
<< >>
von links her
< <= > >=
von links her
== !=
von links her
& (Bitweises AND)
von links her
^
von links her
|
von links her
&&
von links her
||
von links her
? : (Bedingter Operator)
von rechts her
= += -= *= /= %= >>= <<= &= |= ^=
(Zuweisungsoperatoren)
, (Kommaoperator)
von rechts her
von links her
Tabelle 2-2: Prioritätstabelle für die C-Operatoren (Fortsetzung)
2.3
Regeln für C-Variablennamen
왘 Buchstaben (keine Umlaute oder ß), Ziffern und Unterstrich (_) sind erlaubt.
왘 Das erste Zeichen muß ein Buchstabe oder Unterstrich sein.
왘 Ein Variablenname darf beliebig lang sein.
왘 C-Schlüsselwörter sind nicht als Variablennamen erlaubt.
Hinweise:
왘 Variablennamen sollten klein geschrieben werden.
왘 C ist case-sensitiv, d.h. Klein- und Großbuchstaben werden unterschieden.
2.4
Die von C reservierten Schlüsselwörter
auto
double
int
struct
2.5
break
else
long
switch
case
enum
register
typedef
char
extern
return
union
const
float
short
unsigned
continue
for
signed
void
default
goto
sizeof
volatile
do
if
static
while
Verschiedene Arten von C-Konstanten
char-Konstanten
müssen mit ' ' geklammert werden.
Dezimal-Konstanten
beginnen immer mit einer von 0 verschiedenen Ziffer.
kap01.fm Seite 7 Mittwoch, 5. Dezember 2001 10:40 10
2.6
Symbolische Konstanten
7
beginnen immer mit einer 0.
Oktal-Konstanten
Hexadezimal-Konstanten beginnen immer mit 0x oder 0X.
Anhängen eines Suffix an Dezimal-, Oktal- oder Hexadezimal-Konstanten klassifiziert
die Konstante: unsigned (Suffix u bzw. U), long (Suffix l bzw. L), unsigned long (Suffix
ul, uL, Ul bzw. UL).
Gleitpunktkonstanten
Abbildung 2-1 zeigt das Syntaxdiagramm für erlaubte C-Gleitpunktkonstanten.
Gleitpunkt-Konstante:
.
Ziffern-Folge
.
Ziffern-Folge
Ziffern-Folge
.
.
.
.l
f
.
e
.
+
.
.-
Ziffern-Folge
Ziffern-Folge
E
.
.
F
L
Abbildung 2-1: Syntaxdiagramm für eine C-Gleitpunktkonstante
Hinweise:
왘 Die nach e bzw. E angegebene ganze Zahl ist der Zehnerexponent.
왘 Eine Gleitpunktkonstante mit dem Suffix f oder F hat den Typ float.
왘 Eine Gleitpunktkonstante mit dem Suffix l oder L hat den Typ long double.
왘 Eine Gleitpunktkonstante ohne Suffix-Angabe hat den Typ double.
2.6
Symbolische Konstanten
Symbolische Namen können auf zwei Arten an Konstanten vergeben werden:
#define name wert
Mit #define definierte Konstantennamen sollten groß geschrieben werden.
const datentyp name = wert;
unter Verwendung des Schlüsselworts const; const muß vor dem Datentyp bei der
Deklaration stehen, wie z.B. const double e = 2.718281828459;
kap01.fm Seite 8 Mittwoch, 5. Dezember 2001 10:40 10
8
2.7
2
C-Theorie
Einfache Makros
Unter einem Makro versteht man eine Folge von Einzelbefehlen, die unter einem
Namen angesprochen werden können. Soll diese Folge von Einzelbefehlen ausgeführt
werden, so ist nur der Name anzugeben, der diese Befehlsfolge repräsentiert. Man
spricht dann auch vom Makroaufruf. Ein Makroaufruf an einer bestimmten Stelle
bewirkt, daß dort die entsprechenden Befehle, die dieses Makro repräsentiert, einfach
einkopiert werden. Makros werden in C mit
#define makroname(param1,param2,...)
(makrobefehl(e))
definiert.
2.8
Ausdrücke und Operatoren
Ein Ausdruck besteht aus mehreren Operanden, die durch einen oder mehrere der folgenden Operatoren miteinander verknüpft sind.
왘 Klammern
(())
왘 Array-Indizierung
( [] )
왘 Strukturoperatoren
( . -> )
왘 Monadische Operatoren
(! ~ ++ -- – (Vorzeichen) + (Vorzeichen)
* (Zeigerzugriff) & (Adresse)
(casting) sizeof )
왘 Arithmetische Operatoren
(*/%+–)
왘 Shiftoperatoren
( << >> )
왘 Vergleichsoperatoren
( < <= > >= == != )
왘 Dyadische Bitoperatoren
(&^|)
왘 Logische Operatoren
( && || )
왘 Bedingter Operator
(?:)
왘 Einfacher Zuweisungsoperator
(=)
왘 Zusammengesetzte
Zuweisungsoperatoren
( += -= *= /= %= >>= <<= &=
|= ^= )
왘 Kommaoperator
(,)
kap01.fm Seite 9 Mittwoch, 5. Dezember 2001 10:40 10
2.9
Zeiger auf Konstanten und konstante Zeiger
2.8.1
TRUE und FALSE in C
Wert 0
steht für FALSE und
Ein Wert verschieden von 0
steht für TRUE
2.8.2
9
Keine unnötige Auswertung rechts von && und ||
&& Wenn der linke Operand bei && FALSE (in C: 0) zurückliefert, dann ist der
Wahrheitswert des gesamten Ausdrucks in jedem Fall FALSE, und C wertet den rechten Operand nicht aus.
||
Wenn der linke Operand bei || TRUE (in C: verschieden von 0) zurückliefert,
dann ist der Wahrheitswert des gesamten Ausdrucks in jedem Fall TRUE, und C wertet den rechten Operand nicht aus.
2.8.3
Erlaubte/Unerlaubte Operationen für einfache Datentypen
char, int:
Alle Operatoren sind erlaubt.
float, double:
Nicht erlaubt sind:
~
(Einer-Komplement)
%
(Modulo)
<< >>
(Shifts)
& | ^
(Bitweises AND, OR und XOR)
%= >>= <<= &= |= ^=
2.9
Zeiger auf Konstanten und konstante Zeiger
Sie müssen zwischen Zeigern auf Konstanten und konstanten Zeigern unterscheiden:
const int *zgr_auf_konstante;
int *const konstanter_zgr;
const int *const konst_zgr_auf_konstante;
2.10 Ein- und Ausgabe eines Zeichens
getchar() und putchar() ermöglichen die Ein- bzw. Ausgabe eines Zeichens über das Terminal:
getchar()
Eingabe eines Zeichens von der Standardeingabe. So liest z.B.
a=getchar();
das nächste eingegebene Zeichen aus dem Eingabepuffer in die Variable
a.
putchar()
Ausgabe eines Zeichens auf der Standardausgabe. So schreibt z.B.
putchar(antwort);
den Inhalt der Variablen antwort in den Ausgabepuffer (auf den Bildschirm).
kap01.fm Seite 10 Mittwoch, 5. Dezember 2001 10:40 10
10
2
C-Theorie
Diese beiden Funktionen sind in der Datei stdio.h definiert, so daß man
#include <stdio.h>
im C-Programm angeben muß.
Hinweise:
왘 Viele C-Compiler, wie z.B. Borland-C, bieten neben getchar() noch eine eigene
Nicht-ANSI-C-Funktion getch() an. Diese Funktion liest wie getchar() ein Zeichen
ein. Jedoch wird dieses Zeichen nicht gepuffert, sondern direkt nach dem Tastendruck eingelesen. Es ist also bei getch() kein Abschluß der Eingabe mit RETURN
notwendig.
왘 Wenn Sie Zeichen eingeben, werden diese bei einer gepufferten Eingabe (was der
Normalzustand ist) nicht sofort an das Programm übergeben, sondern vom
Betriebssystem in einem internen Puffer abgelegt. Erst nachdem der Anwender die
RETURN-Taste gedrückt hat, beginnt das Programm mit dem Lesen aus diesem
Puffer. Es ist wichtig zu wissen, daß die RETURN-Taste (ASCII-Wert 10) auch noch
im Puffer (am Ende) gespeichert wird. An den entsprechenden Stellen muß immer
sichergestellt sein, daß das überflüssige RETURN-Zeichen aus dem Puffer entfernt
ist. Dies kann grundsätzlich auf zwei verschiedene Arten geschehen:
–
Man verwendet entweder ein sogenanntes Dummy-getchar(), dessen einziger
Zweck das Lesen (Entfernen) des überflüssigen RETURN-Zeichens aus dem
Puffer ist.
–
Oder man ruft fflush(stdin); auf, um den gesamten Eingabepuffer zu leeren.
2.11 Der sizeof-Operator
Mit dem sizeof-Operator kann die Größe eines Datentyps, einer Variable oder eines
Ausdrucks in Bytes ermittelt werden. So sind z.B. die folgenden Angaben möglich:
sizeof(double)
liefert die Anzahl von Bytes zurück, die vom Datentyp double belegt werden.
sizeof(var)
liefert die Anzahl von Bytes zurück, die von der Variable var belegt werden.
sizeof(a+b)
liefert die Anzahl von Bytes zurück, die zur Berechnung des Ausdrucks a+b benötigt werden.
Wichtig ist noch, daß bei Anwendung des sizeof-Operators auf einen Ausdruck dieser
Ausdruck nicht ausgewertet wird, sondern nur die für diese Berechnung benötigte
Byteanzahl bestimmt wird. So würde z.B. bei der Angabe:
kap01.fm Seite 11 Mittwoch, 5. Dezember 2001 10:40 10
2.12
Ausgabe mit printf
11
printf("%d Bytes fuer 2.3 + 3.456\n", sizeof(2.3+3.456));
nicht der Ausdruck 2.3+3.456 berechnet, sondern es würde lediglich die Anzahl der für
diese Berechnung benötigten Bytes bestimmt.
2.12 Ausgabe mit printf
printf("kontrollzeichenkette", argument1, argument2,...)
Die Kontrollzeichenkette gibt an, wie die einzelnen Argumente auszugeben sind. In
der kontrollzeichenkette können ASCII-Zeichen stehen, die unverändert ausgegeben
werden.
2.12.1 Steuerzeichen
Neben ASCII-Zeichen können in der kontrollzeichenkette aber auch die in Tabelle 2-3
angegebenen Steuerzeichen enthalten sein:
Steuerzeichen
Bedeutung
\a
Klingelton (auch mit \007 realisierbar)
\b
Backspace (ein Zeichen zurück positionieren)
\f
Seitenvorschub
\n
Neue Zeile
\r
Wagenrücklauf (an Anfang der momentanen Zeile positionieren)
\t
Tabulator
\v
Vertikales Tabulatorzeichen
\ooo
Zeichen, das der Oktalzahl ooo entspricht
\xhh
Zeichen, das der Hexadezimalzahl hh entspricht
\'
Hochkomma
\"
Anführungszeichen
\\
Backslash
Tabelle 2-3: Mögliche Steuerzeichen in der Kontrollzeichenkette
2.12.2 Umwandlungsvorgaben
Neben den normalen ASCII-Zeichen und den obigen Steuerzeichen können in der kontrollzeichenkette noch Umwandlungsvorgaben angegeben sein.
Umwandlungsvorgaben beginnen immer mit % und beziehen sich auf die nachfolgenden Argumente: Die 1. Umwandlungsvorgabe bezieht sich auf argument1, die 2.
Umwandlungsvorgabe auf argument2 usw. Umwandlungsvorgaben legen immer fest,
wie das entsprechende Argument auszugeben ist.
kap01.fm Seite 12 Mittwoch, 5. Dezember 2001 10:40 10
12
2
C-Theorie
Eine Umwandlungsvorgabe setzt sich wie folgt zusammen:
%FWGLU
F = [Formatierungszeichen]
W = [Weite]
Mindestzahl der auszugebenden Zeichen
G = [Genauigkeit]
. oder .* oder .ganzzahl
L = [Längenangabe]
h (short), l oder L (long)
U = Umwandlungszeichen
Umwandlungszeichen
Tabelle 2-4 zeigt die erlaubten Umwandlungszeichen bei printf:
Umwandlungszeichen
Wert des Arguments wird ausgegeben....
d, i
als eine vorzeichenbehaftete ganze Dezimalzahl (i ist neu in ANSI C)
o
als eine vorzeichenlose ganze Oktalzahl
u
als eine vorzeichenlose ganze Dezimalzahl
x, X
als eine vorzeichenlose ganze Hexazahl (a,b,c,d,e,f) bei x und
(A,B,C,D,E,F) bei X
f
in Form von [-]ddd.dddddd
e,E
in Form von [-]d.ddde±dd bzw. [-]d.dddE±dd; Exponent enthält
mindestens 2 Ziffern
g,G
im e-bzw. E-Format, wenn Exponent <-4 oder ≥Genauigkeit ist, sonst im
f-Format
c
als Zeichen (unsigned char)
s
als Zeichenkette
p
als Zeigerwert (Sequenz von druckbaren Zeichen)
n
keine Ausgabe; das entsprechende Argument sollte ein Zeiger auf eine
Ganzzahl sein. An diese Adresse wird die Anzahl der bisher ausgegebenen
Zeichen geschrieben.
%
Es wird % ausgegeben und kein Argument ausgewertet (nur als %% angeben)
Tabelle 2-4: Mögliche Umwandlungszeichen bei printf
Formatierungszeichen
Tabelle 2-4 zeigt die erlaubten Formatierungszeichen bei printf:
Formatierungszeichen
Bedeutung
-
linksbündige Justierung
+
Ausgabe des Vorzeichens '+' oder '-'
Tabelle 2-5: Mögliche Formatierungszeichen bei printf
kap01.fm Seite 13 Mittwoch, 5. Dezember 2001 10:40 10
2.12
Ausgabe mit printf
13
Formatierungszeichen
Bedeutung
Leerzeichen
Falls das 1. Zeichen des Arguments kein Vorzeichen ist, wird ein Leerzeichen ausgegeben
0
Bei numerischer Ausgabe wird mit Nullen bis zur angegebenen Weite aufgefüllt
#
Auswirkung von # hängt vom Umwandlungszeichen ab:
Bei o bzw. x, X wird der Wert mit vorangestelltem 0 bzw. 0x ausgeben.
Bei e,E,f wird der Wert mit einem Dezimalpunkt ausgegeben, sogar
wenn keine Nachkommastellen existieren.
Bei g,G wird der Wert mit Dezimalpunkt ausgegeben (überflüssige Nachkommanullen werden mitausgeben).
Tabelle 2-5: Mögliche Formatierungszeichen bei printf (Fortsetzung)
Ausgabenweite
Die Weite gibt die Mindestanzahl der auszugebenden Stellen an. Wenn der umgewandelte Wert weniger Zeichen als Weite hat, so wird er links (rechts bei Linksjustierung)
mit Leerzeichen oder Nullen (wenn Formatierungszeichen 0 angegeben) aufgefüllt.
Für Weite sind die in Tabelle 2-6 gezeigten Angaben möglich:
Angabe
Bedeutung
n
Mindestens n Stellen werden ausgegeben. Falls der Wert des entsprechenden Arguments weniger Stellen als n besitzt, werden dennoch n Stellen ausgegeben.
*
Wert des nächsten Arguments (muß ganzzahlig sein) in Argumentenliste legt Weite
fest. Falls der Wert dieses Arguments negativ ist, wird linksbündige Justierung vorgenommen.
Tabelle 2-6: Mögliche Weite-Angaben bei printf
Niemals bewirkt eine nicht vorhandene oder zu kleine Weite-Angabe, daß Zeichen
nicht ausgegeben werden. Falls das Ergebnis einer Umwandlung mehr Zeichen enthält, als Weite vorgibt, dann werden trotzdem alle Zeichen ausgegeben.
Genauigkeitsangabe
Die Genauigkeit wird mit .ganzzahl angegeben. Die Auswirkung von Genauigkeit
hängt vom angegebenen Umwandlungszeichen ab (siehe auch Tabelle 2-7)
Umwandlungszeichen
Genauigkeit legt folgendes fest:
d,i,o,u,x,X
Mindestzahl von auszugebenden Ziffern
e,E,f
Zahl der auszugebenden Nachkommastellen
Tabelle 2-7: Auswirkung der Genauigkeitsangabe bei printf
kap01.fm Seite 14 Mittwoch, 5. Dezember 2001 10:40 10
14
2
Umwandlungszeichen
Genauigkeit legt folgendes fest:
g,G
maximale Zahl von auszugebenden Ziffern
s
maximale Zahl von auszugebenden Zeichen
sonstige
undefiniertes Verhalten
C-Theorie
Tabelle 2-7: Auswirkung der Genauigkeitsangabe bei printf (Fortsetzung)
Für Genauigkeit kann auch
.*
angegeben werden, was bedeutet: das nächste Argument (muß ganzzahlig sein) in der
Argumentenliste legt die Genauigkeit fest. Ist der Wert dieses Arguments negativ, wird
diese Genauigkeitsangabe ignoriert.
Längenangabe
Die in Tabelle 2-8 angegebenen Zeichen sind als Längenangabe erlaubt und haben für
die einzelnen Umwandlungszeichen die in dieser Tabelle angegebene Auswirkung:
Längenangabe
Auswirkung
h
Für die Umwandlungszeichen d,i,o,u,x,X wird das entsprechende Argument als
short-Wert behandelt.
Beim Umwandlungszeichen n wird Argument als "Zeiger auf short int" behandelt.
l
Für die Umwandlungszeichen d,i,o,u,x,X wird das entsprechende Argument als
long-Wert behandelt.
Beim Umwandlungszeichen n wird das entsprechende Argument als "Zeiger auf
long int" behandelt.
L
Für die Umwandlungszeichen e,E,f,g,G wird das entsprechende Argument als long
double-Wert behandelt.
Tabelle 2-8: Auswirkung der Längenangabe bei printf
Falls h, l oder L mit einem anderen Umwandlungszeichen, als oben angegeben, kombiniert wird, so liegt undefiniertes Verhalten vor.
2.13 Eingabe mit scanf
scanf("kontrollzeichenkette", argument1, argument2,...)
Die kontrollzeichenkette gibt an, wie die einzelnen Argumente einzulesen sind. Sie legt
also das Eingabeformat fest. In der kontrollzeichenkette können angegeben sein:
kap01.fm Seite 15 Mittwoch, 5. Dezember 2001 10:40 10
2.13
Eingabe mit scanf
15
왘 ein oder mehrere Zwischenraumzeichen (Leerzeichen, \f, \n, \r, \t oder \v)
Ein Zwischenraumzeichen in der kontrollzeichenkette bedeutet, daß alle in der Eingabezeile folgenden Leerzeichen, Tabulatoren, Seiten- und Zeilenvorschübe bis zum
ersten Nicht-Zwischenraumzeichen zu überlesen sind.
왘 einfache Zeichen (weder % noch Zwischenraumzeichen)
Ein einfaches Zeichen in der kontrollzeichenkette bewirkt, daß die nächsten Zeichen
in der Eingabezeile gelesen werden. Wenn allerdings ein Zeichen aus der Eingabe
nicht dem angegebenen Zeichen entspricht, dann schlägt dieser Leseversuch fehl,
und sowohl dieses als auch das nachfolgende Zeichen bleiben ungelesen.
왘 Umwandlungsvorgaben beginnen immer mit % und beziehen sich auf die folgenden Argumente: 1. Umwandlungsvorgabe auf argument1, 2. Umwandlungsvorgabe
auf argument2 usw.
Eine Umwandlungsvorgabe setzt sich wie folgt zusammen:
%SWLU
S = [*]
Dem Argument wird kein Wert zugewiesen; es wird übersprungen.
W = [Weite]
Maximale Anzahl der zu lesenden Zeichen; wird vor dem Erreichen dieser Zeichenzahl ein Zwischenraumzeichen oder ein anderes "nicht passendes" Zeichen gelesen, wird das Lesen dieses
Werts abgebrochen.
L = [Längenangabe]
Legt Größe des entsprechenden Eingabeelements fest (h für
short und l oder L für long).
U = Umwandlungszeichen
Hieran ist zu erkennen, daß nur das Umwandlungszeichen immer angegeben sein muß.
Die anderen Angaben (*, Weite und Längenangabe) sind optional.
Längenangabe
Hierbei sind die drei Zeichen h, l oder L erlaubt. Tabelle 2-9 zeigt, in welchen Fällen
von dieser Längenangabe Gebrauch zu machen ist:
Argumenttyp
Längenangabe
mögliche Umwandlungszeichen
short int * (anstelle von int *)
h
d, i, n
unsigned short int * (anstelle von unsigned int *)
h
o, u, x
long int * (anstelle von int *)
l
d, i, n
unsigned long int * (anstelle von unsigned int *)
l
o, u, x
Tabelle 2-9: Längenangabe bei scanf
kap01.fm Seite 16 Mittwoch, 5. Dezember 2001 10:40 10
16
2
Argumenttyp
Längenangabe
double * (anstelle von float *)
long double * (anstelle von float *)
C-Theorie
mögliche Umwandlungszeichen
l
e, f, g, E, G
L
e, f, g, E, G
Tabelle 2-9: Längenangabe bei scanf (Fortsetzung)
Umwandlungszeichen
Tabelle 2-10 zeigt die erlaubten Umwandlungszeichen bei scanf.
UmwandlungsZeichen
Eingabedaten
Argumenttyp
Adresse von ...
d
ganze Zahl (Suffix u, U, l, L nicht erlaubt)
Ganzzahlvariable
i
ganze Zahl (Suffix u, U, l, L nicht erlaubt)
Ganzzahlvariable
o
ganze Oktalzahl
unsigned-Variable
u
ganze Zahl
unsigned-Variable
x, X
ganze Hexadezimalzahl
unsigned-Variable
e,f,g,E,G
Gleitpunktzahl
Gleitpunktvariable
s
Zeichenkette (ohne Zwischenraumzeichen)
char-Variable
c
Zeichenkette (Im Unterschied zu %s werden hier Zwischen- char-Variable
raumzeichen gelesen)
p
Zeigerwert
Zeigervariable
n
kein Lesevorgang (Anzahl der bisher gelesenen
Zeichen wird in das zugehörige Argument geschrieben)
Ganzzahlvariable
[liste]
Zeichenkette (Einlesen bis Zeichen, das nicht in liste vorkommt)a
char-Variable
[^liste]
Zeichenkette (Einlesen bis Zeichen, das in liste vorkommt)b char-Variable
%
(das Zeichen) %
(verarbeitet das Zeichen % aus der Eingabe)
kein Argument
Tabelle 2-10: Mögliche Umwandlungszeichen bei scanf
a. Wenn ] in liste angegeben werden soll, so ist es als erstes Zeichen dort anzugeben: []...]
b. Wenn ] in liste angegeben werden soll, so ist es als zweites Zeichen dort anzugeben: [^]...]
Reihenfolge der Abarbeitung von Eingaben durch scanf()
Für jede Umwandlungsvorgabe werden folgende Aktivitäten (in der angegebenen Reihenfolge) auf der Eingabezeile durchgeführt:
1. Zwischenraumzeichen in der Eingabezeile werden einfach übersprungen, außer
die kontrollzeichenkette verwendet an dieser Stelle eines der Umwandlungszeichen [,
c oder n.
kap01.fm Seite 17 Mittwoch, 5. Dezember 2001 10:40 10
2.14
Datentypumwandlungen
17
2. Es wird eine Eingabeeinheit von der Eingabe gelesen1. Eine Eingabeeinheit ist die
längste passende Folge von Eingabezeichen (bis zu einer eventuellen Weite). Das
erste Zeichen nach dieser Eingabeeinheit bleibt ungelesen.
3. Die Eingabeeinheit wird entsprechend den vorgegebenen Umwandlungszeichen in
einen geeigneten Typ konvertiert. Wenn sich die Eingabeeinheit als nicht passend
für dieses Umwandlungszeichen erweist, so liegt eine "falsche Eingabe" vor und
scanf wird verlassen. Nachfolgende Zwischenraumzeichen bleiben ungelesen,
außer sie werden durch eine Umwandlungsvorgabe angefordert.
2.14 Datentypumwandlungen
2.14.1 Implizite Datentypumwandlungen
Werden in Ausdrücken Operanden unterschiedlicher Datentypen über Operatoren
verknüpft, dann sind implizite Datentypumwandlungen notwendig. Hierbei hält sich
C an folgende Regeln:
Regel 1 (char, short, int und unsigned)
Es wird mit nichts kleinerem als int gerechnet, d.h. char und short werden implizit
und ohne Einwirkung des Programmierers nach int umgewandelt.
Reicht der int-Datentyp nicht aus, um den Wert aufzunehmen, wird mit unsigned int
gerechnet.
Ob ein "reiner" char-Wert als vorzeichenbehaftet behandelt wird oder nicht, ist in ANSI
C nicht festgelegt.
Regel 2 (signed und unsigned)
Hier werden mehrere Fälle unterschieden:
왘 unsigned --> in längeren signed- oder unsigned-Datentyp
Der ursprüngliche Wert bleibt unverändert.
왘 signed --> in gleichlangen oder längeren unsigned-Datentyp
Es wird das entsprechende Bitmuster im unsigned-Datentyp abgelegt, wobei im
Falle eines längeren unsigned-Datentyps das Vorzeichenbit "nach vorne verlängert" wird.
왘 signed oder unsigned --> in kürzeren unsigned- oder signed-Datentyp
Für den Fall, daß der Wert zu groß für den Zieltyp ist, gibt es in ANSI C keine festen
Vorschriften. Meist gilt aber, daß vorne überhängende Bits einfach abgeschnitten
werden.
1. außer für das Umwandlungszeichen n
kap01.fm Seite 18 Mittwoch, 5. Dezember 2001 10:40 10
18
2
C-Theorie
왘 unsigned --> in gleichlangen signed-Datentyp
Hierbei wird meist nur das Bitmuster im signed-Datentyp abgespeichert; ANSI C
legt nichts für den Fall fest, daß der unsigned-Wert außerhalb des signed-Wertebereichs liegt.
Regel 3 (Gleitpunktzahlen und Ganzzahlen)
Hier werden zwei Fälle unterschieden:
왘 Gleitpunktzahl ---> in Ganzzahl
Der gebrochene Anteil der Gleitpunktzahl wird abgeschnitten. Wenn der ganzzahlige Anteil außerhalb des Wertebereichs des Ganzzahltyps liegt, so ist das Verhalten
undefiniert.
왘 Ganzzahl ---> in Gleitpunktzahl
Wenn der Wert der Ganzzahl zwar im Wertebereich der darstellbaren Gleitpunktzahlen liegt, aber nicht genau darstellbar ist, dann ist das Ergebnis entweder der
nächsthöhere oder der nächstniedrigere darstellbare Wert (von ANSI C nicht vorgeschrieben).
Regel 4 (float, double und long double)
Hier werden zwei Fälle unterschieden:
왘 float ---> double
float ---> long double
double ---> long double
Der ursprüngliche Wert bleibt unverändert.
왘 double ---> float
longdouble ---> float
long double ---> double
Falls der Wert größer ist, als der Zieldatentyp aufnehmen kann, liegt undefiniertes
Verhalten vor. Ist der Wert für den Zieldatentyp nicht zu groß, kann aber dort trotzdem nicht genau dargestellt werden, da dort weniger Bits zur Darstellung zur Verfügung stehen, ist das Ergebnis der nächsthöhere oder nächstniedrigere
darstellbare Wert (von ANSI C nicht vorgeschrieben).
Regel 5 (Übliche arithmetische Umwandlungen)
Die hier angegebenen impliziten Umwandlungen von Datentypen werden als übliche
arithmetische Umwandlungen bezeichnet. Die dabei geltenden Regeln lassen sich graphisch – wie in Abbildung 2-2 gezeigt – darstellen:
kap01.fm Seite 19 Mittwoch, 5. Dezember 2001 10:40 10
2.14
Datentypumwandlungen
19
Abbildung 2-2: Übliche arithmetische Umwandlungen
Horizontale Umwandlungen werden immer durchgeführt und vertikale Umwandlungen nur bei Bedarf. Für die vertikalen Umwandlungen gilt: Wenn einer der beiden
Operanden in einem Ausdruck einen weiter oben stehenden Datentyp hat, dann wird
zuerst der niedrigere Operand in diesen Datentyp umgewandelt, bevor der Ausdruck
berechnet wird.
Erläuterungen zu Regel 5:
Entsprechend Abbildung 2-2 gilt z.B.: Wenn in einem Ausdruck ein Operand den
Datentyp unsigned int und der andere Operand den Datentyp double hat, so wird der
unsigned int-Operand implizit in double umgewandelt, bevor die Berechnung dieses
Ausdrucks dann im double-Format stattfindet.
2.14.2 Explizite Datentypumwandlungen mit cast-Operator
Explizite Datentypumwandlungen werden über sogenannte Casts erreicht. Ein Cast ist
ein in Klammern gesetzter Datentyp, der einem Ausdruck vorangestellt wird:
(datentyp) ausdruck
Bei einer solchen Konstruktion wird der Wert des ausdrucks in den angegebenen datentyp umgewandelt.
kap01.fm Seite 20 Mittwoch, 5. Dezember 2001 10:40 10
20
2
C-Theorie
2.15 Anweisungen und Blöcke
Fügt man zu einem Ausdruck wie schieb>>=4 oder scanf("%d", &zahl) oder zaehl=7
oder nieder-- ein Semikolon hinzu, so erhält man eine C-Anweisung. Um mehrere
Anweisungen zu einem Block zusammenfassen zu können, müssen die geschweiften
Klammern { und } verwendet werden. Ein solcher Block wird dann wie eine einzelne
Anweisung interpretiert.
2.16 Die if-Anweisung
2.16.1 Einseitige if-Anweisung
if (ausdruck)
anweisung1
anweisung2
anweisung1 kann auch ein Block von Anweisungen sein, die mit { } geklammert sind.
Der nach if in Klammern ( ) angegebene ausdruck ist meist eine Bedingung. Ist die
Bedingung erfüllt bzw. der Wert dieses Ausdrucks ungleich 0 (TRUE), dann wird
anweisung1 ausgeführt, ansonsten wird anweisung1 übersprungen und sofort
anweisung2 ausgeführt. Im Struktogramm und Programmablaufplan wird die einseitige if-Anweisung wie folgt dargestellt:
Struktogramm:
Programmablaufplan:
kap01.fm Seite 21 Mittwoch, 5. Dezember 2001 10:40 10
2.16
Die if-Anweisung
21
2.16.2 Zweiseitige if-Anweisung
if (ausdruck)
anweisung1
else
anweisung2
anweisung1 und anweisung2 können auch ein Block von Anweisungen sein, die mit { }
geklammert sind.
Der nach if in Klammern ( ) angegebene ausdruck ist meist eine Bedingung. Ist die
Bedingung erfüllt bzw. der Wert des Ausdrucks ungleich 0, dann wird anweisung1,
sonst anweisung2 ausgeführt. Das Struktogramm und der Programmablaufplan zur
zweiseitigen if-Anweisung sehen wie folgt aus:
Struktogramm:
Programmablaufplan:
kap01.fm Seite 22 Mittwoch, 5. Dezember 2001 10:40 10
22
2
C-Theorie
2.17 Die bedingte Bewertung
C erlaubt als Sonderform die bedingte Bewertung:
ausdr1 ? ausdr2 : ausdr3
Zunächst wird dabei der ausdr1 bewertet. Ist sein Wert verschieden von 0 (TRUE), so
wird ausdr2 bewertet, andernfalls ausdr3. In jedem Fall wird nur einer der beiden Ausdrücke ausdr2 oder ausdr3 bewertet; dieser Wert ist dann das Ergebnis der gesamten
bedingten Bewertung.
Beispiel
max = (zahl1>zahl2) ? zahl1 : zahl2;
Zunächst wird hier der Ausdruck (zahl1>zahl2) bewertet. Trifft diese Bedingung zu,
wird der Variablen max der Wert von zahl1, sonst der Wert von zahl2 zugewiesen.
2.18 Die switch-Anweisung
Mit der switch-Anweisung kann unter mehreren Alternativen und nicht nur unter
zwei wie bei der if-Anweisung, ausgewählt werden. Die Syntax für die switch-Anweisung ist:
switch (ausdruck) {
case ausdr1:
anweisungen1
case ausdr2:
anweisungen2
:::
case ausdrn:
anweisungenn
default:
anweisungend
}
Der bei switch angegebene (ausdruck) wird ausgewertet und das Ergebnis mit den einzelnen case-Ausdrücken, die long-, int- oder char-Werte sein müssen, verglichen. Wird
keine Übereinstimmung gefunden, so wird zur default-Marke verzweigt, falls diese
angegeben ist. Ist keine default-Marke angegeben, so wird keine Anweisung des
switch-Blocks ausgeführt.
kap01.fm Seite 23 Mittwoch, 5. Dezember 2001 10:40 10
2.18
Die switch-Anweisung
23
switch ist eine Art von Programmschalter und wird in zwei Struktogrammarten dargestellt:
Die Reihenfolge der case- und default-Marken ist beliebig. Die case-Konstanten müssen verschieden sein. Um eine switch-Anweisung verlassen zu können, ist break
anzugeben, ansonsten wird der Programmablauf bei den Anweisungen der nächsten
case-Marke fortgesetzt.
kap01.fm Seite 24 Mittwoch, 5. Dezember 2001 10:40 10
24
2
C-Theorie
2.19 Der Komma-Operator
Sind mehrere Ausdrücke durch ein Komma (,) getrennt, so werden sie von links nach
rechts bewertet. Der Datentyp und Wert des Gesamtausdrucks ist dann gleich dem
Datentyp und Ergebnis der letzten Operation. Im Zusammenhang mit Schleifenparametern wird der Komma-Operator häufig verwendet, da er die Unterbringung von
mehreren Zuweisungen an einer Stelle erlaubt, wo sonst nur eine erlaubt wäre. Die forSchleife ist ein wichtiger Anwendungsfall für den Komma-Operator. In vielen Fällen
müssen mehrere Schleifenvariablen initialisiert und/oder reinitialisiert werden.
Der Komma-Operator hat die niedrigste Priorität überhaupt.
2.20 Die for-Anweisung
Syntaktisch sieht die for-Schleife wie folgt aus:
for (ausdruck1 ; ausdruck2 ; ausdruck3)
anweisung
ausdruck1
initialisiert die Schleifenvariable(n).
ausdruck2
Abbruchkriterium für Schleife, wenn ausdruck2 nicht erfüllt (falsch) ist.
ausdruck3
reinitialisiert die Schleifenvariable(n), z.B. beim Inkrementieren von Schleifenvariable(n).
Die Komponenten (ausdrücke) können einzeln oder auch insgesamt fehlen, jedoch müssen die Semikolons in der Klammer an den richtigen Stellen verbleiben.
anweisung kann auch ein Block von Anweisungen sein, der dann mit {..} zu klammern
ist.
Struktogramm zur for-Schleife:
kap01.fm Seite 25 Mittwoch, 5. Dezember 2001 10:40 10
2.21
Die while-Anweisung
25
Programmablaufplan zur for-Schleife:
Allgemein könnte eine for-Schleife so dargestellt werden:
for (Schleifenvariable=Anfangswert;
Schleifenbedingung;
Schleifenvariable verändern)
Es können auch Ausdrücke innerhalb einer for-Anweisung weggelassen werden. Es ist
sogar möglich, daß man alle drei Ausdrücke innerhalb einer for-Anweisung wegläßt,
wie for ( ; ; ), was einer Endlosschleife for ( ;1; ) entspricht, die nur mit break verlassen
werden kann.
In den Anweisungen einer for-Schleife sind wie bei jeder anderen Schleife wieder weitere Schleifen erlaubt. Man spricht dann von geschachtelten Schleifen.
2.21 Die while-Anweisung
Syntaktisch sieht die while-Schleife wie folgt aus:
while (ausdruck)
anweisung
Die anweisung kann auch ein Block von Anweisungen sein, der dann mit {..} zu klammern ist.
kap01.fm Seite 26 Mittwoch, 5. Dezember 2001 10:40 10
26
2
C-Theorie
Struktogramm zur while-Schleife:
Programmablaufplan zur while-Schleife:
Vor jedem Schleifendurchlauf wird ausdruck berechnet. Solange das Ergebnis ungleich
0 (wahr) ist, wird die Schleifen-anweisung ausgeführt. Erst, wenn die Auswertung von
ausdruck 0 (falsch) liefert, wird die Schleife beendet, und das Programm fährt mit der
nächsten nicht zur Schleife gehörigen Anweisung fort. Jede for-Schleife läßt sich auch
durch eine while-Schleife formulieren und umgekehrt.
kap01.fm Seite 27 Mittwoch, 5. Dezember 2001 10:40 10
2.22
Die do...while-Anweisung
27
2.22 Die do...while-Anweisung
Syntaktisch sieht die do...while-Schleife wie folgt aus:
do
anweisung
while (ausdruck);
Die anweisung kann auch ein Block von Anweisungen sein, der dann mit {..} zu klammern ist.
Struktogramm und Programmablaufplan zur do...while-Schleife:
kap01.fm Seite 28 Mittwoch, 5. Dezember 2001 10:40 10
28
2
C-Theorie
Nach jedem Schleifendurchlauf wird ausdruck berechnet. Ist das Ergebnis ungleich 0
(wahr), wird Schleifen-anweisung nochmals ausgeführt. Erst, wenn die Auswertung
von ausdruck 0 liefert, endet die Schleife. Anders als bei einer for-/while-Schleife wird
bei do...while die Schleifenbedingung am Ende abgefragt, d.h. die Schleifenanweisung
wird mindestens einmal ausgeführt, was bei for-/while-Schleifen nicht garantiert ist,
da dort die Schleifenbedingung am Anfang geprüft wird.
2.23 Die break-Anweisung
Die break-Anweisung dient dazu, eine for-, while-, do...while-Schleife oder eine
switch-Anweisung vorzeitig zu verlassen. Dazu ist nur folgendes anzugeben:
break;
Bei Mehrfachschachtelung bricht break nur die innerste Schleife bzw. die switchAnweisung ab.
2.24 Die continue-Anweisung
Die continue-Anweisung ist der break-Anweisung ähnlich. Sie bewirkt jedoch im
Unterschied zur break-Anweisung nicht den Abbruch der gesamten Schleife, sondern
nur den Abbruch des aktuellen Schleifendurchlaufs, also einen Sprung zum Schleifenende. Dazu muß lediglich
continue;
angegeben werden. continue leitet unverzüglich den nächsten Schleifendurchlauf ein
und hat auch im Gegensatz zu break keine Auswirkung auf switch-Anweisungen.
Bei while und do...while bewirkt continue, daß sofort wieder die Bedingung (ausdruck) ausgewertet wird. Bei for wird als nächstes die Reinitialisierung (ausdruck3)
durchgeführt und dann zur Schleifenbedingung (ausdruck2) verzweigt.
2.25 Marken und goto-Anweisung
goto marke;
Diese Anweisung bewirkt einen Sprung zu der Programmstelle, an der
marke:
angegeben ist.
kap01.fm Seite 29 Mittwoch, 5. Dezember 2001 10:40 10
2.26
Die Konstante EOF
29
goto sollte bis auf wenige Ausnahmefälle vermieden werden. Für marken-Namen gilt
das gleiche wie für Variablennamen (sie müssen mit Buchstaben oder Unterstrich
beginnen, und danach dürfen Buchstaben, Ziffern, Unterstriche folgen); danach folgt
ein Doppelpunkt:
marke:
anweisung;
marken können vor jeder Anweisung stehen, unabhängig davon, ob sie mit goto angesprungen werden oder nicht. Nach einer marke muß immer mindestens eine Anweisung angegeben sein, eventuell auch nur die leere Anweisung, wie z.B.:
marke:
;
In einem Programm darf jede Marke nur einmal angegeben sein.
2.26 Die Konstante EOF
Die Konstante EOF (in stdio.h definiert) ist die Abkürzung für End-Of-File (Dateiende) und kennzeichnet das Ende eines Eingabetextes. Beim Lesen aus einer Datei
steht sie für das Dateiende. Bei interaktiver Eingabe über die Tastatur kann EOF durch
Drücken von (Strg)-(Z) (unter MS-DOS) bzw. (Strg)-(D) (unter Linux/Unix) nachgebildet werden. Ist ein vollständiger Text durch ein C-Programm einzulesen und zu verarbeiten, wird meist folgende Technik angewendet:
#include <stdio.h>
:::
int zeich; /* als int deklarieren, da getchar() int und nicht char liefert */
:::
while ( (zeich=getchar()) != EOF ) {
....Verarbeitung von zeich.....
}
:::
2.27 Zufallszahlen in C
Um den Zufallszahlengenerator aus der C-Bibliothek zu verwenden, empfiehlt sich
folgende Vorgehensweise:
#include <stdlib.h>
#include <time.h>
int
{
main(void)
srand(time(NULL)); /* Zufallszahlengenerator initialisieren */
JEDER AUFRUF VON RAND() LIEFERT NUN EINE ZUFALLSZAHL ZWISCHEN 0 UND RAND_MAX
}
kap01.fm Seite 30 Mittwoch, 5. Dezember 2001 10:40 10
30
2
C-Theorie
Nun wird man aber nicht immer Zufallszahlen aus dem Intervall [0,32767] benötigen,
sondern z.B. aus dem Intervall [1,6], wenn man das Werfen eines Würfels simulieren
möchte. Um dies zu erreichen, muß eine von rand() gelieferte Zufallszahl in das
gewünschte Intervall projiziert werden. Für den Würfel wäre dies möglich mit:
rand()%6+1 /* liefert eine Zufallszahl zwischen 1 und 6 */
Allgemein gilt für eine Projektion in ein Intervall [u,o]:
rand()%(o-u+1)+u
Folgendes Programm simuliert das fünfmalige Werfen eines Würfels:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int
{
/* Fuer NULL, das spaeter verwendet wird */
/* Fuer time(..)
*/
main(void)
int i, augen;
srand(time(NULL));
printf("Bei fünfmaligem Würfeln habe ich folgende Augenzahlen geworfen:\n");
for (i=1 ; i<=5 ; i++) {
augen=rand()%6+1;
printf("%2d.Wurf: %d\n", i, augen);
}
return(0);
}
Bei fünfmaligem Würfeln habe ich folgende Augenzahlen geworfen:
1.Wurf: 6
2.Wurf: 1
3.Wurf: 2
4.Wurf: 1
5.Wurf: 2
Eine andere und sogar etwas genauere Projektionsmethode besteht darin, daß man
sich z.B. eine Grenze im Bereich der Zufallszahlen definiert: Trifft ein Ereignis mit einer
bestimmten Wahrscheinlichkeit ein, so berechnet man eine Grenze im Bereich der
Zufallszahlen und läßt sich dann Zufallszahlen generieren. Liegt die Zufallszahl unter
der Grenze, ist das Ereignis eingetreten. Liegt die Zufallszahl über der Grenze, dann ist
das Ereignis nicht eingetreten.
#define RAND_MAX
0x7fff
...
float grenze;
.......
grenze = wahrscheinlichkeit*RAND_MAX;
if (rand() <= grenze)
kap01.fm Seite 31 Mittwoch, 5. Dezember 2001 10:40 10
2.28
Funktionen
31
Ergebnis ist eingetreten
else
Ergebnis ist nicht eingetreten
.....
2.28 Funktionen
2.28.1 Definition von Funktionen
Eine Funktionsdefinition hat in ANSI C die folgende Form:
datentyp funktionsname( [Parameterdeklarationen] )
{
[Lokale Deklarationen]
......
Funktionsanweisungen
......
}
1
datentyp legt den Datentyp des Wertes fest, den die Funktion als Rückgabewert liefert.
Für die Wahl von Funktionsnamen gelten die gleichen Regeln wie für Variablennamen:
Sie müssen mit Buchstaben oder Unterstrich beginnen, und die weiteren Zeichen dürfen Buchstaben, Ziffern oder Unterstriche sein.
Beispiel
Das folgende Programm definiert eine eigene Funktion hoch(x,y), die einen floatWert x mit einem int-Wert y potenziert, also xy berechnet. Der Funktionsaufruf
hoch(2.5,3) liefert 15.625 (2.53 = 2.5*2.5*2.5).
#include
<stdio.h>
float hoch(float a, int b)
/* Funktionsname(Parameterdeklarationen)
{
int
zaehler;
/*
Lokale Deklarationen
*/
float
rueckgabe_wert=1; /*
*/
if (b<0) {
a = 1/a;
b = -b;
}
for (zaehler=1 ; zaehler<=b ; ++zaehler)
rueckgabe_wert *= a;
return(rueckgabe_wert);
/*
/*
/*
/*
/*
/*
/*
Funktionsanweisungen
*/
*/
*/
*/
*/
*/
*/
*/
}
1. Die in [...] angegebenen Teile sind optional, was bedeutet, daß sie nicht unbedingt angegeben sein
müssen.
kap01.fm Seite 32 Mittwoch, 5. Dezember 2001 10:40 10
32
int
{
2
main(void)
int
float
/*
Funktionsname(leere Liste von formalen Parametern)
exponent;
zahl, ergeb;
printf("x: ");
printf("y: ");
printf("\n");
/*
/*
Lokale Deklarationen
C-Theorie
*/
*/
*/
scanf("%f", &zahl);
scanf("%d", &exponent);
ergeb = hoch(zahl, exponent); /* Aufruf der Funktion
hoch
printf("....%g hoch %d = %g\n", zahl, exponent, ergeb);
*/
/* Wenn der Rueckgabewert (wie hier) nicht zwischengespeichert werden soll */
/* kann hoch auch direkt in printf aufgerufen werden
*/
printf("....%g hoch %d = %g\n", zahl, exponent, hoch(zahl, exponent));
return(0);
}
An diesem Beispiel können die wichtigsten Punkte von Funktionen in C erläutert werden:
왘 Ein C-Programm kann aus einer oder mehreren Funktionen bestehen. Unser C-Programm besteht aus zwei Funktionen mit den Namen main und hoch und den Aufrufen von Standardfunktionen wie printf, scanf.
왘 main ist eine besondere Funktion, da sie den Programmanfang kennzeichnet. Das
heißt, daß in jedem C-Programm eine Funktion mit dem Namen main enthalten
sein muß.
왘 Von der Funktion main aus werden dann normalerweise andere Funktionen aufgerufen. In unserem Beispiel ruft die Funktion main die Funktionen printf und scanf
aus der Standardbibliothek und die selbsterstellte Funktion hoch auf, die vor main
definiert ist.
왘 (void) hinter main zeigt an, daß diese Funktion keine formalen Parameter hat.
왘 Da Funktionen in beliebiger Reihenfolge definiert werden können, hätten wir die
Definition der Funktion hoch auch nach der von main angeben dürfen; man hätte
dabei jedoch einige Punkte berücksichtigen müssen (siehe Seite 35). Man kann
sogar Funktionen für ein Programm in verschiedenen Dateien definieren. Dann
sind natürlich Bezüge zwischen diesen Dateien herzustellen (siehe Seite 49).
왘 In der Funktion main wird mit der Anweisung
ergeb = hoch(zahl, exponent);
die Funktion hoch aufgerufen, d.h. es wird der Programmteil mit dem Namen hoch
ausgeführt, wobei für die formalen Parameter die aktuellen Argumente
a
zahl
b
exponent
eingesetzt werden.
kap01.fm Seite 33 Mittwoch, 5. Dezember 2001 10:40 10
2.28
Funktionen
33
왘 Bei der Definition von hoch müssen die formalen Parameter deklariert werden,
damit ihre Datentypen der Funktion bekannt sind:
float hoch(float a, int b)
Diese Definition legt zusätzlich fest, daß die Funktion hoch als Rückgabe einen
Wert vom Datentyp float liefert.
왘 Nach { werden sogenannte lokale Variablen deklariert:
int
zaehler;
float rueckgabe_wert = 1;
Die hier deklarierten Variablen sind ebenso wie die zuvor deklarierten formalen
Parameter nur innerhalb dieser Funktion bekannt. Hätten wir z.B. in main eine intVariable zaehler deklariert, würde es sich dabei, auch wenn der gleiche Namen
angegeben würde, um eine andere Variable handeln als bei zaehler in der Funktion
hoch.
왘 Die Funktion hoch berechnet nun einen Wert und gibt den berechneten Wert, der
bei unserem Beispiel in der float-Variable rueckgabe_wert liegt, mit Hilfe der
return-Anweisung an die aufrufende Funktion (in unserem Fall an die Funktion
main) zurück:
return(rueckgabe_wert);
왘 Dieser Wert von rueckgabe_wert wird in der Variablen ergeb mit
ergeb = hoch(zahl, exponent);
gespeichert und später am Bildschirm ausgegeben.
왘 Die beiden Anweisungen in unserem Beispiel
ergeb = hoch (zahl, exponent);
printf("...", zahl, exponent, ergeb);
kann man, wenn der Rückgabewert nur temporär gebraucht wird, auch zu einer
Anweisung zusammenfassen, was wir in unserem Programm auch getan haben:
printf("...", zahl, exponent, hoch (zahl, exponent));
2.28.2 Die return-Anweisung
Eine return-Anweisung bewirkt die unmittelbare Beendigung einer Funktion und die
Rückkehr zum Aufrufer der Funktion. Die Syntax für die vollständige return-Anweisung lautet:
return( ausdruck );
gibt den Wert von ausdruck an den Aufrufer zurück, oder
return ausdruck;
gibt ebenso den Wert von ausdruck an den Aufrufer zurück, oder
return;
beendet Funktion ohne Rückgabewert (siehe nächsten Punkt).
kap01.fm Seite 34 Mittwoch, 5. Dezember 2001 10:40 10
34
2
C-Theorie
Wenn der Ausdruck einen Datentyp hat, der sich von dem der zugehörigen Funktion
unterscheidet, dann wird der Wert dieses Ausdrucks in diesen Rückgabetyp konvertiert. Die Umwandlung erfolgt dabei, als ob der Ausdruckswert einer Variablen von
diesem Datentyp zugewiesen würde; siehe auch Implizite Datentypumwandlungen auf
Seite 17.
2.28.3 Funktionen ohne Rückgabewert
Pascal unterscheidet Funktionen und Prozeduren. Prozeduren unterscheiden sich von
Funktionen dadurch, daß sie keinen Wert an Aufrufer zurückgeben. C kennt nur Funktionen. Um Prozeduren in C nachzubilden, muß man als Rückgabe-Datentyp für eine
Funktion den Datentyp void angeben. Folgendes Programm definiert eine Funktion
("Prozedur") ausgabe zur n-maligen Ausgabe eines Zeichens. Das auszugebende Zeichen wird dieser Funktion ebenso als aktuelles Argument übergeben wie die Anzahl,
wie oft es auszugeben ist.
#include
<stdio.h>
/*------ ausgabe ------------------------------------------------------------*/
void ausgabe(unsigned char zeich, int n)
{
int i;
for (i=1 ; i<=n ; i++)
printf("%c", zeich);
printf("\n");
}
/*------ main ---------------------------------------------------------------*/
int main(void)
{
int
zeich, wieoft;
do {
printf("\n\nWelches Zeichen (Abbruch mit 0): ");
zeich=getchar(); fflush(stdin);
if (zeich != '0') {
printf("Wieoft ist dieses Zeichen auszugeben: ");
scanf("%d", &wieoft); fflush(stdin);
ausgabe(zeich, wieoft);
}
} while (zeich != '0');
return(0);
}
Soll eine Funktion keinen Wert an die aufrufende Funktion zurückliefern, ist bei return
kein ausdruck anzugeben. Das beendet die aktuelle Funktion und bewirkt die Rückkehr
zur aufrufenden Funktion (ohne Rückgabe eines Wertes). Man kann in diesem Fall
aber auch vollständig auf return verzichten, wie dies z.B. in der Funktion ausgabe oben
kap01.fm Seite 35 Mittwoch, 5. Dezember 2001 10:40 10
2.28
Funktionen
35
geschehen ist. Es gilt nämlich, daß das Erreichen des Funktionsendes (abschließende })
immer automatisch zu einer Rückkehr (ohne Rückgabe eines Wertes) aus der Funktion
führt.
2.28.4 FORWARD-Deklarationen
Man kann eine Funktion auch erst nach der aufrufenden Funktion definieren. Man
muß dabei aber wissen, daß dann der C-Compiler, der ein Programm von vorn nach
hinten abarbeitet, zum Zeitpunkt des Aufrufs diese Funktion noch nicht kennt. Es gilt
dann in C die folgende Regel:
Findet der C-Compiler den Aufruf einer Funktion, die nicht zuvor definiert oder deklariert ist,
nimmt er als Datentyp für den Rückgabewert immer int an.
Dies kann zum fehlerhaften Verhalten des entsprechenden Programms führen.
Um Fehler dieser Art zu vermeiden, gibt es zwei Möglichkeiten:
1. Die Funktion wird vor ihrem Aufruf definiert.
Dann ist beim Aufruf der Funktion (aufgrund der vorherigen Definition) sowohl
der Rückgabe-Typ als auch der Datentyp der einzelnen Parameter bekannt. Der
Datentyp der einzelnen Parameter ist für den Compiler ebenso wichtig, damit er
die aktuellen Argumente beim Aufruf in den Datentyp des entsprechenden Parameters konvertieren kann; siehe auch Seite 17 (Implizite Datentypumwandlungen).
2. Die Funktion wird vor ihrem Aufruf deklariert.
Eine Funktion deklarieren bedeutet, daß man sie nicht vollständig definiert, sondern nur den Funktionskopf
datentyp funktionsname(Parameterdeklaration);
mit abschließendem Semikolon angibt. So kennt der Compiler bei einem Aufruf
dieser Funktion schon alle erforderlichen Datentypen (des Rückgabewerts und der
Parameter) und kann die notwendigen Schritte durchführen. Die Definition der
Funktion kann sich dann an jeder beliebigen anderen Stelle befinden. Diese Vorgehensweise wird auch mit FORWARD-Deklaration bezeichnet.
Bei der 2. Vorgehensweise benötigt man das von ANSI C eingeführte Prototyping, wie
z.B.:
#include
<stdio.h>
float hoch(float a, int b);
int
{
main()
int
float
/*
/* FORWARD-Deklaration */
Funktionsname(leere Liste von formalen Parametern)
exponent;
zahl, ergeb;
/*
/*
Lokale Deklarationen
*/
*/
*/
kap01.fm Seite 36 Mittwoch, 5. Dezember 2001 10:40 10
36
2
printf("x: ");
printf("y: ");
C-Theorie
scanf("%f", &zahl);
scanf("%d", &exponent);
ergeb = hoch(zahl, exponent); /* Aufruf der Funktion
hoch
printf("....%g hoch %d = %g\n", zahl, exponent, ergeb);
*/
/* Wenn Rueckgabewert (wie hier) nicht zwischengespeichert werden soll */
/* kann hoch auch direkt in printf aufgerufen werden
*/
printf("....%g hoch %d = %g\n", zahl, exponent, hoch(zahl, exponent));
return(0);
}
float hoch(float a, int b)
/* Funktionsname(Parameterdeklarationen)
{
int
zaehler;
/*
Lokale Deklarationen
*/
float
rueckgabe_wert=1; /*
*/
if (b<0) {
a = 1/a;
b = -b;
}
for (zaehler=1 ; zaehler<=b ; ++zaehler)
rueckgabe_wert *= a;
return(rueckgabe_wert);
/*
/*
/*
/*
/*
/*
/*
Funktionsanweisungen
*/
*/
*/
*/
*/
*/
*/
*/
}
2.28.5 Funktionsprototypen
Im ursprünglichen C teilte eine Funktionsdeklaration dem Compiler nur den Datentyp
des Rückgabewerts mit, wie z.B.:
float hoch();
void ausgabe();
ANSI C führte Funktionsprototypen ein, die es ermöglichen, bei der Deklaration einer
Funktion nicht nur den Rückgabe-Datentyp, sondern auch die Typen der einzelnen
Parameter anzugeben, wie:
float
void
hoch(float, int);
ausgabe(unsigned char, int);
Es ist sogar möglich, neben dem Typ eines formalen Arguments noch einen Namen
anzugeben:
float
void
hoch(float zahl, int potenz);
ausgabe(unsigned char zeichen, int n);
Eine Kombination beider Methoden ist zwar zur Wahrung eines guten Programmierstils nicht empfehlenswert, wäre aber auch möglich:
float
void
hoch(float, int potenz);
ausgabe(unsigned char, int n);
kap01.fm Seite 37 Mittwoch, 5. Dezember 2001 10:40 10
2.28
Funktionen
37
2.28.6 Implizite Datentypumwandlung beim Funktionsaufruf
Die Argumente einer Funktion werden beim Aufruf automatisch in die Datentypen
der Parameter (aus der Funktionsdefinition) umgewandelt. Dabei gelten die Regeln
der impliziten Datentypumwandlung, die bereits auf Seite 17 vorgestellt wurden.
2.28.7 Leere Parameterliste durch die Angabe von void
Besitzt eine Funktion keine Parameter, kann natürlich auch kein Parameter deklariert
werden. Für diesen Fall gibt man bei der Funktionsdefinition entweder eine leere Parameterliste oder aber das Schlüsselwort void an. Die zweite Variante ist dabei die bessere.
#include
<stdio.h>
void ueberschrift1(void) /*--- Explizite void-Angabe für leere Parameterliste */
{
printf("Dies ist die Funktion ueberschrift1\n");
}
void ueberschrift2() /*--- keine Parameterangabe kennzeichnet hier eine */
{
/*--- leere Parameterliste
*/
printf("Dies ist die Funktion ueberschrift2\n");
}
int
main(void)
/*--- Auch main ist eine Funktion. Wir verwenden diese
*/
/*--- Funktion hier ohne Parameter und ohne Rückgabewert */
{
ueberschrift1();
ueberschrift2();
return(0);
}
2.28.8 Call by value
Beim Aufruf einer Funktion in C werden nur die Werte der aktuellen Argumente
übergeben.
Das Programm tausch.c soll beispielsweise die Inhalte von zwei Variablen vertauschen:
#include
<stdio.h>
void tausch(int a, int b)
{ int
hilf;
hilf=a;
a=b;
b=hilf;
}
int main(void)
{ int
zahl_1=10, zahl_2=5;
kap01.fm Seite 38 Mittwoch, 5. Dezember 2001 10:40 10
38
2
C-Theorie
printf(" Vor tausch-Aufruf: zahl_1=%d, zahl_2=%d\n", zahl_1, zahl_2);
tausch(zahl_1,zahl_2);
printf("Nach tausch-Aufruf: zahl_1=%d, zahl_2=%d\n", zahl_1, zahl_2);
return(0);
}
Dieses Programm vertauscht nicht (wie erwartet) die Inhalte der beiden int-Variablen
zahl_1 und zahl_2:
Vor tausch-Aufruf: zahl_1=10, zahl_2=5
Nach tausch-Aufruf: zahl_1=10, zahl_2=5
Wie aber ist dann ein Vertauschen möglich? Folgender Abschnitt beantwortet diese
Frage.
2.28.9 Call by reference
Um den Inhalt von Variablen, die außerhalb von Funktionen deklariert wurden, innerhalb von Funktionen zu modifizieren, müssen nicht die Werte, sondern die Adressen
der entsprechenden Variablen als Argumente übergeben werden, wie z.B. im folgenden Programm tausch2.c:
#include
<stdio.h>
void tausch(int *a, int *b)
{ int
hilf;
hilf = *a;
*a = *b;
*b = hilf;
}
int main(void)
{ int
zahl_1=10, zahl_2=5;
printf(" Vor tausch-Aufruf: zahl_1=%d, zahl_2=%d\n", zahl_1, zahl_2);
tausch(&zahl_1, &zahl_2);
printf("Nach tausch-Aufruf: zahl_1=%d, zahl_2=%d\n", zahl_1, zahl_2);
return(0);
}
Dieses Programm vertauscht die Inhalte der beiden int-Variablen zahl_1 und zahl_2:
Vor tausch-Aufruf: zahl_1=10, zahl_2=5
Nach tausch-Aufruf: zahl_1=5, zahl_2=10
kap01.fm Seite 39 Mittwoch, 5. Dezember 2001 10:40 10
2.28
Funktionen
39
Mit dem Aufruf
tausch(&zahl_1, &zahl_2);
werden die Adressen von zahl_1 und zahl_2 als aktuelle Argumente an tausch übergeben. Bei dieser Übergabe werden die Adressen von zahl_1 und zahl_2 in den beiden
reservierten Speicherplätzen a und b der Funktion tausch abgelegt.
2.28.10 Argumentauswertung findet vor Funktionsaufruf statt
Alle Argumente bei einem Funktionsaufruf werden nur einmal vor dem Funktionsaufruf ausgewertet und dann an die entsprechende Funktion übergeben. Bei
#define makromax(x,y) ( (x>y) ? x : y) )
.....
a=5;
b=10;
max = funkmax(++a, ++b);
wird zuerst den Argumenten a der Wert 6 (5+1) und b der Wert 11 (10+1) zugewiesen.
Erst nachdem diese Ausdrücke ausgewertet sind, wird die Funktion funkmax mit den
ermittelten Werten aufgerufen.
Im Gegensatz dazu expandiert der Compiler den Makroaufruf
max = makromax(++a, ++b);
zu folgender Anweisung:
max = ( (++a>++b) ? ++a : ++b ) );
Dies führt dazu, daß zunächst für den Vergleich die beiden Ausdrücke (Argumente)
ausgewertet werden:
++a
++b
(a wird hier der Wert 6 (5+1) zugewiesen)
(b wird hier zunächst der Wert 11 (10+1) zugewiesen)
Dies bedeutet nun, daß b größer als a ist, was dazu führt, daß die Anweisung nach :
(Doppelpunkt) ausgeführt wird, also der Ausdruck (das Argument)
++b
(b wird hier der Wert 12 (11+1) zugewiesen)
erneut ausgewertet wird.
2.28.11 Reihenfolge der Argumentauswertung ist nicht festgelegt
Ob der C-Compiler die Argumente "von links nach rechts" oder aber von "von rechts nach
links" auswertet, ist von C nicht vorgeschrieben. Die einzige ANSI-C-Vorschrift lautet:
Argumente und Funktionsbezeichner müssen vollständig ausgewertet sein, bevor mit der Ausführung des Funktionscodes begonnen wird.
kap01.fm Seite 40 Mittwoch, 5. Dezember 2001 10:40 10
40
2
C-Theorie
Nehmen wir das folgende Programm:
int
funkmax(int x, int y) {
return( (x>y) ? x : y;
}
int main(void)
{ int a=5, b=10, max;
max = funkmax(a=a+b, b=2*b);
printf("a=%d, b=%d, max=%d\n", a, b, max);
return(0);
}
Abhängig von der Abarbeitungsstrategie des Compilers liefert das Programm unterschiedliche Ergebnisse. Bei einem Compiler, der die Argumente "von links nach rechts"
abarbeitet, wird es die folgende Ausgabe liefern:
a=15, b=20, max=20
Dieser Compiler hat also die Argumente "von links nach rechts" abgearbeitet.
a=a+b
b=2*b
(a wird der Wert 15 (5+10) zugewiesen)
(b wird der Wert 20 (2*10) zugewiesen)
Bei einem Compiler, der die Argumente "von rechts nach links" abarbeitet, wird es folgende Ausgabe liefern:
a=25, b=20, max=25
Dieser Compiler hat also die Argumente "von rechts nach links" abgearbeitet.
b=2*b
a=a+b
(b wird der Wert 20 (2*10) zugewiesen)
(a wird der Wert 25 (5+20) zugewiesen)
Portable Programme sollten also die Auswertungsreihenfolge nicht dem Compiler
überlassen, sondern die gewünschte Reihenfolge selbst vor dem Funktionsaufruf definieren, wie z.B.:
왘 Von links nach rechts:
a=a+b;
b=2*b;
max = funkmax(a, b);
왘 Von rechts nach links:
b=2*b;
a=a+b;
max = funkmax(a, b);
kap01.fm Seite 41 Mittwoch, 5. Dezember 2001 10:40 10
2.28
Funktionen
41
2.28.12 Aufrufreihenfolge von Funktionen in Ausdrücken
In einer Anweisung wie
x = funk1() + funk2();
ist nicht festgelegt, ob funk1 vor funk2 aufzurufen ist oder umgekehrt. Die Aufrufreihenfolge ist jedoch bei Funktionen von Wichtigkeit, die den Inhalt derselben Variablen
verändern.
Beispiel
Das folgende Programm zins.c ist nicht portabel, da es die Aufrufreihenfolge der beiden Funktionen zinsen und rueckzahl dem jeweiligen Compiler überläßt.
#include <stdio.h>
#define ZINSSATZ
0.08
float zinsen( float *betr)
{
float zins_betrag = *betr*ZINSSATZ;
*betr += zins_betrag;
return( zins_betrag );
}
float rueckzahl( int rate, float *betra )
{
return( *betra -= rate );
}
int
{
main(void)
float betrag = 10000;
float neu_betrag;
neu_betrag = zinsen(&betrag) + rueckzahl(100, &betrag);
printf("Neuer Betrag ist: %.2f\n", neu_betrag);
printf("Wert von betrag ist: %.2f\n", betrag);
return(0);
}
Dieses Programm könnte abhängig von der Aufrufreihenfolge, die der jeweilige Compiler benutzt, unterschiedliche Ergebnisse liefern:
1. Aufruf von zinsen erfolgt vor rueckzahl.
Neuer Betrag ist: 11500.00
Wert von betrag ist: 10700.00
kap01.fm Seite 42 Mittwoch, 5. Dezember 2001 10:40 10
42
2
C-Theorie
2. Aufruf von zinsen erfolgt nach rueckzahl.
Neuer Betrag ist: 10692.00
Wert von betrag ist: 10692.00
Portable Programme sollten die Aufrufreihenfolge von Funktionen in Ausdrücken
nicht dem jeweiligen Compiler überlassen, sondern die gewünschte Aufrufreihenfolge
selbst festlegen, wie z.B.:
왘 Aufruf von zinsen soll vor rueckzaehl erfolgen:
neu_betrag = zinsen(&betrag);
neu_betrag += rueckzahl(100, &betrag);
왘 Aufruf von zinsen soll nach rueckzaehl erfolgen:
neu_betrag = rueckzahl(100, &betrag);
neu_betrag += zinsen(&betrag);
2.28.13 Ellipsen-Prototypen
Funktionen, die eine variable Anzahl von Argumenten erwarten, müssen mit sogenannten Ellipsen-Prototypen deklariert werden, wie z.B.:
int printf(const char *format, ...);
Die drei Punkte (Ellipse) bei einer Prototyp-Deklaration deuten an, daß beim Aufruf
von printf neben einem fest vorgeschriebenen Parameter format beliebig weitere aktuelle Argumente angegeben werden können.
2.29 Verfahren zum Abarbeiten variabel langer Argumentlisten
Es gibt viele Möglichkeiten, in einer Funktion, die eine variabel lange Argumentliste
erlaubt, auf die einzelnen unbenannten Argumente innerhalb dieser Funktion zuzugreifen.
Vier Verfahren sind nachfolgend anhand eines Beispiels vorgestellt. Dabei soll eine
Funktion vieladd erstellt werden, die die übergebenen Werte addiert und die Summe
zurückgibt. Diese Funktion soll mit einer unterschiedlichen Zahl von zu addierenden
Werten aufgerufen werden können.
2.29.1 Argumentzahl wird beim Aufruf als erstes Argument übergeben
#include
#include
float
{
<stdio.h>
<stdarg.h>
viel_add1(int n, ...)
kap01.fm Seite 43 Mittwoch, 5. Dezember 2001 10:40 10
2.29
Verfahren zum Abarbeiten variabel langer Argumentlisten
va_list
float
int
43
arg_zeiger;
wert, ergeb=0;
i;
va_start(arg_zeiger, n); /*--- Zugriff auf 1. Argument (noch benannt) -*/
for (i=1 ; i<=n ; i++) { /*--- Lesen der restlichen Argumente (unbenannt) ---*/
wert = va_arg(arg_zeiger, double);
ergeb += wert;
}
va_end(arg_zeiger); /*--- Stack wieder in sauberen Zustand versetzen---*/
return(ergeb);
}
int main(void)
{
float summe;
summe = viel_add1(1, 123.45);
/* Aufruf mit 1 Argument */
summe = viel_add1(3, 2.3, 3.5, 100.2);
/* Aufruf mit 3 Argumenten */
summe = viel_add1(5, -10.5, 300.0, 20.5, -50.0, 10.3); /* Aufruf mit 5 Arg. */
}
2.29.2 Ende der Argumentliste wird durch speziellen Wert gekennzeichnet
#include
#include
<stdio.h>
<stdarg.h>
float viel_add2(double zahl1, ...)
{
va_list
arg_zeiger;
double
wert, ergeb;
va_start(arg_zeiger, zahl1); /*--- Zugriff auf 1. Argument (noch benannt) -*/
ergeb = zahl1;
/*--- Lesen der restlichen Argumente (unbenannt); Ende, wenn Argument = 0 --*/
while ( (wert=va_arg(arg_zeiger, double)) != 0 )
ergeb += wert;
va_end(arg_zeiger); /*--- Stack wieder in sauberen Zustand versetzen---*/
return(ergeb);
}
int main(void)
{
float summe;
summe = viel_add2(123.45, 0.0);
/* Ende der Arg.liste durch Wert 0 angezeigt */
summe = viel_add2(2.3, 3.5, 100.2, 0.0);
summe = viel_add2(-10.5, 300.0, 20.5, -50.0, 10.3, 0.0);
}
kap01.fm Seite 44 Mittwoch, 5. Dezember 2001 10:40 10
44
2
C-Theorie
2.29.3 Kennzeichnen jedes Element-Typs durch vorangestelltes Formatelement
Hier muß jedem echten Wert in der Argumentliste ein Formatelement vom Typ char
vorangestellt werden:
'c' oder 'd':
'l'
'f'
'z'
e'
#include
#include
#include
für char- oder int-Werte
für long-Werte
für float- oder double-Werte
für long double-Werte
für Ende der Argumentliste
<stdio.h>
<ctype.h>
<stdarg.h>
long double viel_add3(char format, ...)
{
va_list
arg_zeiger;
long double
wert, ergeb=0;
va_start(arg_zeiger, format); /*--- Zugriff auf 1. Argument (noch benannt) -*/
/*--- Lesen der restlichen Argumente (unbenannt); Ende, wenn Argument = 0 --*/
while (format != 'e') {
switch (tolower(format)) {
case 'c':
case 'd': wert = va_arg(arg_zeiger, int); break;
case 'l': wert = va_arg(arg_zeiger, long); break;
case 'f': wert = va_arg(arg_zeiger, double); break;
case 'z': wert = va_arg(arg_zeiger, long double); break;
}
ergeb += wert;
format = va_arg(arg_zeiger, int); /*--- Naechstes Formatzeichen lesen -*/
}
va_end(arg_zeiger); /*--- Stack wieder in sauberen Zustand versetzen---*/
return(ergeb);
}
int
{
main(void)
long double sum;
sum = viel_add3('d', -200, 'f', 0.5, 'c', 'x', 'l', 1000000, 'z', 1e9L, 'e');
}
2.29.4 Aufruf der Funktion vprintf
Die Funktion vprintf ist wie folgt in der Headerdatei <stdarg.h> deklariert:
int vprintf(const char *format, va_list arg)
vprintf ist äquivalent zur Funktion printf, wobei jedoch die variabel lange Argumentliste durch den Parameter arg (vom Typ va_list) ersetzt ist. arg sollte zuvor durch
einen Aufruf von va_start initialisiert worden sein. vprintf ruft auch nicht das Makro
va_end auf. vprintf läßt sich z.B. einsetzen, um eine eigene Fehlermeldungsroutine zu
erstellen, die genauso aufgerufen wird wie printf.
kap01.fm Seite 45 Mittwoch, 5. Dezember 2001 10:40 10
2.30
Rekursive Funktionen
#include
#include
45
<stdio.h>
<stdarg.h>
/*------- fehl_meld --------------------------------------------*/
void fehl_meld(int kennung, const char *fmt, ...)
{
va_list az;
va_start(az, fmt);
switch (kennung) {
case 0:
vprintf(fmt, az); printf("\n");
case 1: printf("Warnung: "); vprintf(fmt, az); printf("\n");
case 2: printf("Fehler: "); vprintf(fmt, az); printf("\n");
default: fehl_meld(2, "Falscher Aufruf von fehler_meld....", az);
}
va_end(az);
}
int main(void)
{
double wert = 3.0/7;
fehl_meld(0,
fehl_meld(0,
fehl_meld(1,
fehl_meld(2,
fehl_meld(3,
int
break;
break;
break;
break;
zahl = 125;
"%s ist %.6lf", "Drei geteilt durch sieben", wert);
"Hans ist %d alt", 34);
"Illegale Eingabe '%d....", zahl);
"Du musst Zahlen aus Intervall [%d, %d] eingeben", 5, 10);
"%d mal %.3lf = %.3lf", zahl, wert, zahl*wert);
}
2.30 Rekursive Funktionen
C-Funktionen können auch rekursiv aufgerufen werden, d.h., eine Funktion darf sich
selbst wieder aufrufen. Ein solcher rekursiver Aufruf kann entweder direkt oder auf
Umwegen über andere Funktionsaufrufe erfolgen.
Das folgende Programm, fakul.c, berechnet zu einer Zahl n, die einzugeben ist, die
Fakultät
n! = 1 * 2 * 3 * ... n
#include
long
int
{
<stdio.h>
fakul(int n);
/*--- liefert die Fakultaet n! = 1*2*3*...*n */
main(void)
int
(Ausnahme 0! = 1)
bis;
printf("Fakultaet von ? ");
scanf("%d", &bis);
kap01.fm Seite 46 Mittwoch, 5. Dezember 2001 10:40 10
46
2
C-Theorie
printf("
..... %d! = %ld .....\n", bis, fakul(bis));
return(0);
}
long fakul(int zahl)
{
long int ergeb;
if (zahl>0)
ergeb = zahl*fakul(zahl-1); /* Hier ist die Rekursion */
else
ergeb = 1;
/* Ende der Rekursion */
return(ergeb);
}
Alle lokalen Variablen werden bei jedem rekursiven Funktionsaufruf neu auf dem
Stack angelegt, so daß jeder Aufruf der Funktion in einer rekursiven Aufruffolge seine
"privaten" Variablen besitzt. Im Beispiel werden bei jedem Aufruf von fakul neue Speicherplätze für ergeb und zahl angelegt, so ist z.B. ergeb in fakul(3) unabhängig von ergeb
in fakul(2).
Das folgende Programm, rueckwae.c, liest nacheinander Zeichen ein, die es dann in
umgekehrter Reihenfolge wieder ausgibt. Das Ende einer solchen Zeichenkette wird
durch die Eingabe von EOF ((Strg)-(D) unter Linux/Unix und (Strg)-(Z) unter MSDOS) gekennzeichnet.
#include
void
{
<stdio.h>
umdrehen(void)
int
zeichen;
zeichen = getchar();
if (zeichen != EOF) {
umdrehen(); /* Hier ist die Rekursion */
putchar(zeichen);
}
}
int
{
main(void)
printf("Geben Sie Text ein (Ende = EOF)!\n");
umdrehen();
return(0);
}
Typische Anwendungen für rekursive Funktionen werden im Algorithmen-Teil (siehe
Kapitel 4.6 und 4.7) gezeigt.
kap01.fm Seite 47 Mittwoch, 5. Dezember 2001 10:40 10
2.31
Zeiger auf Funktionen
47
2.31 Zeiger auf Funktionen
Ein Zeiger kann nicht nur auf eine Variable, sondern auch auf Funktionen positioniert
werden. Ein Zeiger, der auf eine Funktion gerichtet wird, zeigt auf den Anfang des
Funktionscodes.
Beim folgenden Programm kann der Benutzer steuern, ob er das Volumen eines Zylinders (r*r*pi*hoehe) oder einer Kugel (4/3*r*r*r*pi) berechnen läßt.
#include <stdio.h>
#include <math.h>
#include <ctype.h>
#define PI 4*atan(1)
float berech( float rad, float (*welch_funktion)(float rad) )
{
return(rad* (*welch_funktion)(rad));
}
float zyl_vol(float r) { return(r*PI); }
float kug_vol(float r) { return(r*r*4/3*PI); }
int main(void)
{
float
radius, hoehe;
char
wahl;
printf("\nVolumen eines Zylinders oder einer Kugel ( Z/K ) ? ");
wahl = toupper(getchar());
printf("\nGeben Sie Radius ein: "); scanf("%f", &radius);
if (wahl=='Z') {
printf("Geben Sie Hoehe des Zylinders ein: ");
scanf("%f", &hoehe);
printf(" ---> Volumen (Zylinder): %f\n", hoehe*berech(radius,zyl_vol));
} else
printf(" ---> Volumen (Kugel): %f\n", berech(radius,kug_vol));
}
Mit folgendem Aufruf wird die Adresse der Funktion zyl_vol an berech übergeben:
berech(radius, zyl_vol); /* auch möglich:
berech(radius, &zyl_vol);
*/
Mit folgendem Aufruf wird die Adresse der Funktion kug_vol an berech übergeben:
berech(radius, kug_vol);
/* auch möglich:
berech(radius, &kug_vol);
*/
Mit folgender Parameterdeklaration in der Funktion berech wird ein Zeiger
welch_funktion auf die Funktion vereinbart, die den float-Wert liefert:
float (*welch_funktion)(float rad)
kap01.fm Seite 48 Mittwoch, 5. Dezember 2001 10:40 10
48
2
C-Theorie
Will man nun über den Zeiger welch_funktion eine Funktion aufrufen, muß man
*welch_funktion angeben, da welch_funktion ein Zeiger und folglich *welch_funktion die
eigentliche Funktion ist.
Wenn z.B. welch_funktion auf die Funktion zyl_vol zeigt, dann entspricht die Anweisung
return (rad*(*welch_funktion)(rad));
der Anweisung
return (rad * zyl_vol(rad));
Hätte man bei der Deklaration von welch_funktion in der Funktion berech die Klammern
weggelassen:
float *welch_funktion(...);
hätte dies bedeutet, daß welch_funktion eine Funktion ist, die einen Zeiger auf einen
float-Wert zurückliefert.
Der folgende Unterschied ist wichtig:
float (*fkt_zgr)(...); /* fkt_zgr = Zeiger auf Funktion,
die float-Wert liefert */
float *fkt(...); /* fkt = Funktion,
die Zeiger auf float-Wert liefert */
Um das Zeigerprinzip bei Funktionen besser zu verdeutlichen, hier ein weiteres Beispiel:
float *(*zgr_fkt)(...);
Hier wäre zgr_fkt ein Zeiger auf eine Funktion, die einen Zeiger auf einen float-Wert
liefert.
(Siehe auch Kapitel 2.36, in dem das Erstellen und Verstehen komplexer Datentypen
genauer beschrieben wird, bzw. Seite 371, wo ein Programm vorgestellt wird, mit dem
man automatisch komplexe Datentypen erstellen bzw. analysieren lassen kann.)
2.32 Gültigkeitsbereich, Lebensdauer und Speicherort
2.32.1 Gültigkeitsbereich
C-Programme können sich aus mehreren Dateien zusammensetzen. Jede Programmdatei (Modul) kann einzeln kompiliert werden, ist aber für sich allein nicht lauffähig.
Getrennt kompilierte Module können mit dem Linker zu einem Programm zusammengebunden werden.
kap01.fm Seite 49 Mittwoch, 5. Dezember 2001 10:40 10
2.32
Gültigkeitsbereich, Lebensdauer und Speicherort
49
Unter dem Gültigkeitsbereich eines Objekts (Funktionsname, Variablenname) versteht
man die Bereiche in einzelnen Programmeinheiten, in denen das Objekt angesprochen
werden kann. Beim Gültigkeitsbereich wird zwischen folgenden Arten unterschieden:
modulglobal, programmglobal, lokal
Modulglobale Gültigkeit
Ein modulglobales Objekt ist innerhalb der gesamten Programmdatei (Modul)
ansprechbar, in der es deklariert ist, aber nicht in einer anderen Programmdatei
(Modul). Jede Variable, die außerhalb einer Funktion deklariert ist, bezeichnet man als
modulglobal. Da modulglobale Variablen außerhalb von Funktionen vereinbart sind
und somit vielen Funktionen zur Verfügung stehen, kann innerhalb von Funktionen
auf modulglobale Variablen zugegriffen werden, ohne daß diese als Parameter zu
übergeben sind. Im folgenden Programm werden mit der Funktion tausch die Werte
zweier Variablen vertauscht.
#include
int
<stdio.h>
zahl_1, zahl_2;
/*------ modulglobale
Variablen ----*/
void tausch(void)
{
int hilf;
hilf = zahl_1;
zahl_1 = zahl_2;
zahl_2 = hilf;
}
int
{
main(void)
zahl_1 = 10; zahl_2 = 5;
tausch();
}
Auch gilt, daß alle in einer Programmdatei vereinbarten Funktionen modulglobale
Gültigkeit besitzen, was bedeutet, daß jede in einer Programmdatei definierte Funktion eine andere Funktion, die auch in dieser Programmdatei vereinbart wurde, aufrufen kann.
Programmglobale Gültigkeit
Ein programmglobales Objekt ist von mehreren Programmdateien (Modulen) aus
ansprechbar, d.h von allen Programmdateien, die das Objekt als extern deklarieren.
Im Unterschied zu modulglobalen Objekten, deren Gültigkeit auf die Programmdatei
(Modul) beschränkt ist, in der sie deklariert sind, erstreckt sich der Gültigkeitsbereich
von programmglobalen Objekten über mehrere Programmdateien.
kap01.fm Seite 50 Mittwoch, 5. Dezember 2001 10:40 10
50
2
C-Theorie
Lokale Gültigkeit und Privatisierungseffekt in Blöcken
Ein lokales Objekt ist nur innerhalb des Programmteils, d.h. in einer Funktion oder in
einem Block von Anweisungen (mit {...} geklammert) ansprechbar, in dem es deklariert
ist.
Es gelten folgende Regeln:
왘 Jedes C-Programm setzt sich aus Funktionen zusammen, und jede einzelne Funktion in einem Programm besitzt mindestens einen Block, in dem weitere Blöcke verschachtelt sein können.
왘 Innerhalb eines jeden Blocks können unmittelbar nach { weitere Variablen definiert
werden. Diese sind dann allerdings nur innerhalb dieses Blocks bekannt.
왘 Werden in einem Block bei Variablendeklarationen Namen angegeben, die schon
außerhalb vergeben wurden, so ist die "innere" Variable völlig unabhängig von der
"äußeren", und innerhalb des Blocks kann nur auf die "innere" Variable, nicht aber
auf die "äußere" zugegriffen werden. Die "äußere" Variable kann erst nach dem Verlassen des Blocks wieder angesprochen werden.
왘 Wird im Block auf eine Variable zugegriffen, deren Name nicht in diesem Block
lokal deklariert wurde, wird eine Variable angesprochen, die außerhalb dieses
Blocks vereinbart wurde.
Der Gültigkeitsbereich von Variablen erstreckt sich also auf den Block, in dem sie
deklariert wurden, und auf alle untergeordneten Blöcke, in denen der gleiche Name
nicht neu vereinbart wurde.
Vergessen Sie nicht, daß blocklokale Variablen außerhalb des entsprechenden Blocks
nicht mehr zur Verfügung stehen.
Beispiel
#include
int
{
<stdio.h>
main(void)
int
x;
x=10;
{
int x;
int sum;
sum=0;
for (x=1 ; x<=100 ; x=x+2)
sum += x;
/* x im aeusseren Block */
/* aeusseres x wird benutzt */
/* x im inneren Block */
/* inneres x
*/
/* wird benutzt */
printf("Die Summe der ungeraden Zahlen von 1 bis 100 ist %d\n",sum);
kap01.fm Seite 51 Mittwoch, 5. Dezember 2001 10:40 10
2.32
Gültigkeitsbereich, Lebensdauer und Speicherort
printf("x = %d\n", x);
51
/* inneres x wird ausgegeben */
/* Ausgabe : x = 101
*/
}
printf("x = %d\n",x);
/* aeusseres x wird ausgegeben
/* Ausgabe : x = 10
*/
*/
return(0);
}
Die Summe der ungeraden Zahlen von 1 bis 100 ist 2500
x = 101
x = 10
Da eine neue Deklaration einer Variablen mit schon vorhandenen Namen in einem
untergeordeten Block eine völlig unabhängige Variable festgelegt, wäre es möglich,
auch einen anderen Datentyp für eine gleichnamige Variable anzugeben, wie etwa:
int v;
funk1()
{
double v;
...
}
2.32.2 Lebensdauer
Eine weitere Eigenschaft eines Objekts, die von seinem Gültigkeitsbereich (programmglobal, modulglobal, lokal) unabhängig ist, ist seine Lebensdauer. Die gibt es in zwei
Formen:
Statische Lebensdauer
Hierzu gehören Objekte, die während der ganzen Programmlaufzeit einen festen Platz
im Speicher besitzen.
Kurzzeitige Lebensdauer (automatic)
Dieser Klasse werden Variablen zugeordnet, die nur für die Dauer eines Funktionsaufrufs oder der Ausführung eines Blocks von Anweisungen (mit {...} geklammert) definiert sind.
Variablen dieser Kategorie werden nicht an einem festen Platz, sondern im Stack
gespeichert. Der Stackbereich einer Funktion wird beim Verlassen der Funktion wieder
freigegeben. Beim erneuten Aufruf der Funktion werden die entsprechenden Variablen
wieder neu auf dem Stack angelegt.
kap01.fm Seite 52 Mittwoch, 5. Dezember 2001 10:40 10
52
2
C-Theorie
2.32.3 Speicherort
Ein Objekt kann im Speicher oder in einem Prozessor-Register gehalten werden.
2.32.4 Das Schlüsselwort extern
Lebensdauer:
statisch,
Gültigkeitsbereich: programmglobal,
Speicherort:
im Datensegment
2.32.5 Das Schlüsselwort auto (überflüssig)
Lebensdauer:
kurzzeitig,
Gültigkeitsbereich: lokal,
Speicherort:
im Stack
2.32.6 Das Schlüsselwort static
static innerhalb einer Funktion
Lebensdauer:
statisch,
Gültigkeitsbereich: lokal,
Speicherort:
im Datensegment
Hierbei ähneln static-Variablen den auto-Variablen. Der Unterschied besteht darin,
daß lokale static-Variablen nach ihrer Rückkehr aus einer Funktion erhalten bleiben.
static-Variablen in einer Funktion sind "private", aber doch ständig vorhandene Variablen dieser Funktion.
static außerhalb einer Funktion
Lebensdauer:
statisch,
Gültigkeitsbereich: modulglobal,
Speicherort:
im Datensegment
Eine static-Variable, die außerhalb einer Funktion deklariert ist, besitzt nur in dem
Modul, in dem sie deklariert ist, Gültigkeit. Sie ist eine modulglobale Variable, auf die
in keiner anderen Programmdatei zugegriffen werden kann. Jedoch kann jede Funktion aus diesem Modul eine solche Variable benutzen, d.h. lesen oder verändern. Solche Variablen reservieren nicht nur für die gesamte Programmlaufzeit Speicherplatz,
sondern ermöglichen auch eine Art von "Privatsphäre" für ein Modul.
2.32.7 Das Schlüsselwort register
Lebensdauer:
kurzzeitig,
Gültigkeitsbereich: lokal,
Speicherort:
im Register
kap01.fm Seite 53 Mittwoch, 5. Dezember 2001 10:40 10
2.33
Die Schlüsselwörter const und volatile
53
Eine register-Definition dient hauptsächlich zur Erhöhung der Ablaufgeschwindigkeit
eines Programms. Der Speicherplatz einer register-Variablen wird in der CPU reserviert. In der CPU können Daten wesentlich schneller verarbeitet werden als im
Arbeitsspeicher.
Hinweis: Der Adreßoperator & darf nicht auf register-Variablen angewendet werden.
2.33 Die Schlüsselwörter const und volatile
2.33.1 Das Schlüsselwort const
const teilt dem Compiler mit, daß das zugehörige Objekt nicht modifiziert werden
darf, d.h., nach dieser Deklaration darf einem solchen Objekt weder ein Wert zugewiesen werden noch darf es inkrementiert oder dekrementiert werden.
const und Zeiger
Einige Besonderheiten sind im Zusammenhang mit Zeigern und const zu beachten:
const int
*zgr_auf_konstante = &variable;
Der Inhalt des Speicherplatzes, auf welchen zgr_auf_konstante zeigt, darf beim
Zugriff über zgr_auf_konstante nicht verändert werden. zgr_auf_konstante selbst
dagegen darf verändert werden. So wäre z.B.
zgr_auf_konstante++;
erlaubt, aber
(*zgr_auf_konstante)++; nicht erlaubt.
int *const
konstanter_zgr = &variable;
Hier darf der Inhalt, auf den konstanter_zgr zeigt, verändert werden, aber
konstanter_zgr selbst darf nicht modifiziert werden. So wäre z.B.
konstanter_zgr++;
nicht erlaubt, aber
(*konstanter_zgr)++; erlaubt.
const int *const
konst_zgr_auf_konst = &variable;
Hier darf weder der Inhalt, auf den konst_zgr_auf_konst zeigt, noch der Zeiger
konst_zgr_auf_konst selbst verändert werden. So wäre z.B.
konst_zgr_auf_konst++;
nicht erlaubt und
(*konst_zgr_auf_konst)++; nicht erlaubt.
const-Parameter bei Funktionsdefinitionen
In der Parameterliste von Funktionsdefinitionen kann ebenfalls das Schlüsselwort
const verwendet werden, wie z.B. die Prototyp-Deklarationen von den Funktionen
printf und scanf aus <stdio.h> zeigen:
int
int
printf(const char *format, ...);
scanf(const char *format, ...);
kap01.fm Seite 54 Mittwoch, 5. Dezember 2001 10:40 10
54
2
C-Theorie
Als erstes Argument wird diesen Funktionen ein String (char-Zeiger) übergeben,
wobei dieser String (auf den der char-Zeiger zeigt) von der Funktion nicht verändert
werden kann.
2.33.2 Das Schlüsselwort volatile
volatile kann als Gegenstück zu const verstanden werden: Es sollte für Variablen verwendet werden, die nicht nur durch das Programm selbst, sondern auch jederzeit von
"außerhalb" (z.B. durch Interrupts) verändert werden können. Bei Angabe dieses
Schlüsselworts muß der Compiler sicherstellen, daß jedes vom Programmierer vorgegebene Lesen und Beschreiben eines volatile-Objekts genau so stattfindet, wie vorgegeben. Ein Compiler darf also vorgegebene Lese- oder Schreiboperationen auf volatileObjekte nicht "wegoptimieren".
2.33.3 Kombination von const und volatile
Die Kombination der beiden Schlüsselwörter ist auch möglich. Die Deklaration
extern const volatile int
real_time_clock;
bedeutet, daß der Inhalt von real_time_clock zwar von der Hardware verändert werden
darf, aber es kann dieser Variablen vom Programm aus weder ein Wert zugewiesen
werden, noch kann sie von dort aus inkrementiert oder dekrementiert werden.
2.34 Arrays und Zeiger
2.34.1 Eindimensionale Arrays
Arraydefinition
char buchst[26];/* eindim. Array mit 26 Elementen vom Typ char */
Die Anzahl der Elemente wird bei der Definition in eckigen Klammern angegeben. Mit
obiger Definition wird ein zusammenhängender Speicherblock mit Namen buchst und
mit 26 aufeinanderfolgenden Elementen (hier also 26 char-Variablen) reserviert. Mit
obiger Definition werden also auf einmal 26 char-Variablen (buchst[0], buchst[1],
buchst[2], ..., buchst[25]) festgelegt.
Sind n Elemente (hier: n=26) für ein Array definiert, erfolgt die Adressierung über Indizes von
0 bis n-1. Beachten Sie, daß jedes Array mit der Nummer 0 und nicht mit 1 beginnt.
Beispiel
int tage[32];
definiert ein Array tage mit 32 int-Variablen tage[0], ..., tage[31].
int *zeig[20];
definiert ein Array zeig mit 20 Zeigern zeig[0], ..., zeig[19] auf int-Variablen.
kap01.fm Seite 55 Mittwoch, 5. Dezember 2001 10:40 10
2.34
Arrays und Zeiger
55
Nur statische Arrays sind erlaubt; d.h., bei der Arraydefinition muß die obere Grenze
eine Konstante sein:
datentyp arrayname[konstante];
Speicherplatz für ein Array = Anzahl der Bytes eines Elements * Arraylänge
So wird z.B. durch die folgende Arraydefinition
int monat[12];
ein Speicherplatz von 24 Bytes (12 * 2 Bytes für int) belegt.
Wenn der Datentyp double durch 8 Bytes realisiert ist, so belegt die folgende Definition:
double werte[1000];
einen Speicherplatz von 8000 Bytes (1000 * 8 Bytes für double)
Die Größe des durch ein Array belegten Speicherplatzes läßt sich auch mit dem sizeofOperator ermitteln. Folgendes Programm verdeutlicht dies, indem es sizeof sowohl
auf den Arraynamen als auch auf den Datentyp selbst (wie sizeof(char[100])) anwendet.
Beide Operationen liefern das gleiche Ergebnis:
#include <stdio.h>
#define GROS
100
int main(void)
{
char
c[GROS];
short
s[GROS];
int
i[GROS];
long
l[GROS];
float
f[GROS];
double d[GROS];
printf("c[%d] =
printf("s[%d] =
printf("i[%d] =
printf("l[%d] =
printf("f[%d] =
printf("d[%d] =
}
%d
%d
%d
%d
%d
%d
Bytes
Bytes
Bytes
Bytes
Bytes
Bytes
(%d)\n",
(%d); ",
(%d); ",
(%d)\n",
(%d); ",
(%d)\n",
GROS,
GROS,
GROS,
GROS,
GROS,
GROS,
sizeof(c),
sizeof(s),
sizeof(i),
sizeof(l),
sizeof(f),
sizeof(d),
sizeof(char[GROS]));
sizeof(short[GROS]));
sizeof(int[GROS]));
sizeof(long[GROS]));
sizeof(float[GROS]));
sizeof(double[GROS]));
c[100] = 100 Bytes (100)
s[100] = 200 Bytes (200); i[100] = 200 Bytes (200); l[100] = 400 Bytes (400)
f[100] = 400 Bytes (400); d[100] = 800 Bytes (800)
Vorsicht: Arrayindizes erstrecken sich nur von 0 bis n-1
Ein häufiger Fehler ist, daß man ein Array der Größe n mit einer for-Schleife der folgenden Art beschreibt:
kap01.fm Seite 56 Mittwoch, 5. Dezember 2001 10:40 10
56
2
C-Theorie
for (i=1; i<=n ; i++)
array[i] = ...;
Der Fehler besteht darin, daß man annimmt, das Array erstrecke sich über die Elemente 1..n, aber es erstreckt sich nur über die Elemente 0..n-1, was dazu führt, daß
beim letzten Durchlauf der for-Schleife (i ist dann n) mit array[n] = ....; bereits in fremden Speicherplatz geschrieben wird. Um dieses Überschreiben in Anwendungen, bei
denen eine Indizierung von 1 bis n naheliegender ist, zu vermeiden, läßt man häufig
das 0. Element ungenutzt. Man wendet dann oft folgende Technik an:
#define MAX_ELEMENTE
.....
datentyp
konstante
array_name[MAX_ELEMENTE+1];
2.34.2 Mehrdimensionale Arrays
In der Praxis benötigt man häufig auch mehrdimensionale Arrays, wie z.B. zwei-, drei, ..., n-dimensionale Arrays.
Zweidimensionale Arrays
Sehr häufig werden zweidimensionale Arrays, wie z.B. für Tabellen oder Matrizen
gebraucht.
datentyp
arrayname[zeilen] [spalten];
datentyp bestimmt den Datentyp der einzelnen Elemente des zweidimensionalen
Arrays. zeilen und spalten bestimmen die Anzahl der Zeilen und Spalten des zweidimensionalen Arrays.
Beispiel
int tabelle[5][4];
definiert ein zweidimensionales Array mit 5 Zeilen ([0]...[4]) und 4 Spalten
([0]...[3]).
Folgendes Programm speichert eine Investionstabelle und gibt diese dann aus:
#include <stdio.h>
void striche(char zeich)
{ int i;
for (i=1 ; i<=52 ; i++)
printf("%c", zeich);
printf("\n");
}
int main(void)
{
int invest[3][4], i, j;
kap01.fm Seite 57 Mittwoch, 5. Dezember 2001 10:40 10
2.34
Arrays und Zeiger
invest[0][0]
invest[0][1]
invest[0][2]
invest[0][3]
=
=
=
=
10532;
8955;
9374;
11783;
invest[1][0]
invest[1][1]
invest[1][2]
invest[1][3]
=
=
=
=
9743;
12377;
11539;
13893;
invest[2][0]
invest[2][1]
invest[2][2]
invest[2][3]
=
=
=
=
3747;
5988;
10782;
12977;
printf("\n\n%15s|", " ");
for (i=1994 ; i<=1997 ; i++)
/* Ausgabe der Jahre
printf("%7d |", i);
printf("\n");
striche('=');
for (i=0 ; i<=2 ; i++) {
printf("%13s%d |","Abteilung",i+1);
for (j=0 ; j<=3 ; j++)
printf("%7d |",invest[i][j]);
printf("\n");
striche ( (i==2) ? '=' : '-' );
}
return(0);
57
*/
}
Dieses Programm gibt folgendes aus:
|
1994 |
1995 |
1996 |
1997 |
====================================================
Abteilung1 | 10532 |
8955 |
9374 | 11783 |
---------------------------------------------------Abteilung2 |
9743 | 12377 | 11539 | 13893 |
---------------------------------------------------Abteilung3 |
3747 |
5988 | 10782 | 12977 |
====================================================
An der Zuweisung invest [0][3] = 11783; (nicht invest[0,3] = 11783; wie in anderen
Sprachen) können wir erkennen, daß in C ein zweidimensionales Array in Wirklichkeit
ein eindimensionales Array ist. Über die Indizes wird dabei eine zweidimensionale
Darstellung nachgebildet.
Drei-, vier-, fünf- und sonstige mehrdimensionale Arrays
Es können natürlich nicht nur zweidimensionale Arrays, sondern auch drei-, vier- und
sonstige mehrdimensionale Arrays definiert werden, wie z.B. das folgende fünfdimensionale Array
kap01.fm Seite 58 Mittwoch, 5. Dezember 2001 10:40 10
58
2
double
C-Theorie
x[10][200][50][30][100];
Diese Definition führt bereits zu einer Speicheranforderung von
10*200*50*30*100 = 300 Millionen * 8 Byte (für double) ---> mehr als 2 GigaByte.
2.34.3 Arrayinitialisierungen
Bei der Initialisierung von ein- und mehrdimensionalen Vektoren muß die Werteliste in
geschweiften Klammern eingebettet werden, wie z.B.:
int investition[3][4] = { 10532, 8955, 9374, 11783, 9743, 12377,
11539, 13893, 3747, 5988, 10782, 12977 };
Hier werden die einzelnen Werte in der Reihenfolge genannt, in der sie im Speicher
abgelegt werden. Diese Form der Initialisierung entspricht der folgenden Form:
int investition[3][4] = { { 10532, 8955, 9374, 11783 },
{ 9743, 12377, 11539, 13893 },
{ 3747, 5988, 10782, 12977 } };
Bei dieser zweiten Initialisierungsform für mehrdimensionale Arrays werden durch
Setzen von geschweiften Klammern die verschiedenen Ebenen des definierten Objekts
nachgebildet. Diese Form ist der ersten Form vorzuziehen, da sie besser lesbar ist.
Bei der folgenden Initialisierung
char vokale[5] = {'A', 'E', 'I', 'O', 'U'};
ist zu beachten, daß das char-Array vokale mit fünf Buchstaben gefüllt wird und nicht
mit \0 abgeschlossen ist, weshalb es auch nicht als String behandelt werden sollte.
Statt dessen sollten eben nur die einzelnen Elemente des Arrays angesprochen werden,
wie vokale[3] oder vokale[1]. Dagegen könnte das nachfolgende char-Array falsch sehr
wohl als String verwendet werden, da es explizit mit \0 abgeschlossen wird.
char falsch[]={'F','A','L','S','C','H','E','u','E','I','N','G','A','B','E','\0' };
Diese Angabe entspricht vollständig den beiden folgenden Angaben:
char *falsch = "Falsche Eingabe";
char falsch[] = "Falsche Eingabe";
Eine dieser beiden letzten Angaben ist der vorherigen Initialisierungsform vorzuziehen. Wird die Länge eines Arrays nicht angegeben, berechnet der Compiler diese
Länge, indem er die Initialisierungen zählt. In den obigen Angaben ist die Arraylänge
also 16 (15 relevante Zeichen und ein abschließendes \0).
kap01.fm Seite 59 Mittwoch, 5. Dezember 2001 10:40 10
2.34
Arrays und Zeiger
59
Beispiel
Die folgende Angabe:
int
x[] = { 1, 5, 7 };
definiert und initialisiert x als ein eindimensionales Array mit drei Elementen.
Bei der folgenden Angabe:
float y[4][3] = { { 1, 3, 5 },
{ 2, 4, 6 },
{ 3, 5, 7 } };
liegt eine vollständig geklammerte Initialisierung vor: 1, 3, 5 inititialisiert die erste
Zeile von y (Element y[0]; somit werden y[0][0], y[0][1] und y[0][2] initialisiert). Die
nächsten zwei Zeilen initialisieren dann entsprechend y[1] und y[2]. Da die Initialisierungsliste nicht vollständig ist, wird y[3] (also y[3][0], y[3][1], y[3][2]) mit Nullen initialisiert. Den gleichen Effekt hätte man mit
float y[4][3] = { 1, 3, 5, 2, 4, 6, 3, 5, 7 };
erreicht. Hier werden die ersten drei Elemente aus der Initialisierungsliste in y[0], die
nächsten drei in y[1] und die letzten drei in y[2] abgelegt.
Die folgende Angabe:
float z[4][3] = {
{ 1 }, { 2 }, { 3 }, { 4 }
};
initialisiert die erste Spalte von z (z[0][0]=1, z[1][0]=2, z[2][0]=3, z[3][0]=4); die restlichen Elemente werden 0.
Die folgende Angabe:
short q[4][3][2] = { { 1 },
{ 2, 3 },
{ 4, 5, 6 }
};
definiert ein dreidimensionales Array, das wie folgt initialisiert wird:
q[0][0][0] = 1
q[1][0][0] = 2, q[1][0][1] = 3
q[2][0][0] = 4, q[2][0][1] = 5,
restliche Arrayelemente = 0
q[2][1][0] = 6
Die folgenden Angaben:
char
s[] = "abc",
t[3] = "abc";
definieren die char-Arrays s und t, deren Elemente mit Zeichenkonstanten initialisiert
werden. Diese Deklarationen sind identisch mit:
kap01.fm Seite 60 Mittwoch, 5. Dezember 2001 10:40 10
60
2
char
C-Theorie
s[] = { 'a', 'b', 'c', '\0' },
t[3] = { 'a', 'b', 'c' };
Der Inhalt dieser Arrays kann jederzeit verändert werden, was nicht für folgende
Deklaration gilt:
char *zeig = "abc";
Hier wird zeig als char-Zeiger definiert, der auf ein "Array von char" mit der Länge 4
zeigt. Wird zeig verwendet, um den Inhalt dieses Arrays zu verändern, liegt ein undefiniertes Verhalten vor. Dem Compiler ist es nämlich erlaubt, solche Zeichenketten im
Read-only-Speicher unterzubringen.
Implizite Initialisierung bei static-Variablen/Arrays
static-Objekte (Variablen oder Arrays) werden auch ohne explizite Angabe von Initialisierungswerten vom Compiler mit dem Wert 0 initialisiert, wie z.B.:
static int
x, *zeig;
entspricht der Angabe:
static int
x=0, *zeig=NULL;
Die folgende Angabe
static int
a[3][4];
entspricht der Angabe:
static int
a[3][4] = {
{ 0, 0, 0, 0 },
{ 0, 0, 0, 0 },
{ 0, 0, 0, 0 } };
Lokale (automatic) Objekte (Variablen oder Arrays) werden dagegen bei fehlender
Angabe von Initialisierungswerten nicht implizit vom Compiler mit dem Wert 0 initialisiert.
Initialisierung von lokalen Arrays in ANSI C erlaubt
Bei lokalen (automatic) Variablen muß der Initialwert keine Konstante sein, sondern er
kann auch durch einen beliebigen Ausdruck erst berechnet werden, da lokale Variablen beim Eintritt in die Funktion bzw. in den Block jedesmal neu initialisiert werden.
In ANSI C ist auch die Initialisierung lokaler Arrays erlaubt, was im ursprünglichen C
noch nicht möglich war.
Initialisierte Arrays mit const vor Überschreiben schützen
In der praktischen Softwareentwicklung kommt es häufiger vor, daß man in einem
Array Werte hinterlegt, die immer konstant bleiben und niemals verändert werden sol-
kap01.fm Seite 61 Mittwoch, 5. Dezember 2001 10:40 10
2.34
Arrays und Zeiger
61
len. Um solche Werte, die als Initialisierungsliste angegeben werden, vor einem versehentlichen Überschreiben zu schützen, muß das Schlüsselwort const benutzt werden.
const char
const long
meldung[] = "...Bitte neue Diskette einlegen......";
zehnpot[] = { 1, 10, 100, 1000, 10000, 100000, 1000000 };
Initialisierung von lokalen Arrays mit 0 oder NULL
Während nicht explizit initialisierte globale und static-Arrays immer automatisch mit
dem Wert 0 bzw. mit der Zeigerkonstante NULL initialisiert werden, gilt dies für lokale
Arrays nicht. Um lokale Arrays mit 0 bzw. NULL zu initialisieren, ist es nicht notwendig, explizit alle einzelnen Elemente mit 0 bzw. NULL zu initialisieren. In diesem Fall
reicht es aus, nur das erste Element mit 0 bzw. NULL zu initialisieren. Beispielsweise
könnte statt:
int
int
summe[10] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
wort[8]
= { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
auch nur
int
int
summe[10] = {0};
wort[8]
= {NULL};
angegeben werden, was den gleichen Effekt hätte, nämlich alle zehn Elemente von
summe auf 0 und alle acht Elemente von wort auf NULL setzen. Sind nämlich weniger
Initialisierer als Elemente angegeben, werden die restlichen Elemente wie bei globalen
und static-Arrays mit dem Wert 0 bzw. der Zeigerkonstante NULL initialisiert. Das
ganze funktioniert natürlich nur für 0 und NULL.
So entspricht z.B. die folgende Initialisierung:
int
mult[10] = {1};
nicht
int
mult[10] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
sondern der folgenden Initialisierung:
int
mult[10] = { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0};
Dimensionierungsangaben bei der Initialisierung
Bei der Initialisierung mehrdimensionaler Arrays kann auf die Angabe der Elementezahl der ersten Dimension verzichtet werden, auf die Elementezahl der übrigen
Dimensionen jedoch nicht, denn der Compiler benötigt diese, um die Speicherposition
eines Elements bei mehrfacher Indizierung zu ermitteln.
kap01.fm Seite 62 Mittwoch, 5. Dezember 2001 10:40 10
62
2
C-Theorie
Wie viele Arrayelemente im speziellen Fall angelegt werden, hängt von der Liste der
Initialisierungswerte ab. Gibt man für alle Arrayelemente Initialwerte an, kann man
auf die Angabe der ersten Dimension verzichten. So sind z.B. die beiden folgenden
Angaben identisch:
int a[3][4] = { 100, 200, 300, 400, 10, 20, 30, 40, 150, 300, 450, 600 };
int a[][4] = { 100, 200, 300, 400, 10, 20, 30, 40, 150, 300, 450, 600 };
Im zweiten Fall berechnet der Compiler aus der zweiten Dimension (4) und der Zahl
der Initialwerte (12) die erste Dimension (3). Gibt man z.B. nur 10 (statt 12) Initialisierungswerte an, legt der Compiler bei
int
a[3][4] = { 100, 200, 300, 400, 10, 20, 150, 300, 450, 600 };
und auch bei
int
a[][4]
= { 100, 200, 300, 400, 10, 20, 150, 300, 450, 600 };
ein zweidimensionales Array mit drei Zeilen und vier Spalten an, da zehn Werte mindestens drei Zeilen mit je vier Spalten benötigen. Die beiden letzten Arrayelemente
werden dabei mit dem Wert 0 initialisiert. Hat die Initialisierungsliste beispielsweise
aber nur sieben Werte, wie z.B.
int
a[3][4] = { 100, 200, 300, 400, 10, 20, 150 };
so entspricht diese Angabe der folgenden
int
a[3][4] = {
{ 100, 200, 300, 400 },
{ 10, 20, 150
} };
oder auch:
int
a[3][4] = {
{ 100, 200, 300, 400 },
{ 10, 20, 150,
0 },
{
0,
0,
0,
0 } };
Mit der Angabe
int
a[][4]
= { 100, 200, 300, 400, 10, 20, 150 };
wird dagegen ein Array mit zwei Zeilen und vier Spalten angelegt, da für sieben Werte
nicht mehr als zwei Zeilen benötigt werden. Die oben angegebene Definition ist somit
identisch zu
int
a[2][4]
= {
{ 100, 200, 300, 400 },
{ 10, 20, 150
} };
= {
{ 100, 200, 300, 400 },
{ 10, 20, 150, 0 } };
oder auch:
int
a[2][4]
kap01.fm Seite 63 Mittwoch, 5. Dezember 2001 10:40 10
2.34
Arrays und Zeiger
63
Läßt man die Elementezahl der ersten Dimension weg, werden immer nur so viele Elemente der ersten Dimension (Zeilen) generiert, wie für die Speicherung der angegebenen Initialisierungswerte notwendig ist. Durch zusätzliche Zeilenklammerung kann
man dies jedoch beeinflussen:
int
a[][4]
= {
{ 100, 200, 300, 400 },
{ 10, 20
},
{ 150
} };
Die Angabe oben erzeugt ein Array mit drei Zeilen und vier Spalten, was z.B. der folgenden Angabe entspricht:
int
a[3][4]
= {
{ 100, 200, 300, 400 },
{ 10, 20,
0,
0 },
{ 150,
0,
0,
0 } };
2.34.4 Arrays als Parameter von Funktionen
In C gibt es keine Möglichkeit, ein Array direkt an eine Funktion zu übergeben. Statt
dessen übergibt man immer nur die Anfangsadresse des Arrays.
Bei der Deklaration der formalen Parameter einer Funktion sind die beiden folgenden
Angaben völlig äquivalent:
int vekt[]
und
int *vekt
Welche Schreibweise man angibt, hängt im wesentlichen davon ab, wie man dann in
einer Funktion auf die Arrayelemente zugreift, um so zu dokumentieren, daß man dieses Argument
왘 als Array:
int vekt[] /* bei Zugriffen wie vekt[i] */
왘 oder als Zeiger:
int *vekt /* bei Zugriffen wie *vekt */
behandelt. Unabhängig von der Deklaration des Parameters können beide Zugriffsarten in einer Funktion verwendet werden, wenn dies sinnvoll erscheint. Es gibt auch die
Möglichkeit, nur einen Teil eines Arrays an eine Funktion zu übergeben. Dazu setzt
man z.B. beim Funktionsaufruf einen Zeiger auf den Anfang des zu übergebenden
Teilarrays. Mit der Definition
int feld[5];
und den Funktionsaufrufen
ausgabe(&feld[2]);
oder
kap01.fm Seite 64 Mittwoch, 5. Dezember 2001 10:40 10
64
2
C-Theorie
ausgabe(feld+2);
wird an die Funktion ausgabe als Startwert von feld die Adresse des dritten Elements
von feld übergeben.
Bei der Definition von ausgabe bestehen wieder zwei Möglichkeiten, die formalen Parameter zu deklarieren:
ausgabe(int vekt[])
oder
ausgabe(int *vekt)
Wird beim Aufruf einer solchen Funktion ein Arrayname als Argument übergeben,
wird bei der Ausführung der Funktion diese Adresse auf dem Stack hinterlegt. Man
arbeitet also dann in der Funktion mit einer Zeigervariablen, deren Inhalt auch verändert werden kann, was aber nicht zur Änderung der Anfangsadresse des Arrays führt.
Tritt ein mehrdimensionales Array als formaler Parameter einer Funktion auf, so kann
die erste Dimensionierung wie bei eindimensionalen Arrays offengelassen werden.
Beispiel
int vekt[][10];
char kreuz_wort[][45];
Die zweite und alle weiteren Dimensionen müssen jedoch angegeben werden. Statt der
beiden obigen Angaben könnte man auch folgende formalen Parameterdeklarationen
angeben:
int (*vekt)[10];
char (*kreuz_wort)[45];
Diese beiden Deklarationen, die äquivalent zu den obigen sind, verdeutlichen besser,
daß es sich bei vekt um einen Zeiger auf int-Arrays der Größe 10 und bei kreuzwort um
einen Zeiger auf char-Arrays der Größe 45 handelt. Mit kreuzwort++ wird also z.B.
um 45 Bytes weitergeschaltet. Es gibt also verschiedene Möglichkeiten, zweidimensionale Arrays als Parameter in Funktionen zu deklarieren
int matrix[10][10]
oder
int matrix[][10]
oder
int (*matrix)[10]
kap01.fm Seite 65 Mittwoch, 5. Dezember 2001 10:40 10
2.34
Arrays und Zeiger
65
da in allen diesen Fällen beim Aufruf der Funktion nur die Anfangsadresse des entsprechenden Arrays übergeben wird. Beispielsweise kann man eine Funktion
ausgabe_array zur Ausgabe einer Matrix auf drei verschiedene Arten definieren:
void
void
void
ausgabe_array(char *text, int array[2][3], int zeilnr)
ausgabe_array(char *text, int array[][3], int zeilnr)
ausgabe_array(char *text, int (*array)[3], int zeilnr)
Dagegen wäre in diesem Beispiel die folgende Definition nicht möglich gewesen:
void
ausgabe_array(char *text, int *array[3], int zeilnr)
Denn hier hätte man festgelegt, daß ein Array mit drei int-Zeigern übergeben wird
und nicht, wie gefordert, ein zweidimensionales int-Array, dessen erste Dimension die
Länge 3 hat.
call by value für Arrays (Zeiger) mit Schlüsselwort const
Da bei der Übergabe eines Arrays immer die Anfangsadresse (Zeiger) des Arrays übergeben wird, ist es nicht klar, ob die entsprechende Funktion den Inhalt des adressierten
Speicherbereichs verändert (call by reference) oder nur liest (call by value). Um dem Compiler mitzuteilen, daß der Speicherbereich, auf den eine übergebene Adresse zeigt, nur
gelesen und nicht verändert wird, kann man der entsprechenden Parameterdeklaration das Schlüsselwort const voranstellen. Wird dann in der Funktion versucht, in den
Speicherbereich zu schreiben, meldet der Compiler einen Fehler.
2.34.5 sizeof-Operator liefert die Größe eines Arrays
Mit dem sizeof-Operator läßt sich die Anzahl der Bytes ermitteln, die von einem Array
belegt werden. Dividiert man diese Größe durch die Anzahl der Bytes, die der Datentyp eines Elements belegt (wie z.B. sizeof(int) oder sizeof(double)), so erhält man die
Anzahl der Elemente im entsprechenden Array.
#include
int
double
char
int
{
<stdio.h>
kalender[12][31];
matrix[25][40];
adressen[200][100];
main(void)
printf("kalender:\n");
printf("
%d Elemente\n", sizeof(kalender)/sizeof(int));
printf("
%d Bytes\n", sizeof(kalender));
printf("
bei %d Bytes fuer int\n", sizeof(int));
printf("matrix:\n");
printf("
%d Elemente\n", sizeof(matrix)/sizeof(double));
printf("
%d Bytes\n", sizeof(matrix));
printf("
bei %d Bytes fuer double\n", sizeof(double));
kap01.fm Seite 66 Mittwoch, 5. Dezember 2001 10:40 10
66
2
C-Theorie
printf("adressen:\n");
printf("
%d Elemente\n", sizeof(adressen)/sizeof(char));
printf("
%d Bytes\n", sizeof(adressen));
printf("
bei %d Bytes fuer char\n", sizeof(char));
return(0);
}
kalender:
372 Elemente
744 Bytes
bei 2 Bytes fuer int
matrix:
1000 Elemente
8000 Bytes
bei 8 Bytes fuer double
adressen:
20000 Elemente
20000 Bytes
bei 1 Bytes fuer char
2.34.6 Gegenüberstellung von Arrays und Zeigern
Zugriff auf Arrayelemente ist auch über Zeiger möglich
Allgemein gilt:
array[i]=wert;
entspricht:
*(array+i)=wert;
Zeigt zeiger auf ein bestimmtes Element eines Arrays elem, dann zeigt zeiger+1 auf das
folgende Element und zeiger-1 auf das vorhergehende:
int elem[3], *zeiger;
.....
zeiger=&elem[1];
*(zeiger-1) = 10; /* entspricht: elem[0]=10; */
*zeiger
= 20; /* entspricht: elem[1]=20; */
*(zeiger+1) = 30; /* entspricht: elem[2]=30; */
*(zeiger-1) = *zeiger + *(zeiger+1); /* entspricht elem[0]= elem[1]+ elem[2]; */
Allgemein gilt also, daß zeiger+i auf das i-te Element nach zeiger und zeiger-i auf das i-te
Element vor zeiger zeigt. Wenn also zeiger auf vektor[0] zeigt, dann entspricht *(zeiger+1)
dem Element vektor[1] und *(zeiger+i) dem Element vektor[i]: zeiger+i ist also die
Adresse des Elements vektor[i] (&vektor[i]), und *(zeiger+i) ist das Arrayelement vektor[i] selbst.
Da auf Maschinenebene nur einzelne Bytes adressiert werden können, bedeutet dies
abhängig vom Datentyp des Zeigers eine Multiplikation von i mit der Anzahl der
kap01.fm Seite 67 Mittwoch, 5. Dezember 2001 10:40 10
2.34
Arrays und Zeiger
67
Bytes des entsprechenden Objekttyps, auf den der Zeiger zeigt. Die anschließende
Addition auf den Wert von Zeiger ermittelt dann das entsprechende Element. Wenn
z.B. die folgende Arraydefinition vorliegt
double
messwerte[10];
und für double 8 Bytes verwendet werden, dann bedeutet die folgende Angabe
messwerte[7]
/* entspricht:
*(messwerte+7)
*/
daß auf die Adresse zugegriffen wird, die 7*8 Bytes von der Anfangsadresse messwerte
entfernt liegt.
Den Unterschied zwischen der C-Ebene und der Maschinenebene bei der Adressierung soll folgendes Beispiel aufzeigen:
int
*zeiger, feld[4];
Hierbei wird angenommen, daß der Datentyp int zwei Bytes belegt.
zeiger = feld;
zeiger = zeiger+3;
zeiger+3 auf C-Ebene entspricht auf Maschinenebene zeiger+3*2, wenn int-Variablen
zwei Bytes belegen. War z.B. zeiger zuvor auf die Adresse 100 gerichtet, zeigt er nach
der Zuweisung zeiger=zeiger+3 auf die Adresse 106, da auf Maschinenebene
100+2*3(=106) gerechnet wird.
Arrayname ist konstanter Zeiger auf erstes Element
Es gilt, daß der Name eines Arrays automatisch ein konstanter Zeiger auf das erste Element des Arrays ist.
int vektor[5];
definiert ein Array vektor mit den fünf Elementen vektor[0], vektor[1], ..., vektor[4]. vektor[i] liegt dann i Elemente vom Arrayanfang entfernt. Gibt man nur den Arraynamen
vektor allein (ohne Index) an, erhält man die Anfangsadresse von vektor. Will man also
einen Zeiger, der z.B. mit
int
*zeiger;
definiert ist, auf den Anfang des Arrays vektor positionieren, kann eine der beiden folgenden Anweisungen, die beide das gleiche leisten, angegeben werden:
zeiger = &vektor[0];
oder
zeiger = vektor;
kap01.fm Seite 68 Mittwoch, 5. Dezember 2001 10:40 10
68
2
C-Theorie
Soll nun das erste Element von vektor einer int-Variablen ganz zugewiesen werden,
kann man, da zeiger und vektor auf das erste Element zeigen, zwischen folgenden beiden Anweisungen wählen:
ganz = *zeiger;
oder
ganz = *vektor;
Beide entsprechen:
ganz = vektor[0];
Der Name eines Arrays ohne Angabe von Indizes entspricht immer einem Zeiger auf
den Anfang des Arrays; d.h. daß man über einen Zeiger verfügen kann, ohne diesen
dekalriert zu haben. Dies wiederum bedeutet, daß ein solcher implizit fest vorgegebener Zeiger nicht verändert werden kann und immer auf dieselbe Adresse (Anfangsadresse eines Arrays) zeigt.
Unterschied zwischen Arraynamen und echten Zeigern
Zwischen einem Arraynamen ohne Indexangabe und einem explizit definierten Zeiger
besteht ein wesentlicher Unterschied, den es zu beachten gilt:
int
*zeig, vektor[5];
Bei einem explizit definierten Zeiger (zeig) handelt es sich um eine Variable, deren Wert
verändert werden kann. D.h.: In solche Zeigervariablen kann ständig eine andere
Adresse geschrieben werden, wie z.B.
zeig=vektor; oder zeig=&vektor[0]; oder
oder zeig=&vektor[3];
zeig=zeig+3; oder zeig=vektor+3;
Bei einem implizit eingeführten Zeiger (wie hier vektor) handelt es sich dagegen um
eine Konstante, deren Wert niemals manipuliert werden kann. D.h., daß bei der Definition eines Arrays immer automatisch ein konstanter Zeiger (Arrayname ohne Indexangabe) angelegt wird, der immer auf den Arrayanfang zeigt. Ausdrücke wie
vektor=zeiger;
++vektor;
vektor=&vektor[3];
sind also nicht erlaubt.
C kennt eigentlich nur eindimensionale Arrays
C kennt nur eindimensionale Arrays, und die Größe eines Arrays muß als Konstante festgelegt sein. Jedoch kann ein Element eines Arrays ein Objekt eines beliebigen Typs
sein, einschließlich eines anderen Arrays. Dies macht es möglich, mehrdimensionale
Arrays zu simulieren.
kap01.fm Seite 69 Mittwoch, 5. Dezember 2001 10:40 10
2.34
Arrays und Zeiger
2.34.7 Erlaubte Operationen mit Zeigern
Wir nehmen für die unten genannten Beispiele die folgenden Definitionen an:
int
*zeig1, *zeig2, wert=3;
Es sind nun diese vier Operationen mit Zeigern erlaubt:
왘 Addition einer int-Konstante oder einer int-Variablen, wie z.B.:
/* Positioniere zeig1 um 4 Elemente weiter
*/
zeig1 += 4;
zeig1 += wert; /* Positioniere zeig1 um wert (3) Elemente weiter */
왘 Subtraktion einer int-Konstante oder einer int-Variablen, wie z.B.:
/* Positioniere zeig1 um 4 Elemente zurück
*/
zeig1 -= 4;
zeig1 -= wert; /* Positioniere zeig1 um wert (3) Elemente zurück */
왘 Subtraktion zweier Zeiger, wie z.B.:
zeig1 – zeig2
왘 Vergleiche zweier Zeiger, wie z.B.:
zeig1 > zeig2
zeig1 < zeig2
zeig1 >= zeig2
zeig1 <= zeig2
zeig1 == zeig2
zeig1 != zeig2
2.34.8 Unerlaubte Operationen mit Zeigern
Wir nehmen für die unten genannten Beispiele die folgenden Definitionen an:
int
*zeig1, *zeig2, wert=3;
Diese drei Operationen mit Zeigern sind nicht erlaubt:
왘 Addition, Division und Multiplikation von Zeigern, wie z.B.:
zeig1 = zeig2 + zeig1;
zeig2 = zeig1/zeig2;
zeig1 *= 4;
왘 Operationen mit float- oder double-Werten, wie z.B.:
zeig1 += 3.37;
zeig2 -= 4.5;
왘 Shift-, Bit- oder logische Operationen, wie z.B.:
zeig2 >>= 4;
zeig1 = !zeig2;
zeig2 = zeig1 & zeig2;
69
kap01.fm Seite 70 Mittwoch, 5. Dezember 2001 10:40 10
70
2
2.34.9
C-Theorie
Zeigerarrays
Zeigerarrays sind Arrays, deren Elemente Zeiger sind. Mit der folgenden Definition
int *zeigarray[6];
wird ein Zeigerarray mit sechs Elementen (sechs Zeigern) definiert, die alle auf eine
int-Variable zeigen können. Da eckige Klammern stärker als * binden, ist zeigarray
ein Array von Zeigern auf eine int-Variable. Jedes Element von zeigarray (zeigarray[0], zeigarray[1], ..) ist somit ein Zeiger auf einen int-Speicherplatz.
zeigarray selbst ist wie bei anderen Arrays auch ein impliziter Zeiger und zeigt auf
den Anfang des Arrays, also auf das erste Element des Arrays. zeigarray ist somit ein
Zeigerzeiger (Zeiger, der wieder auf einen Zeiger zeigt).
Hierbei ist zu beachten, daß die Elemente des Arrays (die Zeiger) noch nicht initialisiert sind, sondern irgendwohin im Speicherplatz zeigen. Die folgende Anweisung
wäre absolut falsch:
*zeigarray[3] = 7;
Diese Anweisung würde nämlich an die Adresse, die zufällig im Element zeigarray[3] steht, den Wert 7 schreiben, und somit fremden Speicherplatz überschreiben.
Unterschiede zwischen zweidimensionalen Arrays und Zeigerarrays
Die Verwendung zweidimensionaler Arrays besitzt manchmal Nachteile gegenüber eindimensionalen Zeigerarrays.
Zweidimensionales Array:
const char
engl_wort[14][9] = {
"zero", "one",
"two", "three", "four",
"five",
"six",
"seven", "eight", "nine", "ten",
"eleven", "twelve", "thirteen"
};
Hier beträgt der Speicherbedarf: 14*9 = 126 Bytes
Eindimensionales Zeigerarray:
const char
*engl_wort[] = {
"zero", "one",
"two", "three", "four",
"five",
"six",
"seven", "eight", "nine", "ten",
"eleven", "twelve", "thirteen"
};
Hier beträgt der Speicherbedarf: 28 + 63 + 14 = 105 Bytes (zwei Bytes für Zeiger angenommen).
Ein anderer Vorteil von eindimensionalen Zeigerarrays gegenüber der ersten Version
ist, daß man weitere Wörter an das Array engl_wort bei der Initialisierung anhängen
könnte (wie z.B. fourteen usw.), ohne daß dadurch weitere Änderungen im restlichen
Programm notwendig werden, vorausgesetzt, daß die entsprechende Dimension
immer automatisch berechnet wird mit:
kap01.fm Seite 71 Mittwoch, 5. Dezember 2001 10:40 10
2.34
Arrays und Zeiger
int
71
max = sizeof(engl_wort) / sizeof(engl_wort[0]) – 1;
In der ersten Version dagegen müßte die Arraygröße bei der Definition von engl_wort
angepaßt werden. Zudem wären Änderungen im restlichen Programm notwendig: 13
müßte überall durch die neue Größe ersetzt werden.
Um auf Zeichen in einzelnen Strings (z.B. h in eight) zuzugreifen, gibt es mehrere
Möglichkeiten:
engl_wort[8][3]
oder
*(engl_wort[8] + 3)
oder
*(*(engl_wort+8) + 3)
2.34.10 Zeiger auf Arrays
Durch Setzen von Klammern können die Vorrangsregeln von * und [] durchbrochen
werden.
Mit der folgenden Definition z.B.
int (*zeigarray)[6];
wird nicht wie vorher ein Zeigerarray mit sechs Elementen, sondern nur ein Zeiger
definiert, der auf ein Array mit sechs int-Variablen zeigt. Mit der Angabe von zeigarray[1] adressiert man also das nächste Array (sechs int-Variablen weiter). Mit zeigarray[1][2] würde man also auf das dritte Element des zweiten Arrays zugreifen. Ein
Anwendungsbeispiel ist z.B. das Vertauschen von zwei Arrays, indem man nicht die
beiden Arrays physikalisch komplett umkopiert, sondern eben nur ihre Anfangsadressen vertauscht, wie z.B.:
#include
int
int
<stdio.h>
array1[2][3] = { { 1, 2, 3}, { 4, 5, 6}};
array2[2][3] = { {11, 12, 13}, {14, 15, 16}};
/*-------------------------- Setzen von zwei Zeigern auf die Arrays ---------*/
int (*z1)[3] = array1;
int (*z2)[3] = array2;
void ausgabe_array(char *text, int array[][3], int zeilnr);
void swap_array(int (**a1)[3], int (**a2)[3]);
/*------------------------------------------------------------ main ---------*/
int main(void)
{
swap_array(&z1, &z2); /* Vertauschen der beiden Arrayzeiger */
kap01.fm Seite 72 Mittwoch, 5. Dezember 2001 10:40 10
72
2
C-Theorie
ausgabe_array("z1", z1, 2);
ausgabe_array("z2", z2, 2);
}
/*--------------------------------------------------- ausgabe_array ---------*/
void ausgabe_array(char *text, int array[][3], int zeilnr)
{
int i, j;
printf("%s.............\n", text);
for (i=0; i<zeilnr; i++) {
for (j=0; j<3; j++)
printf("%4d", array[i][j]);
printf("\n");
}
}
/*------------------------------------------------------ swap_array ---------*/
void swap_array(int (**a1)[3], int (**a2)[3])
{
int (*h)[3];
h = *a1;
*a1 = *a2;
*a2 = h;
}
2.34.11 Zeigerzeiger
Mit folgender Angabe wird ein Zeigerzeiger definiert:
int **zeig_zeig;
Ein Zeigerzeiger ist ein Zeiger, der wieder auf einen Zeiger zeigt. Ein Zeigerzeiger enthält also die Adresse eines Speicherplatzes, in dem wieder eine Adresse gespeichert ist.
Beim Zugriff über zeig_zeig gilt dann folgendes:
**zeig_zeig ist eine int-Variable,
*zeig_zeig ist ein Zeiger auf eine int-Variable und
zeig_zeig
ist ein Zeiger auf einen Zeiger, der auf eine int-Variable zeigt.
Natürlich sind auch Angaben wie z.B.
double ***zzz;
/* Zeiger-Zeiger-Zeiger */
int ****array[10]; /* Array,dessen Elemente Zeiger-Zeiger-Zeiger-Zeiger sind */
2.34.12 Zeigerarrays mit Funktionsadressen
Fortgeschrittene C-Programmierer verwenden in bestimmten Fällen Zeigerarrays,
deren Elemente Funktionsadressen sind. So lassen sich in bestimmten Anwendungsfällen endlose case-Marken in einer switch-Anweisung vermeiden.
kap01.fm Seite 73 Mittwoch, 5. Dezember 2001 10:40 10
2.34
Arrays und Zeiger
73
Das nachfolgende einfache Beispiel soll dies verdeutlichen: Es soll ein kleines Rechenprogramm erstellt werden, das zunächst zwei Zahlen einliest und dann den Benutzer
fragt, welche Rechenoperation mit diesen beiden Zahlen durchzuführen ist: Addition,
Subtraktion, Multiplikation, Division oder Potenz.
#include
#include
<stdio.h>
<math.h>
/*---------------------------------double add (double a, double b) {
double subtr(double a, double b) {
double mult (double a, double b) {
double divi (double a, double b) {
Funktionsdefinitionen ----------------*/
return(a+b); }
return(a-b); }
return(a*b); }
return(a/b); }
/*------------------------------------- Array von 5 Zeigern auf Funktionen */
double (*(rech_funkt[5]))(double a, double b) = { add, subtr, mult, divi, pow };
/*--------------------------------------------------- main ----------------*/
int main(void)
{
int
operator;
double a, b;
printf("Gib 2 Werte (durch Komma getrennt) ein : ");
scanf("%lf, %lf", &a, &b);
while (1) {
/*--- Endlosschleife, die durch BREAK verlassen wird */
printf("Gib Operator ein (+=0, -=1, *=2, /=3, ^=4): ");
scanf("%d", &operator);
if (operator>=0 && operator<=4)
break;
}
printf("Das Ergebnis ist %lf\n", (*(rech_funkt[operator]))(a,b));
return(0);
}
Hier wird zunächst ein Array rech_funkt der Größe 5 definiert. Die Elemente dieses
Arrays sind Zeiger auf Funktionen mit zwei double-Argumenten:
double (*(rech_funkt[5]))(double a, double b);
Um die Elemente dieses Arrays gleich zu initialisieren, werden die entsprechenden
Funktionsnamen bei der Definition im Initialisierungsteil angegeben:
double (*(rech_funkt[5]))(double a, double b) = {
add, subtr, mult, divi, pow
};
Dies bedeutet, daß z.B. rech_funkt[2] die Adresse der Funktion mult und
rech_funkt[4] die Adresse der Funktion pow enthält.