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.