C - Kurs Hadersbeck
Transcrição
C - Kurs Hadersbeck
C - Kurs Hadersbeck Literaturhinweise: Kernighan, Ritchie : The C Programming Language, Prentice Hall, 1988 Darnell, Margolis : Software Engineering in C, Springer Verlag, 1988. 1 1. Einleitung 1.1. Historische Bemerkungen Entwickelt von Kernighan, Ritchie (AT&T Bell Laboratoriers) Relativ geringer Sprachumfang : Kontrollstrukturen, wenig Variablentypen, Funktionen, Modulares Programmieren, Kein Input Output. Erste Version 1983 von C veröffentlicht in "The C Programming Language" (Kernighan, Ritchie) (1983) UNIX geschrieben in C. Neuere ANSI Version (1988) wird zum Standard (Besseres Funktionen Konzept, definierte Library), veröffentlicht in "The C Programming Language" , second Edition (Kernighan, Ritchie) (1988) 2. Grundlagen 2.1. Allgemeine Programmstruktur Ein C-Programm kann aus beliebigen Funktionen, die über verschiedene Module verteilt sind bestehen. Eine Funktion innerhalb des Programms entspricht der Hauptfunktion und heißt main(). Sie stellt den Programmeinsprungspunkt dar. Jedes C-Programmodul hat folgende Struktur : [ Praeprozessor Direktiven ] [ Typ - Deklarationen ] [ Definition und Deklaration von Variablen (globale Variablen) ] [ Deklaration von Funktionen ] Definition von Funktionen (nur eine Funktion wird main genannt ) Jede Funktion hat folgende Struktur : [ Funktionstyp ] Funktionsname ( [ Parameter 1 ,..., Parameter n ] ) Funktionsrumpf Ein Funktionsrumpf hat folgende Struktur : { [ Deklarationen von Variablen ] } Statements Beispiel : Funktion mit Hauptprogramm 2 Praeprozessor - Direktiven funktion { /* Anweisungen der Funktion_1 */ } main () { /* Hauptprogramm */ } dazu Programmbeispiele : 1. Programm : #include <stdio.h> hallo () { puts("Hallo"); } main () { hallo(); } 2. Programm : #include <stdio.h> #define readint(Z) scanf("%d",&Z) /* Minimum, Produkt berechnet die kleinere und das Produkt zweier Zahlen, die der Benutzer eingegeben hat. */ int mult (int a, int b) { return (a*b); } main() { int kleiner,prod; int zahl1, zahl2; /* das Ergebnis */ /* Eingabe Werte */ /* prompt fuer die Benutzereingabe */ puts(" Bitte geben Sie die erste Zahl ein "); readint(zahl1); puts(" Bitte geben Sie die zweite Zahl ein "); readint(zahl2); /* suche die kleinere Zahl */ if (zahl1 < zahl2) kleiner = zahl1; else kleiner = zahl2; printf("Die kleinere der Zahlen %d und %d ist :%d\n",zahl1,zahl2,kleiner); /* berechne das Produkt */ prod = mult(zahl1,zahl2); printf("Das Produkt der Zahlen %d und %d ist : %d \n",zahl1,zahl2,prod); 3 } 2.1.1. Aufgaben 1. Kreieren Sie in Ihrem Homeverzeichnis einen Ordner Uebung1 2. Implementieren Sie in diesem Ordner das Programm 1 und Programm 2 aus diesem Kapitel. Der Source-Filename für Programm 1 ist : prog-1.c Der Source-Filename für Programm 2 ist : prog-2.c 2. Schreiben Sie ein Programm addiere, das drei ganze Zahlen einliest, die größte und die Summe der drei Zahlen ausgibt. Dazu verwenden Sie die Funktion add : int add(int a, int b, int c) { return (a+b+c) } 2.2. Kommentare in Programmen Kommentare können an beliebiger Programmstelle stehen und werden von den Zeichen /* und */ eingerahmt. Die Länge der Kommentare ist unbeschränkt, wobei die Kommentare jedoch nicht verschachtelt sein dürfen. 2.3. Konstanten 2.3.1. ganze Zahlen integer Konstanten : 1234 long integer : suffix l oder L 234L, oktal Zahl : '\013' oder prefix 0 (Hochkomma auf der Appletastatur : alt ´ (links von der DELETE Taste) ) hexadezimal Zahl : '\xhh' oder prefix 0x unsigned integer : postfix u oder U 2.3.2. floating Zahlen float Konstanten : 2.3, 1e-2 2.3.3. Zeichen und Zeichenketten string Konstanten : "abc" character Konstanten : 'c' 4 2.4. Variablen 2.4.1. Was sind Variablen Eine Variable besteht aus zwei Teilen : Einem Namen und einem Objekt. Jede Variable zeigt genau auf ein Objekt und kann zu einem Zeitpunkt nur einen Wert annehmen. x = Variablenname 10 Wert der Variable int x; Adresse Adresse Adresse Beispiel : Häuser werden mit Hausnummern verwaltet. Der Variablenname entspricht der Hausnummer. Jede Hausnummer zeigt auf ein Haus (Objekt) und kann zu einem Zeitpunkt nur einen Wert (Nummer) annehmen. Das Objekt steht an einer bestimmten Stelle (Speicheradresse), die sich nicht ändern kann. 5 2.4.2. Regeln für Variablen in C – – – - Jede Variable muß vor dem Gebrauch deklariert werden Variablennamen beginnen mit Buchstaben Groß/Kleinschreibung ist signifikant Namen sollten nicht mit Underscore beginnen (reserviert für Systemvariablen) 31 Buchstaben sind signifikant bei lokalen Variablen 2.4.3. Grundtypen von Variablen 2.4.3.1. Ganze Zahlen int ... Integer, implementationsabhängig 16, 32 oder 64 Bit short ... Integer, 16 Bit long ... Integer, 32 oder 64 Bit unsigned int, unsigned short, unsigned long ... kein Vorzeichen 2.4.3.2. Zeichen char unsigned char 2.4.3.3. ... Character 7 Bits ... Character 8 Bits enum enum ... Aufzählung einer Menge durch ihre Symbole enum boolean { YES, NO}; 2.4.3.4. float 2.4.3.5. double floating ... Reelle Zahl, 32 Bit double ... Reelle Zahl, 64 Bit 6 2.4.4. Deklaration von Variablen Jede Variable muß mit ihrem Namen und ihrem Typ vor dem Gebrauch definiert werden: Daraufhin reserviert der Kompiler Speicherplatz für sie. Die Definition einer Variablen nennt man Deklaration. Bei einer Deklaration wird der Typ und die mit Komma getrennten Namen der Variablen angegeben. Variablen können außerhalb oder innerhalb von Funktionen, oder am Anfang eines Blocks deklariert werden. Beispiel : int lower; float x,y; char esc; 2.4.5. Lebensdauer von Variablen Die Lebensdauer einer Variable ist abhängig von der Deklarationsstelle. Liegt die Deklarationsstelle : 1. Fall : außerhalb einer Funktion (globale Variable) Lebensdauer innerhalb aller Funktionen des gesamten Programms. 2. Fall : innerhalb einer Funktion (lokale Variable) Lebensdauer innerhalb der Funktion 3. Fall : innerhalb eines Blocks Lebensdauer innerhalb des Blocks 2.4.6. Gültigkeit von Variablen Die Gültigkeit einer Variable entspricht solange der Lebensdauer der Variablen, bis nicht innerhalb einer Funktion oder eines Blocks eine Variable mit dem gleichen Namen definiert wird. Beispiel : #include <stdio.h> #define readint(Z) scanf("%d",&Z) int a,b,c,d; int add(int a,b) { int c; c = a + b; return c; } main() { int c; puts( " Bitte geben Sie zwei Zahlen ein"); readint(a); readint(b); c = add(a,b); printf(" Die Addition lautet = %d\n",c); } 7 2.4.7. Schlüsselwörter Folgende Schlüsselwörter sind reserviert und dürfen nicht als Variablennamen verwendet werden : auto break else case char const float continue default do double long enum extern short for goto if int switch register return unsigned signed sizeof static struct typedef union void volatile while 2.4.8. Initialisierung von Variablen Variablen können bei der Deklaration auch Initialisierungswerte zugewiesen werden. Beispiel : char esc = '\\'; int grenze = MAXLINE + 1; int upper=100, abc[100]; float x,y,z=1.0e-5; 2.4.9. Zuweisen von Werten an eine Variable lvalue lvalue lvalue = rvalue oder <op> = rvalue ist äquivalent zu = value(lvalue) <op> rvalue Achtung : lvalue hat : Typ, Adressenklasse, Namen, Adresse rvalue hat : Typ, Namen, Wert Beispiel : x = 2 + 3; x += 1; entspricht x = x + 1; 8 2.4.10. Konditionale Zuweisung expr1 ? expr2 : expr3 entspricht : if expr1 then expr2 else expr3; Beispiel : z = (a>b) ? a : b entspricht : z = max(a,b) oder if (a>b) z=a; else z=b; 2.4.11. Funktionen zum Einlesen und Ausgeben von Variablenwerten Zum Einlesen von Variablenwerten gibt es die Funktion scanf(" Kontrollstring ",Adresse_Variable1,Adresse_Variable2 .... ) Beim Einlesen muß die Adresse der Variable in scanf eingetragen werden (& Operator vor dem Variablennamen). Zum Ausgeben der Werte von Variablen gibt es die Funktion printf(" Kontrollstring ",Variable1,Variable2 .... ) Im Kontrollstring wird der Text der Zeile und die Formatierungsanweisung (% Operator für Formatierungsbuchstaben) für jede Variable angegeben. Soll am Ende der Zeile ein Zeilenvorschub eingefügt werden, muß dafür ein explizites Zeilenvorschub Zeichen eingefügt werden. Die wichtigsten Formatierungsbuchstaben sind : d für dezimal Zahlen f für reelle Zahlen c für Buchstaben 9 Beispiel : int m; float x; char ant; printf("Bitte geben Sie eine ganze Zahl ein >>>"); scanf("%d",&m); printf("Die Eingabe war %d\n",m); printf("Bitte geben Sie eine reelle Zahl ein >>>"); scanf("%f",&x); printf("Es wurde die ganze Zahl %d und die reelle Zahl %f eingegeben\n",m,x); printf("Es wurde die ganze Zahl >>>%d<<< und die reelle Zahl >>>%f<<< eingegeben\n",m,x); printf("Weitermachen y/n >>>"); fflush(stdin); scanf("%c",&ant); 2.4.12. Aufgaben 1. Schreiben Sie ein Programm das eine ganze und eine reelle Zahl einliest. Geben Sie das Quadrat und die Summe der Zahlen aus. 2. Schreiben Sie ein Programm, das einen Buchstaben einliest und den Buchstaben mit seinem ASCII -Wert ausgibt. Ändern Sie den Buchstaben jenachdem in seinen äquivalenten Groß/Kleinbuchstaben. 10 2.5. Operatoren 2.5.1. Arithmetische Operatoren Binäre arithmetische Operatoren sind : + * / / % plus minus Multiplikation Division, reellwertig Division, ganzzahlig Modulo Operator 2.5.2. Typenkonvertierung bei arithmetischen Ausdrücken In einem arithmetischen Ausdruck mit Variablen verschiedener Datentypen wird eine Variable mit schwächerem Typ automatisch in einen stärkeren konvertiert : schwach char -> int -> -> stark float -> double. Explizite Konvertierung muß mit dem cast Operator erzwungen werden. Der cast Operator besteht aus den Zeichen ( und ), den gewünschten Type eingeschlossen. Beispiel : x = (int) y; cast operator mit dem Typen int int a,b; (a/b)*b + (a%b) == a; float kreisfläche(float radius) { float pi = 3.14; return 2 * radius * radius *pi; } 11 2.5.3. Aufgabe Welches Ergebnis und welcher Typ wird erzwungen: short a = 10; long b = 1L; float x = 9.0; double y = 2.3d-2; a b b x x y = = = = = = (short) b; ______ a; ______ a + x; ______ sqrt ((double) x); ______ y + x; ______ a + b + x + y ______ __________ __________ __________ __________ __________ __________ 2.5.4. Relationale und Logische Operatoren Es gibt die logischen Werte : wahr (jede Zahl ungleich Null) und falsch (nur die ganze Zahl Null). logische Operatoren sind : < <= > >= == != && || ! kleiner kleiner gleich größer größer gleich gleich ungleich und oder nicht Achtung : Die Priorität von && ist größer als die von ||, sowohl && als auch || haben aber eine niedrige Priorität als > <, >= und <= (z.B. x < y && y < x ) Die Negation ( ! ) ist einstellig und konvertiert TRUE (nicht Null) in FALSE (Null) und FALSE (Null) in TRUE (nicht Null). Komplexe logische Ausdrücke werden von links nach rechts abgearbeitet, die Abarbeitung ist beendet, wenn das Ergebnis bestimmt ist. Beispiel : if (j < MAX && ((c=getchar()) != '\n')) if ((1<lim-1) || (!valid) && (x!=4)) x=0 12 2.5.5. Die Wahrheitstabelle bei logischen Ausdrücken int p,q; p q p && q p || q ! p ——————————————————————————————————————— 0 0 0 0 1 0 1 0 1 1 1 0 0 1 0 1 1 1 1 0 2.5.6. Aufgabe Was ist der Wert : 2 == 7 _____ 1 >= 0 4 > 4 _____ _____ 1 = 2 _____ Beispiel : float reziprokwert(float x) { if (x != 0) return 1/x; else puts(" Achtung Eingabewert ist gleich Null") } 13 2.5.7. Increment und Decrement Operatoren ++ -- addiert eins auf eine Variable, subtrahiert eins von einer Variable. ++ und -- können bei Variablen als postfix und als prefix verwendet werden, haben aber dann bei der Auswertung des Ausdrucks unterschiedliche Bedeutungen. als prefix : verändere den Wert der Variable und verwende den neuen Wert im Ausdruck als postfix: verwende zuerst den Wert der Variable innerhalb des Ausdrucks und verändere ihn dann. Beispiel : int a = 1; b = ++a; Der Wert von a ist 2 Der Wert von b ist 2 int a = 1; b = a++; Der Wert von a ist 2 Der Wert von b ist 1 2.5.8. Aufgabe Was ist der Wert des Ausdrucks : int j = 0,m=1,n=1; x = m++ - --j; ___________ m += ++j +2; ___________ j = m++ * m++; ___________ 14 2.5.9. Arithmetische Zuweisungsoperatoren Operator Zuweisung Additionszuweisung Subtrakt. Zuweisng. Multipliz. Zuweis. Divisions Zuweisung Modulo Zuweisung Symbol = += -= *= /= %= Form a=b a += b a -= b a *= b a /= b a %= b Operation Speichere den Wert von b in a Speichere den Wert von a+b in a Speichere den Wert von a-b in a Speichere den Wert von a*b in a Speichere den Wert von a/b in a Speichere den Wert von a%b in a Achtung bei Zuweisungen, wenn die Priorität von Operatoren ins Spiel kommt. Beispiel : a = a * 3 + 4; ist ungleich zu a *= 3 + 4; 2.5.10. Aufgaben Welche Werte werden bei folgenden Ausdrücken berechnet : int m = 3,n= 4; float x = 2.5,y=1.0; a) b) m +=n+x-y; m /= x+n+y; ____________ ____________ c) m %= y+m; ____________ d) x+= y -=m; ____________ 2.5.11. Bitweise Operatoren & (bitwise AND), | (bitwise inclusive OR), ^ (bitwise exclusive OR), << left shift, >> right shift, ~ (one komplement), z.B. : ausblenden von Bits : n = n & 0177; z.B. : setzen von Bits : n = n | 0101; z.B. : shift von Bits : 007 << 3 ist 070; 07 >> 1 ist 03 15 2.5.12. Prioritäten der Operatoren Innerhalb von Ausdrücken gelten folgende Prioritäten bei der Auswertung des Ausdrucks. Je höher der Wert, desto früher wird der Operator ausgewertet. Level Operatoren Assoziativität 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 () [] -> . ! ~ ++ -- + - * & (type) sizeof + * / % >> << < <= > >= == != & ^ | && || ?: = += -= *= /= %= &= ^= |= <<= , links nach rechts rechts nach links links nach rechts links nach rechts links nach rechts links nach rechts links nach rechts links nach rechts links nach rechts links nach rechts links nach rechts links nach rechts rechts nach links rechts nach links links nach rechts Beispiel : a == b && x || !y (a == b) && (x || !y) ( x=getchar() != EOF) ( (x=getchar()) != EOF) 16 >>= 2.5.13. Aufgaben 1. Welche Werte werden von folgenden Programmen ausgedruckt ? #include <stdio.h> #define readint(Z) scanf("%d",&Z) #define readfloat(Z) scanf("%f",&Z) int arith (void) /* 1. Programm : Arithmetik */ { int a,b,c,d; int erg; puts(" Geben Sie vier ganze Zahlen ein \a: "); readint(a); readint(b); readint(c); readint(d); erg = a + b; printf(" a + b = %d\n",erg); erg = a / b; printf(" a / b = %d\n",erg); erg = a % b; printf(" a mod b = %d\n",erg); erg = c++; printf("Nach erg = c++ ist der Wert von erg = %d, von c = %d \n",erg,c); erg = --a; printf("Nach erg = --a ist der Wert von erg = %d, von a = %d \n",erg,a); a += 2; printf(" Nach a += 2 ist der Wert von a = %d\n",a); } int logik (void) /* 2. Programm Logische Ausdruecke */ { int a,b,c,d; float x; puts(" Geben Sie vier ganze Zahlen ein : "); readint(a); readint(b); readint(c); readint(d); if ( b != 0 ) { x = a/b; printf(" Der Reziprokwert = %f\n",x); }; if ( a==b ) puts( " a ist gleich b "); else puts(" a ist ungleich b"); if ( a != b && ( a != c || a != d) ) printf( "jawohl \n"); if ( a != b + c ) printf(" a != b + c \n"); } main (void) { arith(); logik(); } 17 2. Schreiben Sie ein Programm, das eine ganze Zahl einliest und ausdruckt ob die Zahl gerade oder ungerade ist. 3. Schreiben Sie ein Programm das eine ganze Zahl einliest, die Quersumme berechnet und ausgibt. Das Programm soll auch die Oktal und Hexadezimaldarstellung der Zahl ausgeben Hinweis : (Formatierungsanweisung) 4. Schreiben Sie ein Wechselkursprogramm für Amerikanische Dollars Das Programm liest den Betrag bzw. Dollar Betrag ein und berechnet den Wert in der jeweils anderen Währung. 5. Schreiben Sie ein Programm das eine ganze und eine reelle Zahl einliest. Geben Sie das Quadrat und die Summe der Zahlen aus. 6. Schreiben Sie ein Programm, das einen Buchstaben einliest und den Buchstaben mit seinem ASCII -Wert ausgibt. Ändern Sie den Buchstaben je nach dem in seinen äquivalenten Groß/Kleinbuchstaben. 18 3. Strukturierung von Programmen 3.1. Kontrollstrukturen 3.1.1. Statements und Blöcke Jeder Ausdruck, abgeschlossen von einem Semikolon ist ein Statement. Somit ist das Semikolon kein Trennsymbol sondern das Abschlußzeichen eines Statements. Mehrere Statements können mit geschweiften Klammern zu einem Block zusammengefaßt werden. Der Block (=Block-statement) gilt nach außen wie ein Statement und wird nicht mit einem Semikolon abgeschlossen. Es gibt folgende Statements : expression-statement compound-statement selection-statement iteration-statement jump-statement labeled-statement 3.1.2. expression statement Dem expression statement entsprechen Wertzuweisungen, Wertvergleiche oder Funktionsaufrufe. Expressions werden sequentiell der Reihe nach ausgewertet und von einem Semikolon abgeschlossen. Die leere expression heißt Nullstatement. Beispiel : x = 1; i <= a; a = b + x; ; 3.1.3. compound statement Die geschweiften Klammern gruppieren statements zu einem compound statement (=Block). Innerhalb eines compound statements können Variablen deklariert werden. Die Lebensdauer der Variablen ist nur auf diesen Block geschränkt. Beispiel : { } int a; a=x; /* Hier kein Semikolon notwendig, da Block-statement */ oder : if (a==0) { b=1; c=1; d=1; printf(“%d”,i); oder : if (a==0) { int i; } i= a + 1; printf(“%d”,i); } 19 3.2. selection-statements 3.2.1. selection statment : if , if - else if (expression) statement if (expression) statement1 else statement2 Das if statement testet den numerischen Wert der Bedingung (expression). Falls der Wert ungleich Null ist (TRUE Fall), wird statement1 ausgeführt. Es wird nur ein einziges statement nach der Bedingung ausgeführt. Sollen mehrere statements zusammengefaßt werden, so müssen sie mit geschweiften Klammern geklammert werden. Bei geschachtelten if .. else gehört das else immer zum näheren if. Achtung : expression muß immer in runden Klammern eingschlossen werden. Beispiel : if (i==2) else oder puts("i ist gleich zwei"); puts(" i ist ungleich zwei") ; if ((i==2) || (j!=0)) { puts(" Der Fall ist eingetreten"); i = 3; } else puts(" Ja du Glückspilz "); Achtung, folgendes ist zulässig : Ausdruck if (i=2) if (++i) if (i=j<n) if (i) if (2) Bedeutung Zuweisung an i, dann Test : immer true Erhöhen des Wertes von i, dann Test Zuerst errechnen und zuweisen des Wertes an i, dann test ob i TRUE (!= 0) oder FALSE (=0) Test ob i TRUE (!= 0) oder FALSE (=0) immer TRUE (!= 0) 20 3.2.2. selection-statement : switch switch (expression) { case constant-expr: statements1 case constant-expr: statements2 default : statementsn } Jeder Label hat eine oder mehrere ganzzahlige Konstanten oder Konstanten Ausdrücke. Entsprechend dem Wert von expression werden sequentiell alle case constant-expr. ausgewertet und verglichen, ob sie mit dem Wert übereinstimmen. Bei Übereinstimmung werden die nachfolgenden statements ausgeführt und beim nächsten case Statement weiter getestet. Soll die Abarbeitung der sequentiellen Tests nach der Ausführung eines statements abgebrochen werden, so muß dieses statement mit break abgeschlossen werden. Die Abarbeitung wird dann hinter dem gesamten switch statement fortgesetzt. Wird keine Übereinstimmung zwischen dem Wert von expression und constant-expr. gefunden, so wird auf einen eventuell vorhandenen default Fall verzweigt. Beispiel : switch (c) { case ´0´ case ´2´ case ´2´ default } 3.3. : case ´1´ : : : case ´3´: : puts("0,1"); break; puts("2"); puts("2,3"); puts(" Nichts davon"); Iteration statements 3.3.1. iteration statement : while while (expression) statement Solange expression TRUE (ungleich Null) ist , wird statement ausgeführt. Es wird genau ein statement ausgeführt. Sollen mehrere statements ausgeführt werden, müssen sie zu einem compound-statement zusammengefaßt werden. Statement wird solange wiederholt, bis der Wert von expression FALSE (gleich Null) ist. 21 Beispiel : int while_1(void) { int num; puts("Gib den Countdown Wert ein >>>"); readint(num); /* Achtung bei Endlosschleifen */ while ( num > 0 ) { num --; printf(" Countdown laeuft >>%d<<\n",num); }; printf(" Start \n"); } Achtung vor Unendlichschleifen : i = 5; while (i) ; i --; while (i > 0) printf(" Countdownwert ist = %d\n",i); i--; 3.3.2. iteration statement : for for (expr1; expr2; expr3) statement expr1 : Wird vor dem Durchlauf der Schleife ausgeführt (Initialisierung) expr2 : Testbedingung der Scheife : Wiederholung falls expr2 ungleich Null expr3 : Wird nach einem Durchlauf von statement ausgeführt (Increment) entspricht : expr1; while (expr2) { statement; expr3; } Achtung : Im Gegensatz zu anderen Programmiersprachen ist die for Schleife bei C viel flexibler, da die Komponenten expr1, expr2, expr3 einer for loop beliebige Ausdrücke sein können, deren Werte auch innerhalb der Komponenten verändert werden können. 22 Beispiele : Es dürfen auch expr's der for Schleife fehlen : Die endlos Schleife : for (;;) ; Der Index einer for Schleife muß nicht unbedingt ein ganzzahliger Wert sein : int for_2(void) { char a; puts(" Die Buchstaben von a bis g lauten : "); for (a='a'; a < 'g'; a ++) printf(" %c ",a); printf("\n"); int for_3(void) { float x; puts(" Die Reihe von x - 1/x lautet : "); for (x=3; x > 0.00005; x = x - 1/x) printf(" %6.3f\n ",x); printf("\n"); } for (i=0; i<n; i++) { printf(" Der Schleifendurchlauf Nummer %d\n",i); m= n+i; } komplexe Ausdrücke in expr1, expr2 und expr3, als Rumpf der for Schleife wird die leere Ausweisung verwendet. Sämtliche Berechnungen werden innerhalb der for Schleife ausgeführt. Dies ist jedoch ein sehr komplizierter Programmstil und eigentlich abzulehnen. for (i=0,j=1,x=10; i=j*2*x; i = i*j,x--) ; 3.3.3. Aufgabe : Welche Werte werden bei folgendem Programmausschnitt ausgedruckt : int i,j; int n,m; n=m=4; for (i=0;j<n;i++) for (j=0;j<m;j++) printf("Der Matrixindex lautet : i = %d 23 j = %d \n",i,j); 3.3.4. iteration statement : do - while do statement while (expression); Das statement wird ausgeführt und die expression ausgewertet. Falls die expression TRUE ist (ungleich Null) wird das statement erneut ausgeführt. Wird expression gleich Null (FALSE) terminiert die Schleife und es wird hinter das Programm wird hinter dem do statement fortgesetzt. 3.4. jump statements 3.4.1. jump statement : break Das break statement stellt ein vorzeitiges exit aus einem for, while, do oder switch statement dar. Das break statement veranlaßt, die innerste Schleife sofort zu verlassen. Das continue statement beendet eine Iteration innerhalb einer Schleife und springt direkt zum nächsten Schleifendurchlauf. 3.4.2. jump statement : goto, label label : und goto label; Es wird empfohlen, auf die Programmierung mit Lables zu verzichten (Spagetticode). Es gibt genügend Kontrollanweisungen um Sprünge innerhalb des Programms zu vermeiden. 3.5. Die Vor- und Nachteile von for, do und while Die for Loop wird dann verwendet : - wenn der Schleifenindex im Vordergrund steht. (z.B. Durchlaufen von Arrays). - Bei einfachen Interationsschritten - Falls die Iteration und das Abbruchkriterium eng zusammenhängen Die while ( ) Loop wird verwendet : - wenn das Abbruchkriterium im Vordergrund steht - es soll zuerst getestet werden, bevor die Statements im while Block ausgeführt werden, da sonst Fehler auftreten könnten (z.B. Arithmetischer Ausdruck um Division durch Null zu vermeiden) Die do () Loop wird verwendet : - wenn die Statements im do Block im Vordergrund stehen. - Es sollen zuerst die Statements ausgeführt werden und dann die Bedingung getestet werden. (z.B : Einlesen von Abbruchkriterien) 24 Beispiel : #include <stdio.h> main() /* Demonstration of blocks and statements */ { char in,buffer[10]; int len,i=0; while ( ( (buffer[i++]=getchar() ) != '\n') && (i < 10)) ; len=i; for (i=0;i<len;i++) { char c; switch (c = buffer[i]) { case '0':case '1':case '2':case '3':case '4':case '5' : case '6':case '7': puts(" Die Zahl ist kleiner 8"); break; case '8':case '9': puts(" Die Zahl ist größer 8"); break; default : if (( c >= 'A' && c <= 'Z') || ( c >= 'a' && c <= 'z')) puts(" Wir haben einen Buchstaben"); else puts(" Wir haben ein Sonderzeichen"); } } } 3.5.1. Aufgaben 1. Schreiben Sie ein Programm, das 20 Buchstaben ein liest und zählt wieviele Groß bzw. Kleinbuchstaben und Umlaute darunter waren. 2. Schreiben Sie ein Programm, das bis zu 5 mal eine nur im Programm bekannte Geheimzahl abfagt. Bei jedem Durchlauf wird dem Benutzer ein neuer Tip gegeben, der einen neuen Hinweis zu dieser Geheimzahl gibt. Wird die Zahl gefunden, dann werden die Anzahl der Versuche ausgegeben. 3. Schreiben Sie ein Programm, das eine Zahl einliest und in Abhängigkeit der Zahl einen beliebigen Spruch auf das Terminal ausgibt. Nur bei einer bestimmten Zahl soll das Program terminieren. Verwenden Sie zum beenden des Programms dazu die Lösung aus Aufgabe 2 25 4. Zusammengesetzte Datentypen 4.1. Arrays Um Elemente gleichen Typs indiziert ansprechen zu können gibt es in C Arrays. Arrays werden mit ihrem Typ, dem Variablennamen und der Dimension deklariert. Beispiel : int zahlen[10]; float x[5],y[1000]; Mit der Deklaration eines Arrays wird Speicherplatz für die festgelegten Elemente zur Verfügung gestellt. Die einzelnen Elemente werden über Indizies angesprochen : erstes Element : zweites Element : usw. Index 0 1 ...... n Index 0 : z.B.: x[0] Index 1 : z.B.: x[1] Wert Wert des ersten Elements Wert des zweiten Elements ...... Wert des n+1 Elements Achtung : Das erste Element hat den Index 0 nicht den Index 1. Beispiel : int i; for (i=0;i<5;i++) x[i] = 0.0; Mit der Deklaration eines Arrays können die einzelnen Elemente auch initialisiert werden. Dabei muß entweder die Dimension groß genug sein um die einzelnen Elemente abspeichern zu können, oder die Dimension wird bei der Deklaration weggelassen. Im zweiten Fall errechnet der C Compiler die Dimension des Arrays selbst: Beispiel : int z[5] = {1,2,3,4,5}; oder : int z[] = {1,2,3,4,5}; (Errechnete Dimension [5]) Dies bedeutet: z[0]=1,z[1]=2,z[2]=3,z[3]=4, z[4]=5 Wird die Dimension größer gewählt als die Anzahl der abzuspeichernden Elemente, werden globale und statische Arrays automatisch mit Nullen aufgefüllt. Lokale Arrays werden nicht mit Nullen aufgefüllt, der Inhalt der nicht initialisierten Elemente ist zufällig (Achtung!). Werden Arrays als Funktionsargumente übergeben, können sie in der Argumentliste mit oder ohne Dimension aufgeführt werden. Beispiel : 26 int sort(int zahlen[]) oder int sort(int zahlen[10]) Innerhalb der Funktion wird die Dimension weder zur Kompilationszeit, noch zur Laufzeit überprüft, bzw. überwacht. Der Programmierer muß auf den richtigen Zugriff selbst achten. 4.2. Strings In C gibt es keinen Standardtyp string. Strings können in C in einem Array von Buchstaben gespeichert werden. Beispiel : char str[10]; definiert Array von Buchstaben der Länge 10 mit dem Namen str. Werden in einem solchen Array Buchstaben abgespeichert und als letzter Buchstabe eine Null gespeichert, dann spricht man von einem String. Einem String in C entspricht also in C eine mit Null terminierte Zeichenkette. Beispiel : Als Characterarry : char str[5]; str[0] = 'a'; str[1] = 'b'; str[2] = 'c'; str[3] = 'd'; a b c d Als String : char str[5]; str[0] = 'a'; str[1] = 'b'; str[2] = 'c'; str[3] = 'd'; str[4] = 0; /* oder '\0' */ a b c d \0 Die Programmiersprache C erwartet, daß Strings mit dem Eintrag Null als Terminierungszeichen abgeschlossen werden. Die Initialisierung eines Strings zieht diese Tatsache in Betracht und stellt für das Terminierungszeichen einen zusätzlichen Speicherplatz zur Verfügung. Das Array von Buchstaben kann aber auch ohne Dimension deklariert werden. Beispiel : richtig : char str[6] = "Hallo" oder : 27 char str[] = "Hallo" dies entspricht folgendem Eintrag im Speicher : h a l l o \0 Der Kompiler laesst folgendes zu, meldet aber keinen Fehler : char str[5] = "Hallo" Die Tatsache, daß die Zahl Null bei strings immer als Terminierungszeichen verwendet wird, und der Zahl Null der logische Ausdruck FALSE entspricht, wird in C-Programmen ständig ausgenutzt : Beispiel : /* kopieren des Inhalts von Strings */ char a[5] = "abcd",b[5]; int i; /* 1. Lösung ohne Ausnutzung des Terminierungszeichens : */ for (i=0;i<5;i++) b[i] = a[i]; /* 2. Lösung unter Ausnutzung des Terminierungszeichens: */ for (i=0;a[i];i++) b[i] = a[i]; b[i]=0; /* 3. Lösung unter Ausnutzung des Terminierungszeichens: */ i=0; while (a[i] ) { b[i] = a[i]; i++; }; b[i]=0; Beispiel : /* die Länge eines Strings : */ int length(char str[]) { int erg; erg = 0; while (str[erg]) erg++; return erg; } 28 4.2.1. Aufgabe Ist die Initialisierung zulässig und falls ja, was steht im Speicher : char s1[6] = "Hallo"; char s2[5] = "Hallo"; char s3[5] = "H"; char s4[] = "Hallo"; int a1[5] = {1,2,3,4,5,6}; int a2[] = {1,2,3,4,0}; int a3[5] = {1,2}; /* lokale Variable */ int a3[5] = {1,2}; /* globale Variable */ 29 4.3. Character Eingabe und Ausgabe Funktionen Bei der Ein/Ausgabe von Buchstaben dient das Terminal sowohl als Eingabemedium (stdin) und als Ausgabemedium (stdout) int getchar(void); getchar liest einen Buchstaben vom Terminal z.B. buchstabe = getchar(); char *gets(char *s); gets liest die nächste Zeile von stdin in den Vektor s und ersetzt dabei den abschließenden Zeilentrenner durch '\0'. Die Funktion liefert s oder NULL bei Dateiende oder bei einem Fehler. int putchar(int c); putchar schreibt einen Buchstaben auf das Terminal int puts(char *s); puts schreibt die Zeichenkette s und einen Zeilentrenner in stdout. Die Funktion liefert EOF, wenn ein Fehler passiert, andernfalls einen nicht-negativen Wert. int ungetc(int c, FILE *stream); ungetc stellt c (umgewandelt in unsigned char) in stream zurück, von wo das Zeichen beim nächsten Lesevorgang wieder geholt wird. Man kann sich nur darauf verlassen, daß pro Datenstrom ein Zeichen zurückgestellt werden kann. EOF darf nicht zurüchgestellt werden. ungetc liefert das zurückgestellte Zeichen oder EOF bei einem Fehler. 4.4. Aufgaben 1. Lese von der Tastatur 10 verschiedene Buchstaben ein. Lehne dabei doppelt eingegebene Buchstaben mit dem Hinweis "Buchstabe doppelt" ab. Gib alle 10 Buchstaben auf dem Bildschirm aus. 2. Schreiben Sie ein Programm, mit folgenden Funktionen : - berechnet die Länge eines Strings - kopiert einen String in einen anderen - liest vom Terminal Sätze ein und bestimmt an Hand der Satzzeichen (. ! und ? ) die Anzahl der eingelesenen Sätze. - liest einen String ein und sucht das Vorkommen eines Buchstaben, der über das Terminal eingegeben wurde - vergleicht ob zwei strings identisch sind - liest einen String ein und bestimmt ob er ein palindrom ist. - die Reihenfolge der Buchstaben eines strings umdreht. 3. Schreiben Sie ein Programm das zwei strings einliest und feststellt, ob der zweite String im ersten enthalten ist. 30 5. Funktionen Eine Funktion besteht aus einem Funktionskopf und einem Funktionsrumpf. Im Funktionskopf stehen der Funktionstyp, Funktionsname und falls gewünscht die Funktionsargumente. 5.1. Funktionstyp Eine Funktion kann falls gewünscht ein Ergebnis zurückgeben, wobei der Typ des Funktionsergebnisses dann den Typ der Funktion definiert. Funktionen die kein Ergebnis zurückgeben sind vom Typ void. Beispiel : int test(), char test(), long test(), float test(), struct test(), enum test(), pointer test(), void test() Das Ergebnis einer Funktion wird mit dem return - Statement zurückgegeben. Der zurückgegebene Wert wird entsprechend dem Funktionstyp konvertiert. Beispiel : return; return a; return (a*b) /* Kein Ergebnis */ /* Übergabe des Werts einer Variable */ /* Übergabe des Werts eines Expressions */ int pow(int x) { return (x*x) } 5.2. Funktionsname Bei Namen von Funktionen sind die gleichen Regeln gültig wie bei Namen von Variablen. 5.3. Funktionsargumente Die Funktionsargumente stehen eingeschlossen in runden Klammern hinter dem Funktionsnamen. Nach dem ANSI Standard wird jedes Argument mit seinem Typ und dem Argumentnamen aufgeführt. Der Nicht - ANSI Standard führt nur die Argumentnamen auf, der Typ mit den Argumentnamen wird in den Folgezeilen aufgeführt. Eine Funktion kann auch ohne Argumente definiert werden. Beispiel : ANSI : äquivalent dazu ist : Nicht ANSI : test (int a, char y, char x) test (a,y,x) int a;char y;char x; 31 Beispiel : Funktionen ohne Argument : ANSI : test (void) Nicht ANSI : test () Nach dem ANSI Standard sollte jede Funktion vor ihrem Aufruf mit ihrem Funktionskopf, abgeschlossen von einem Semikolon, deklariert werden. Die Deklaration nennt man Funktionsprototyp. Beispiel : char gross (char a); äquivalent dazu ist : char gross (char); Achtung: Der Unterschied zwischen Funktionsprototyp und Funktionsdefinition besteht darin, daß der Prototyp mit einem Semikolon abgeschlossen wird, also ein Statement ist. Die Funktionsdefinition wird nicht von einem Semikolon abgeschlossen, da hinter der Deklaration sofort der Funktionsrumpf beginnt oder beim Nicht-ANSI Standard die Argumenttypen aufgeführt werden. 5.4. Funktionsrumpf Der Funktionsrumpf ist in geschweiften Klammern eingeschlossen und entspricht somit einem compound statement. Am Anfang des Blocks stehen die Deklarationen lokaler Variablen. Die Argumente der Funktion stehen als lokale Variablen zu Verfügung. 5.5. Die Argumentübergabe : call-by-value <-> call-by-reference Bei C-Funktionen werden die Argumente immer mit ihrem Wert (=rvalue) übergeben (=call-by-value). Soll der Wert einer Variablen innerhalb der Funktion geändert werden und diese Änderung außerhalb der Funktion Bestand haben, muß der geänderte Wert der Variable auch in die Speicheradresse der Variable außerhalb der Funktion eingetragen werden. Dies kann nur erreicht werden, wenn als Funktionsargument die Adresse der Variable außerhalb der Funktion übergeben wird (=call-by-reference). Damit die Funktion weiß, daß es sich bei dem Argument um eine Speicheradresse handelt, muß im Funktionsheader dem Argumentnamen der * Operator vorgesetzt werden. Beispiel : int test (float *x, int *y) ; Bei jedem Zugriff auf diese Variable muß der Variable dieser * Operator ebenfalls vorgesetzt werden, sonst wird die Adresse der Variable als ganzzahliger Wert behandelt ! 32 Beispiele : int test (int *double, int *y) { *double = 2 * *y; } Hauptprogramm: main() { int a,b; a=10, b=20; quadrat(&a,&b); } int quadrat(int *x,*y) { *y = *x * *x; } Hauptprogramm: Variablen a b Adresse 1000 1002 Wert 10 20 Stack: Variable x y Adresse 6000 6002 Wert (int) (int) nun der Aufruf: quadrat(&a,&b); Stack: Variable x y Adresse 6000 6002 Wert 1000 1002 (Adresse) (Adresse) Verlassen von main und weiterarbeiten in der Funktion mit den Variablen unter den Adressen 1000 und 1002 nach dem Abarbeiten der Funktion quadrat im Hauptprogramm: Variable Adresse Wert a 1000 10 (int) b 1002 100 (int) 33 5.5.1. call-by-value Das Funktionsargument ist eine lokale Variable der Funktion. Sie wird auf dem Stack abgespeichert und bekommt als Wert den Übergabewert. Dem Argument kann innerhalb der Funktion kein Wert zugewiesen werden, da sie die Adresse der Variable mit der sie aufgerufen wird nicht kennt. Die Übergabeart call-by-value wird nur beim Herauslesen von Variablenwerten verwendet. Beispiel : call-by-value int schreiben (int a) { printf(" Der Wert lautet %d \n",a) } Aufruf : schreiben(x); 5.5.2. call-by-reference (= by name) Der Funktion wird als Argument die Adresse der Variable über den Stack übergeben. So kann die Funktion auf das im aufrufenden Programm gespeicherte Argument zugreifen. Innerhalb der Funktion lebt das Argument als Adresse. Zur Unterscheidung ob die Funktion mit der Adresse oder dem Wert der Adresse arbeitet muß innerhalb der Funktion der * Operator verwendet werden. Damit beim Aufruf der Funktion die Adresse der Variable übergeben wird und nicht der Wert, muß der & Operator vorgestellt werden. Die Übergabeart call-by-reference wird hauptsächlich beim Einlesen oder Verändern von Variablenwerten verwendet. Beispiel : call-by-name int rechnen (int *a,int *b) { *b = *a + 100; } Aufruf : rechnen(&x,&y); Vorsicht : Wird auf den * Operator verzichtet, so arbeitet man nicht mit der Variable als Verweis auf einen Speicherplatz, sondern mit einer Variablen, deren Wert der übergebenen Speicheradresse entspricht. Beispiel : Achtung Problem : int problem { a = 100; (int *a) /* setzt die Adresse in a auf 100 */ 34 5.6. Beispiele zum Aufruf von Funktionen 5.6.1. Aufruf call-by-name und call-by-reference #include <stdio.h> void header(void); void lesen (float *wert); void calc_diff(float wert,float *result); main () { float wert,result; int I,anz; header(); printf(" Bitte geben Sie die Anzahl der Durchläufe ein >>>"); scanf("%d",&anz); for (i=0;i<anz;i++) { lesen(&wert); calc_diff(wert,&result); printf(" resultat : %f \n",result); }; } void header(void) { puts(""); puts("****************************************"); puts("Hallo Hier ist das Programm call-by-name"); puts(" Warte was kommt"); puts("****************************************"); puts(""); } void lesen(float *wert) { puts("Bitte geben sie eine Zahl ein (float) "); scanf("%f",wert); printf(" Die Eingabe war %f\n",*wert); } void calc_diff(float wert,float *result) { float dividend; if (wert == 0) { puts(" Achtung Division durch Null ! Der Wert wird verändert !"); wert = 1.0; }; } printf(" Bitte geben Sie den Dividenden ein (float) >>>>"); scanf("%f",÷nd); *result = dividend / wert; 35 5.6.2. Vertauschen der Werte von zwei Variablen Da die Inhalte der zwei Variablen ausgetauscht werden sollen, ist call-by-reference notwendig : Beispiel : int swap(int *zahl1,int *zahl2) { int help; help = *zahl1; *zahl1 = *zahl2; *zahl2 = help; return; } Aufruf : swap(&x,&y); 5.6.3. Übergabe von Arrays an Funktionen Arrayelmente werden als Argument entweder mit einer festen Dimension oder einer offenen Dimension übergeben: Beispiel : oder int init_array(int numbers[],int len) int init_array(int numbers[20],int len) Bei der Übergabe eines Arrays an eine Funktion muß als Argument die Adresse des ersten Arrayelements übergeben werden. Defaultmäßig wird bei der Angabe eines Variablenarray als Funktionsargument die Adresse des ersten Elements übergeben. Beispiel : int zahlen[20]; init_array(zahlen,20); /* defaultmäßig die Adresse des ersten Elements */ init_array(zahlen[0],20); /* Falscher Aufruf !!! */ init_array(&zahlen[0],20); /* Äquivalent zum ersten Aufruf */ init_array(&zahlen[5],20-5); /* Initialisiert ab dem 6. Element */ Initialisierung aller Werte eines Arrays auf Null : int init_array(int numbers[],int length_of_numbers) { int i; for (i=0;i< length_of_numbers;i++) numbers[i] = 0; return; } Aufruf : init_array(x,10); oder init_array(&x[0],10); /* falls gilt : int x[10] */ 36 5.6.4. Austausch des Inhalts zweier Arrayelemente Es wird die Funktion swap verwendet. Damit die Werte vertauscht werden können muß die Funktion die Adressen kennen. Deshalb ist folgender Aufruf notwendig : (Austausch von x[0] und x[1]) swap(&x[0],&x[1]); 5.6.5. Funktionen als Argumente von Funktionen In C können Funktionen als Argumente übergeben werden. Als Argument wird ein Pointer auf eine Funktion definiert und beim Aufruf mit der jeweiligen Funktion versorgt. Beispiel : /* Funktionen als Argumente */ #include <stdio.h> int int int int add(int x, int y); sub(int x, int y); mul(int x, int y); rechne (int (*operation) (int,int),int x, int y); main_funktion_arg() { int erg; int x,y; puts(" Eingabe von zwei ganzen Zahlen "); scanf("%d %d",&x,&y); erg = rechne(add,x,y); printf(" Die Addition von x und y ist : %d\n",erg); erg = rechne(sub,x,y); printf(" Die Subtraktion von x und y ist : %d\n",erg); erg = rechne(mul,x,y); printf(" Die Multiplikation von x und y ist : %d\n",erg); } int add(int x, int y) { return (x + y); } int sub(int x, int y) { return (x - y); } int mul(int x, int y) { return (x * y); } int rechne (int (*operation) (int,int),int x, int y) {int ergebnis; return (*operation) (x,y); } 37 5.7. Aufgabe 1. Es sollen zwei Funktionen geschrieben werden, die die Buchstaben in einem String, der vom Terminal eingegeben wurde, in Groß- bzw. Kleinbuchstaben konvertiert. Der String wird als Argument übergeben 2. Es soll eine Funktion str_sort geschrieben werden, das einen String vom Terminal einliest und die Buchstaben im string alphabetisch sortiert. Der String wird als Argument übergeben. 3. Es sollen zehn ganze Zahlen in ein Array eingelesen werden. In einer Funktion my_sort soll dieses Array linear geordnet werden und anschliessend die Elemente sortiert ausgegeben werden. Tip : Zum Sortieren eines Array empfiehlt es sich eine Funktion zu schreiben die den Index des kleinsten Arrayelements in einem Array, beginnend bei einem Startindex sucht : int find_index_of_min(int numbers[], int length_of_numbers_array,int from,int initial_value) Eingabe : int numbers[] ... Array aller gespeicherten Zahlen int length_of_numbers_array ... Number of Elements in numbers int from ... start from Index within array int initial_value ... Startvalue, min should be smaller Ausgabe ; return value ... Index des kleinesten Elements innerhalb des array. 38 6. Pointer 6.1. Unterschied zwischen Pointer - und Standardvariablen Der Unterschied liegt darin, daß Standardvariablen auf eine Speicherzelle zeigen in der der primitive Wert der Variable gespeichert ist. Eine Pointervariable zeigt auf eine Speicherzelle in der die Adresse einer anderen Variable gespeichert ist. Pointer können auf primitve oder auch wieder auf Pointervariablen zeigen. Mit der Technik der Pointer können dann sehr einfach Adressen und Inhalte von Adressen verwaltet werden. Variable int x = 10; int y = -300; Variablenname Adresse der Speicherzelle Wert=primitiver Wert x <=> 1002 y <=> 1004 10 -300 Pointer int *p; p = &x; Pointername p <=> Adresse der Speicherzelle 2002 Wert=Adresse 1002 39 6.2. Deklaration eines Pointers Durch Vorstellen eines Sternes vor den Variablennamen wird bei der Deklaration der Variable automatisch eine Pointervariable deklariert: Beispiel : int *p; float *xp,*yp,*zp; Pointer sind Variablen, deren Wert eine Adresse ist, sie zeigen somit auf eine beliebige Speicheradresse. Die Verwendung von Pointern wird dahingegen eingeschränkt, daß jeder Pointer nur auf Variablen bestimmten Typs zeigen kann. Dieser Typ wird bei der Deklaration eines Pointers in Form des Pointertyps festgelegt. Bei der Arbeit mit Pointern werden zwei Operatoren wichtig : & (Adreß-Operator), und * (DereferenzierungsOperator). Der Adreß Operator liefert die Adresse einer Variable, der Dereferenzierungs Operator liefert den Wert der Adresse auf die die Pointervariable zeigt. Beispiel : int x; int *y; /* Deklaration einer int Variable */ /* Deklaration eines Pointers auf int */ ergibt : Variable x y x &x y *y ... ... ... ... Adresse der Speicherzelle Wert 1004 1008 entspricht entspricht entspricht entspricht dem der der dem Wert einer Variablen,, bisher nicht definiert Adresse der Variablen, also 1004 Adresse des Pointers, also 1008 Wert der Variable auf die der Pointer zeigt 40 Die Zuweisung x y = 10; = &x; /* Zuweisung eines Wertes*/ /* Zuweisung der Adresse einer Variable auf die der pointer zeigen soll */ ergibt : Variable x y Die Zuweisung *y = *y + 10; ergibt : Variable x y Die Zuweisung y = y + 10; Adresse der Speicherzelle Wert 1004 10 1008 1004 /* Ändere den Wert der Adresse auf die y zeigt */ Adresse der Speicherzelle Wert 1004 20 1008 1004 /* Erhöhe die Adresse auf die y zeigt */ ergibt : Variable x y Adresse der Speicherzelle Wert 1004 20 1008 1014 Die Zuweisung ergibt keinen Sinn y = *y + 10; /* Erhöhe die Adresse auf die y zeigt */ (Vorher y = &x ) ergibt : Variable x y Adresse der Speicherzelle Wert 1004 20 1008 30 41 6.3. Adreßarithmetik mit Pointern 6.3.1. Addition und Subtraktion einer Konstante Bei Pointern ist eine eigenschränkte Arithmetik erlaubt. Wird ein Pointer um eine konstante Zahl erhöht, bzw. erniedrigt, dann wird der Adreßwert des Pointers um die Anzahl von Bytes erhöht, wie die Elemente des Grundtyps belegen. Beispiel : char *pchar; short *px; char str[3]; short zahlen[3] /* /* /* /* Pointer auf char */ Pointer auf short */ Array of character, 3 Elemente = 3 * 1 Byte*/ Array of short, 3 Elemente = 3 * 2 Bytes*/ str[0] = 'a'; str[1] = 'b'; str[2] = 0; zahlen[0]=1; zahlen[1]=2; zahlen[2]=3; pchar = str; px = zahlen; /* Der Adreßwert nimmt den Wert der Adresse des /* ersten Elements von str an */ putchar(*pchar); /* ergibt die Ausgabe 'a' */ printf("%d",*px); /* ergibt die Ausgabe von 1 */ Variable str zahlen pchar px Adresse der Speicherzelle 1000 1001 1002 1003 1005 1007 1008 1009 Wert a b 0 1 2 3 1000 1003 pchar = pchar +1; /* Der Adresswert wird um 1 byte erhöht, /*der Pointer zeigt auf das nächste Element des strings */ putchar(*pchar); /* ergibt die Ausgabe 'b' */ pchar = pchar -1; /* Der Adresswert wird um 1 byte (da char) erniedrigt, der Pointer zeigt auf das vorherige Element des strings */ putchar(*pchar); /* ergibt die Ausgabe 'a' */ px = px + 2; /* Der Adresswert wird um 2*2 Bytes erhöht */ printf("%d",*px); /* ergibt die Ausgabe von 3 */ 42 6.3.2. Autoincrement und Autodecrement Bei Pointern kann man auch mit den Autoincrement ( ++ ) und Autodecrement ( -- ) Operatoren arbeiten. Autoincrement einer Pointervariablen bedeutet: zeige auf das nächste Element. Dementsprechend wird die Adresse des Pointers um die Anzahl der belegten Bytes eines Elements erhöht. Beispiel : char *pchar; /* Pointer auf char, es wird 1 byte belegt */ short *px; /* Pointer auf short, es werden 2 bytes belegt */ char str[3]; /* Array of character, 3 Elemente */ str[0] = 'a'; str[1] = 'b'; str[2] = 0; pchar = str; /* Der Adresswert nimmt den Wert der Adresse des /* ersten Elements von str an */ pchar++; /* autoincrement : */ pchar--; /* autodecrement : */ 6.3.3. Anzahl der belegten Bytes der Standardtypen Diese Angaben sind Implementationsabhängig. Bei den folgenden Angaben geht man von einem 16 bzw. 32 Bit Prozessor aus : char *p; short *p; int *x; long *x; float *y; double *z; /* /* /* /* /* /* belegt belegt belegt belegt belegt belegt 1 2 2 4 4 8 Byte */ Bytes */ Bytes */ Bytes */ Bytes */ Bytes */ p++; x++; y++; z++; /* /* /* /* erhöht erhöht erhöht erhöht den den den den Wert Wert Wert Wert von von von von p x y z um um um um 2 4 4 8 */ */ */ */ 6.3.4. Pointerarithmetik und die Prioritäten von Operatoren Bei gemeinsamer Verwendung arithmetischer und address Operatoren ist zu beachten, daß die Priorität der address Operatoren, es handelt sich um die Operatoren & und *, höher ist als die der arithmetischen Operatoren (+ und -). Die Priorität der Increment und Decrement Operatoren (-- und ++) ist aber genauso hoch wie die der address Operatoren. Stehen die Operatoren nebeneinander, so ist die Auswertungsreihenfolge von rechts nach links definiert. Eine Klammerung ist nötig um diese Reihenfolge zu verändern, um zu definieren, ob man auf den Adresswert oder auf den Wert auf den die Adresse zeigt verändern will. Beispiel : ++*p identisch zu ++(*p) erhöhe den Wert auf den der Pointer p zeigt um eins (* steht rechts von ++) 43 Steht zwischen den Operatoren der Variablenname, so ist die Reihenfolge der Auswertung wie gewöhnlich von links nach rechts: *p++ (*p)++ nimm den Wert auf die die Adresse zeigt und erhöhe die Adresse auf die p zeigt um eins. (++ steht rechts von der Variable) erhöhe den Wert um eins auf den p zeigt. p = p + 1 identisch zu p++ erhöhe die Adresse von p um 1 p = p - 2 identisch zu p -= 2 erniedrige die Adresse von p um 2 *p++ = *q++ weise den Wert von q an den Wert von p und erhöhe anschließend die Adressen von p und q Arithmetik mit Pointern : long x,*y; x = 20; y = &x; a) Welchen Wert haben x und y nach der folgenden Zuweisung : *y = *y + 10; Variable Adresse der Speicherzelle Wert x 1004 y 1008 1012 b) Welchen Wert haben x und y nach der folgenden Zuweisung : x = *y + 1; Variable Adresse der Speicherzelle Wert x 1004 y 1008 1012 c) Welchen Wert haben x und y nach der folgenden Zuweisung : *y += 1; Variable Adresse der Speicherzelle Wert x 1004 y 1008 1012 d) Welchen Wert haben x und y nach der folgenden Zuweisung : ++*y; Variable Adresse der Speicherzelle Wert x 1004 y 1008 1012 e) Welchen Wert haben x und y nach der folgenden Zuweisung : (*y)++; Variable Adresse der Speicherzelle Wert x 1004 y 1008 1012 44 f) Welchen Wert haben x und y nach der folgenden Zuweisung : *y++; Variable Adresse der Speicherzelle Wert x 1004 y 1008 1012 6.4. Pointer und Arrays In C sind die Konzepte Pointer und Arrays sehr ähnlich: Jeder indizierte Zugriff auf ein Array kann auch mit Pointeroperationen erreicht werden. Der Zugriff mit Pointern ist schneller als Arrayindizierung, aber oft nicht leicht zu durchschauen. Jeder Zugriff auf ein Array wird im Maschinencode als Pointerzugriff realisiert. 6.4.1. Die Deklaration eines Arrays und Pointer Beispiel : int a[3],i; int *pa; /* Schaffen eines Arrays für 3 Elemente */ /* Schaffen eines Pointers auf int Elemente */ for (i=0;i<3;i++) a[i] = i; Variable a pa Adresse der Speicherzelle 1000 1002 1004 1006 Wert 0 1 2 6.4.2. Pointer Adresswertzuweisung eines Arrays Der Variablenname eines Array entspricht der Anfangsadresse des Arrays. Beispiel : pa = a oder pa = &a[0] trägt in den Pointer pa die Adresse des ersten Elements von a ein, pa zeigt somit auf das Array a. Variable a pa Adresse der Speicherzelle 1000 1002 1004 1006 Wert 0 1 2 1000 45 6.4.3. Zugriff auf Arrayelemente über Pointer Jeder Zugriff auf die Arrayelemente a[i] kann auch als Pointerzugriff *(a+i) realisiert werden. Beispiel : Initialisierung eines Array über Pointer : int int int for oder zahlen[5]; i; *px; (i=0;i<5;i++) zahlen[i] = 0; px = zahlen; /* Übertragen der Anfangsadresse des Arrays */ for (i=0;i<5;i++) *(px+i) = 0; 6.4.4. Wertzuweisung an das Array über Arrayindizees und über Pointeradressen Jeder Wertzuweiung an Arrayelemente kann über Arrayindizees oder Pointer realisiert werden. Beispiel : a[2] = -100; pa = a; *pa = 50; *(pa+1) = 10; trägt in das dritte Element den Wert -100 ein. Der Wert auf den der Adresswert von pa zeigt wird auf 50 gesetzt, der auf den die Adresse pa+1 zeigt auf 10. Variable a pa pa+1 pa+2 Adresse der Speicherzelle 1000 1002 1004 1006 1008 10010 Wert 50 10 -100 1000 1002 1004 Zuweisung des Wertes von der Speicheradresse auf die der Pointers zeigt an eine Variable : x = *pa x = *(pa+1) x = *(pa+2) entspricht x = a[0] entspricht x = a[1] entspricht x = a[2] 46 6.4.5. Arraynamen als Funktionsargument Bei einem Arrayname als Funktionsargument wird automatisch die Adresse des ersten Elements übergeben. Innerhalb der Funktion wird ein Arrayargument als Adresse auf ein Element gesehen. Somit kann man Arrayargumente auch als Pointer betrachten. Beispiel : char str[10]; char *ptr; /* strlen, Rückgabe die Länge eines strings s */ int strlen(char *s) { int n; for (n=0; *s != '\0'; s++) n++; /* Falsch wäre : for (n=0; s != '\0'; s++) n++; */ /* Falsch wäre : for (n=0; s != '\0'; *s++) n++; */ return n; } Die Funktion hat als Argument einen Pointer auf char. Die interne Inkrementierung ist somit legal. Wird strlen aufgerufen, so hat die Inkrementierung von s keinen Einfluß auf das Funktionsargument, da die Funktion strlen auf einer lokalen Kopie von s arbeitet. Aufrufe sind zulässig wie : strlen("hello world"); strlen(str); strlen(ptr); /* string Konstante */ /* char array str[10] */ /* char *ptr */ 6.4.6. Arrays als Funktionsparameter Ein Array kann als Funktionsparameter auf zwei Arten als Argument deklariert werden : char a[] oder char *xa Die Deklaration als Pointer ist vorzuziehen, da sie die Adresscharakteristik mehr in den Vordergrund stellt. Innerhalb der Funktion muß der Programmierer analog der Definition mit dem Argument arbeiten. Beispiel : int strlen_pointer(char *s) { int n; for (n=0;*s != '\0'; s++) n++; return n; } oder int strlen_array(char a[]) { int n; for (n=0;a[n] != '\0'; n++) ; return n; } Einer Funktion kann auch die Adresse eines Subarrays übergeben werden. Innerhalb der Funktion beginnt dann das Array auf der Adresse des Subarrays. 47 Beispiel : char str[] = "guten"; char *pa; int len; pa = str; len = strlen(str); len = strlen(&str[4]); len = strlen(pa); len = strlen(pa+2); /* /* /* /* Ausgabe 5 */ Übergabe "n" : Ausgabe 1 */ Ausgabe 5 */ Übergabe "ten" : Ausgabe 3 */ *(pa+1) *(pa+6) str[0] str[1] str[6] *pa g u 1024 1025 t e 1026 1027 n \0 1028 1029 1030 Beispiel : len = strlen(pa+2); /* Übergabe "ten" : Ausgabe 3 */ int strlen (char *p) erhält als Argument die Adresse des dritten Elements ( Adresse 1026). Also einen Pointer auf das zweite Element : p p+1 p+2 intern entspricht intern entspricht intern entspricht str[2] extern. str[3] extern. str[4] extern. In diesem Zusammenhang kann man auch mit negativen Indizees arbeiten : p-1 intern entspricht str[1] extern. Achtung : Der Kompiler überwacht nicht das Überschreiten von Arraygrenzen ! 48 Beispiel : /* count number of words in a string */ #include <ctype.h> #include <stdio.h> char zeile[132] = {0}; int wrd_count(char *s); main() { puts(" Eingabe Zeile "); gets(zeile); printf(" Die Anzahl der Wörter ist : %d \n",wrd_count(zeile)); } 6.4.7. Aufgabe Schreibe eine Funktion, die zwei Stringargumente konkateniert: function conc(char *s1,char *s2,char *s1s2); ja isspace ja nein falls string Ende ja fertig 49 nein char nein cnt++ 6.4.8. Pointer Arrays und Pointer to Pointer Da Pointer Variablen sind, können sie wie normale Variablen in Arrays gespeichert werden. Bei einem Beispiel zeigt sich der Vorteil : Sortieren von strings. Jeder Pointer im Array zeigt auf einen string, soll die Reihenfolge der strings geändert werden, muß nur die Adresse im richtigen Arrayindex geändert und kein Stringinhalt umkopiert werden. Das geordnete Array wird der Reihe nach über die Indizes angesprochen. Beispiel : char *lines[5]; Index 0 1 2 3 4 Adresse 1024 1030 1036 1042 1048 Wert 2000 2006 2011 2014 2020 Wert Hallo\0 Leute\0 wie\0 gehts\0 euch\0 Adresse 1024 1030 1036 1042 1048 Wert 2000 2006 2011 2014 2020 Index 0 1 2 3 4 Wert 2000 2006 2020 2014 2011 nach dem alphabetischen Sortieren : Die Zuweisung zum Umkopieren von Pointern lautet hier z.B. : lines [4] = lines[2]; char *phelp; char *lines [5]; phelp lines [5] lines [3] = lines [5]; = lines [3]; = phelp; 50 6.4.9. Schaffen von Speicherplatz bei Array of Pointer Arbeitet man mit Arrays von pointern, werden zur Deklarationszeit die Arrrayelemente als Pointer definiert. Es werden lediglich Pointerarrayelemente im Speicher abgelegt. Damit die einzelnen Pointer auf Elemente zeigen können muß für diese Elemente erst Speicher beschafft werden. Diese Speicherbeschaffung nennt man dynamische Speicheroperationen. Dazu gibt es in der Standardlibrary Funktionen. Beschaffen von Speicher : #include <stdlib.h> void *calloc(size_t numberofmembers, size_t numberofbytes); oder : void *malloc(size_t numberofbytes); Ergebnis : Anfangsadresse des Speicherbereichs oder NULL, falls kein Speicher mehr zur Verfügung ist. Verändern der Größe von beschafftem Speicherplatz : void realloc(void *pntr, size_t numberofbytes); numberofbytes definiert die neue Anzahl der Bytes. *pntr darf nur auf Speicherplatz zeigen, der mit malloc oder calloc beschafft wurde. Freimachen von Speicherplatz : #include <stdlib.h> free(void *pntr); Ergebnis : Speicher wird wieder frei für die nächste alloc Funktion. Achtung : Nur Speicher, der mit alloc, malloc oder realloc beschafft wurde, kann mit free freigegeben werden. 51 Beispiel : char *strings[5] int i; Index 0 1 2 3 4 Adresse 1024 1026 1028 1030 1032 Nun wird für jeden Pointer auf char ein String der Länge 3 beschafft : for (i=0;i<5i++) strings[i] = malloc (3); Adresse 1024 1026 1028 1030 1032 Adresse 2000 2001 2002 2003 2004 2005 2006 usw. Wert 2000 2003 2006 2009 2012 Wert erster Buchstabe, String 1 zweiter Buchstabe, String 1 dritter Buchstabe, String 1 erster Buchstabe, String 2 zweiter Buchstabe, String 2 dritter Buchstabe, String 2 ersterBuchstabe, String 3 usw. 52 6.5. Mehrdimensionale Arrays C bietet die Möglichkeit mehrdimensionale Arrays zu definieren, die analog der aus der Mathematik bekannten Matrixschreibweise entsprechen : Beispiel : Ein Array mit 3 Zeilen und 6 Spalten : static float x[3][6] = { { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0}, { 10.0, 20.0 30.0, 40.0, 50.0, 60.0} { 100.0, 200.0, 300.0, 400.0, 500.0, 600.0} Zugriffe : } x [2][5] = 600.0; /* zuerst Zeile, dann Spalte */ Da Arraynamen und Pointer äquivalent sind, kann man auf Elemente in einem mehrdimensionalen Array auf verschiedene Art zugreifen: x [i][j] ist äquivalent zu : *(x[i] + j) (* (x + i)) [j] *((*(x+i)) + j) *(&(x[0][0] + 6*i + j) Die Klammern sind notwendig, da die eckigen Klammern [] höhere Wertigkeit haben als der Wert-Operator * . Werden mehrdimensionale Arrays als formale Parameter von Funktionen übergeben, müssen alle Dimensionen, außer der ersten angegeben werden. : int max (float x [][6]) oder int max (float *x [6]) oder int max (float x [3][6]) 53 6.6. Argumente an das Hauptprogramm übergeben : An das Hauptprogramm main() können bei der Programmiersprache C beim Starten des Programms Argumente übergeben werden : Beispiel : Der Programmfile heißt auswertung, dann kann das Programm gestartet werden mit z.B.: auswertung 1 2 3 4. Im Hauptprogramm stehen die Werte der externen Parameter zur Verfügung, wenn die Hauptfunktion main folgendermaßen definiert wurde : main(int argc, char *argv[]); in argc wird die Anzahl der Elemente in der Aufrufzeile gespeichert (Im Beispiel 5), der zweite Parameter ist ein Array von Pointern, wobei jedes Element des Arrays auf ein Element in der Aufrufzeile zeigt. Die Elemente der Aufrufzeile werden als string übergeben. im Beispiel : argc = 5 argv[0] = auswertung argv[1] = 1 argv[2] = 2 argv[3] = 3 argv[4] = 4 54 6.7. Aufgaben Beispiele zu Pointer : /* Aus C-Puzzlebuch von A.R. Feuer, Hanser Verlag,1985, S.65) 1. Was gibt das folgende Programm aus : #define PR(x) printf("%d \n",x); int a[] = {0,1,2,3,4}; main() { int i,*p; for (i=0;i<=4;i++) PR(a[i]); for (p = &a[0];p<&a[4];p++) PR(*p); for (p = &a[0],i=1;i<=5;i++) PR(p[i]); for (p = a,i=0;p+i<=a+4;p++,i++) PR(*(p+i)); for (p=a+4;p>=a;p--) PR(*p); for (p=a+4,i=0;i<=4;i++) PR(p[-i]); for (p=a+4,p>=a;p--) PR(a[p-a]); } /* Aus Darnell, C A Software Engeniering approach */ Folgende Deklarationen seien gegeben : static int a[2][3] = { { -3,14,5},{1,-10,8}}; static int *b[] = { a[0],a[1]}; int *p = b[1]; a) *b[1]; PR(*b[1]); b) *(++p) c) *(*a+1)+1 d) *(--p-2) 2 . Welche Ausdrücke sind gleich : a) *(a[j]+k) b) **(a[j+k]) c) (*(a+j))[k] d) (*(a+k))[j] e) *((*(a+j))+k f) **(a+j)+k g) *(&a[0][0] + j + k) 55 6.8. Beispiele für Arrays of Strings Im nachfolgenden Beispielen die Arbeit mit Arrays of Strings vorgeführt. Außerdem wird das Lesen von Strings aus einem File gezeigt. Zuerst werden alle Zeilen eines Files in ein String Array gelesen und anschließend die Reihenfolge der strings umgedreht Bsp: /* read strings from a file */ #include <stdio.h> #include <stdlib.h> #include <ctype.h> #define MAX_NUM_OF_LINES 20 #define STRSIZE 80 char *in_buf[MAX_NUM_OF_LINES]; char line[STRSIZE]; main() { char filename[STRSIZE]; FILE *in_file; int num_of_lines=0; int i; printf(" Eingabe filein >>>"); gets(filename); if ((in_file = fopen(filename,"r")) == NULL) { printf ("Error opening filename %s \n",filename); exit (1); } while (fgets(line,STRSIZE,in_file) != 0 && num_of_lines < MAX_NUM_OF_LINES) { printf("line= %s",line); in_buf[num_of_lines] = (char *) malloc (strlen(line)); line[strlen(line)-1]=0; /* remove \n, like chop operator */ strcpy(in_buf[num_of_lines],line); num_of_lines++; } puts(" Der File wurde eingelesen ..."); for(i=0;i<num_of_lines;i++) printf("in_buf[%d]=%p -> %p = %s\n",i,&in_buf[i],in_buf[i],in_buf[i]); reverse_elements(in_buf,num_of_lines); puts(" Die Reihenfolge der Zeilen wurde umgedreht ..."); for(i=0;i<num_of_lines;i++) printf("in_buf[%d]=%p -> %p = %s\n",i,&in_buf[i],in_buf[i],in_buf[i]); puts(" End of Progam "); } 56 int reverse_elements(char *in_buf[],int num_of_lines) { /* Die Routine dreht die Reihenfolge aller Strings im in_buf um IN: char *in_buf[] int int num_of_lines ... Array of strings ... anzahl der Zeilen OUT: char *in_buf[] ... ungedrehtes Array of strings */ int index; for (index=0; index < num_of_lines/2; index ++) { /* 1. Fall …. */ printf(" Swap Element %d with %d \n",index,num_of_lines-index-1); swap_array_elem(in_buf,index,num_of_lines-index-1); /* Ende 1. Fall */ /* 2. Fall …. */ printf(" Swap Address %p with %p \n", &in_buf[index],&in_buf[num_of_lines-index-1]); swap_strings(&in_buf[index],&in_buf[num_of_lines-index-1]); /* Ende 2. Fall */ } } /**********************************************************/ int swap_array_elem(char *array[],int from,int to) { char *temp; temp = array[from]; array[from] = array[to]; array[to] = temp; } /**********************************************************/ int swap_strings(char **a,char **b) { char *help; printf(" swap Inhalt von Pointer %p mit %p \n",a,b); help = *a; printf(" save %p\n",help); *a = *b; *b = help; return; } /**********************************************************/ int swap_void(void **a,void **b) { char *help; help = a; a = b; b = help; } 57 Ausgabe 1. Fall: swap_array_elem(in_buf,index,num_of_lines-index-1); line= line= line= line= line= 1 2 3 4 5 abc def ghi jkl mno in_buf[0]=1400002e0 in_buf[1]=1400002e8 in_buf[2]=1400002f0 in_buf[3]=1400002f8 in_buf[4]=140000300 -> -> -> -> -> in_buf Element 0 1 2 3 4 Die Reihenfolge der in_buf[0]=1400002e0 in_buf[1]=1400002e8 in_buf[2]=1400002f0 in_buf[3]=1400002f8 in_buf[4]=140000300 in_buf Element 0 1 2 3 4 140000940 140000960 140000980 1400009a0 1400009c0 = = = = = 1 2 3 4 5 abc def ghi jkl mno Adresse of Array 1400002e0 1400002e8 1400002f0 1400002f8 140000300 Zeilen wurde -> 1400009c0 -> 1400009a0 -> 140000980 -> 140000960 -> 140000940 Adresse of Array 1400002e0 1400002e8 1400002f0 1400002f8 140000300 Zeigt auf 140000940 140000960 140000980 1400009a0 1400009c0 Inhalt 1 abc 2 def 3 ghi 4 jkl 5 mno umgedreht ... = 5 mno = 4 jkl = 3 ghi = 2 def = 1 abc Zeigt auf 1400009c0 1400009a0 140000980 140000960 140000940 58 Inhalt 5 mno 4 jkl 3 ghi 2 def 1 abc 2. Fall: swap_strings(&in_buf[index],&in_buf[num_of_lines-index-1] Zweiter Fall: printf(" Swap Address %p with %p \n",&in_buf[index],&in_buf[num_of_lines-index-1]); swap_strings(&in_buf[index],&in_buf[num_of_lines-index-1]); Swap Swap Swap Swap Element Address Element Address 0 with 4 1400002e0 with 140000300 1 with 3 1400002e8 with 140000ef8 Die Reihenfolge der in_buf[0]=1400002e0 in_buf[1]=1400002e8 in_buf[2]=1400002f0 in_buf[3]=1400002f8 in_buf[4]=140000300 in_buf Element 0 1 2 3 4 Zeilen wurde -> 1400009c0 -> 1400009a0 -> 140000980 -> 140000960 -> 140000940 Adresse of Array 1400002e0 1400002e8 1400002f0 1400002f8 140000300 umgedreht ... = 5 mno = 4 jkl = 3 ghi = 2 def = 1 abc Zeigt auf 1400009c0 1400009a0 140000980 140000960 140000940 59 Inhalt 5 mno 4 jkl 3 ghi 2 def 1 abc } 6.9. Beispiel für die Verwendung von Pointer of Pointer Im nächsten Beipiel wird ein Programm vorgestellt, das die Notwendigkeit der Parameterübergabe von Pointer auf Pointer zeigt. Wird das Programm mit der Routine get_memory_falsch(str) aufgerufen, stürzt das Programm ab. Der Aufruf mit : get_memory_richtig(&str) funktioniert. Im weiteren zeigt das Programm, daß auch ein falsches Programm funktionieren kann. Wird die Deklaration der Integervariable int i auskommentiert, dann funktioniert das Programm in beiden Fällen. Beispiel /* Dieses Programm zeigt, wieso beim Beschaffen von Speicher in einer Subroutine ** verwendet werden muß. Die Routine get_memory_falsch(str) Die Routine get_memory_richtig(&str) funktioniert. */ #include <stdio.h> main() { int i; /* Bemerkung: Falls die vorherige Zeile auskommentiert wurde, funktioniert das Programm in beiden Fällen */ char *str; // } get_memory_falsch(str); get_memory_richtig(&str); printf(" Read String >>>>"); fgets(str,132,stdin); printf(" String is = %s\n",str); get_memory_falsch(char *string) { string = (char *) malloc(132); /* Hier wurde Speicher für die STACK Variable string besorgt */ } get_memory_richtig(char **string) { *string = (char *) malloc(132); /* Hier wurde Speicher besorgt, der unter der Adresse von String abgespeichert wird */ } 60 6.9.1. Beschreibung des Programmablaufs 6.9.1.1. // Falscher Aufruf: get_memory_falsch(str); Das Programm legt folgendes Adressbuch an: int i; char *str; Typ Variable int char * i str Adresse der Variable 628 624 Wert Adresse der Variable 800 Wert Adresse der Variable 800 Wert 0 600 Aufruf der Routine: get_memory_falsch(str); Innerhalb der Rouitine wird folgendes Adressbuch angelegt: get_memory_falsch(char *string) Typ Variable char * string Argumentübergabe get_memory_falsch(str); liefert folgendes Adressbuch: Typ Variable char * string 600 Zweisung innerhalb der Routine:: string = (char *) malloc(132); besorgt Speicher für 132 Bytes, beginnend mit der Adresse 2000 und überschriebt den Wert 600 Typ Variable char * string Adresse der Variable 800 Wert 2000 Da der Wert der Adresse 2000 nur unter der Temporären Variable string abgespeichert wurde, hat sich die das Hauptprogramm diese Adresse nicht gemerkt. Die Anweisung: fgets(str,132,stdin); versucht im Speicher unter der Adresse 600 die eingelesenen Buchstaben abzuspeichern. 61 6.9.1.2. // Richtiger Aufruf: get_memory_richtig(&str); Das Programm legt folgendes Adressbuch an: int i; char *str; Typ Variable int char * i str Adresse der Variable 628 624 Wert Adresse der Variable 800 Wert Adresse der Variable 800 Wert 0 600 Aufruf der Routine: get_memory_richtig(&str); Innerhalb der Rouitine wird folgendes Adressbuch angelegt: get_memory_richtig(char *string) Typ Variable char * string Argumentübergabe get_memory_richtig(&str); liefert folgendes Adressbuch: Typ Variable char ** string 624 Zweisung innerhalb der Routine:: *string = (char *) malloc(132); besorgt Speicher für 132 Bytes, beginnend mit der Adresse 2000 und speichert diese Adresse unter *string, also der Adresse 624 ab: Zweisung: *string = (char *) malloc(132); ergibt Speicher für 132 Bytes, beginnend mit der Adresse 2000 Typ Variable char * string Adresse der Variable 800 Wert Adresse der Variable 628 624 Wert 624 Im Hautprogramm: Typ Variable int char * i str Der Wert der Adresse 2000 ist nun im Hauptprogramm unter der Variable str gespeichert. Die Anweisung: fgets(str,132,stdin); speichert problemlos unter der Adresse 2000 die eingelesenen Buchstaben ab. 62 0 2000 6.10. Beispiel zum Blockweisen Einlesen einer Textdatei und Erzeugen eines Lexikons /** Aufgabe: Programm soll einen Text zeilenweise einlesen die Zeile in einzelne "Wörter" aufspalten. Die Wörter sollen für die ganze Datei in einer Datenstruktur gespeichert werden, so dass dann sortiert werden könnte oder dergleichen. Wir werden nach dem Durchlauf durch die Datei die aufgespaltenen Wörter lediglich auf eine Datei schreiben. Vorgestellt wird ein Verfahren dynamisch Speicher für die Pointer auf die Strings und für die Strings selbst zu allozieren. 1.) für jedes Wort wird mit malloc Speicher bereitgestellt in seiner Groesse. 2.) für die Stringliste wird ein Doppelpointer verwendet, an den ein Anfangspuffer von 20 Einträgen mit malloc zugewiesen wird. Nach dem Abarbeiten einer Zeile wird der Speicherbereich um die Anzahl der gelesenen Wörter erweitert. **/ #include <stdio.h> #include <string.h> #define MAXLETTERS 132 #define PUFFERGROESSE 20 void split(char * temp_string); char **lexikon; int speicher_groesse; int lexikon_zeiger; //globale Variablen int main(void){ FILE * in_file; FILE * out_file; char in_filename[MAXLETTERS]; char out_filename[MAXLETTERS]; char temp_string[MAXLETTERS]; int i; //Pufferspeicher für Lexikon besorgen, //Puffer ist bei PUFFERGROESSE 20, 20 Pointer, //sprich 20 Lexikoneinträge lexikon = (char**) malloc(sizeof(char*) * PUFFERGROESSE); speicher_groesse = PUFFERGROESSE * sizeof(char*); printf("speichergroesse: %d\n",speicher_groesse); printf("give me the name of the infile>>>"); fgets(in_filename,MAXLETTERS,stdin); in_filename[strlen(in_filename) -1] = 0; //chomp 63 //Filehandele auf Filepionter oeffnen if ((in_file = fopen(in_filename,"r")) == NULL){ printf("File %s konnte nicht geöffnet werden\n",in_filename); exit(1); } printf("give me the name of the outfile>>>"); fgets(out_filename,MAXLETTERS,stdin); out_filename[strlen(out_filename) -1] = 0; //chomp //Filehandele auf Filepionter oeffnen if ((out_file = fopen(out_filename,"w")) == NULL){ printf("File %s konnte nicht geöffnet werden\n",in_filename); exit(1); } //Infile einlesen: while( fgets(temp_string,MAXLETTERS,in_file)){ if(temp_string[strlen(temp_string) -1] == 10) //chomp temp_string[strlen(temp_string) -1] = 0; printf("in main before split call\n"); split(temp_string); //Zeile zerlegen i =0; while(i<(speicher_groesse - PUFFERGROESSE * sizeof(char*))/sizeof(char*)){ //nur Kontrollausgabe nach split printf("%d: %s\n",i,*(lexikon+i)); //ueberlegen warum Schleifenbed. i++; // funktioniert } } /********Wortliste auf Datei schreiben ***/ i =0; while(i < lexikon_zeiger){ fprintf(out_file,"%s\n",*(lexikon+i)); i++; //formatierte Ausgabe } } void split(char * temp_string){ char *temp_word; char *zeiger_temp_word; int inword = 1; int test; int i = 0; int wort_zahl = 0; temp_word = (char*) malloc(sizeof(char) * MAXLETTERS); zeiger_temp_word = temp_word; //zeiger für Pointerarithmetik printf("in split before while\n"); while(*temp_string){ //waren innerhalb bleiben innerhalb if(inword ==1 && (!isspace(*temp_string))){ 64 *temp_word = *temp_string; temp_word++; temp_string++; } } else //waren auserhalb des Wortes kommen jetzt innerhalb if(inword == 0 && !isspace(*temp_string)){ inword = 1; *temp_word++ = *temp_string++; } else //waren ausserhalb bleiben ausserhalb if(inword == 0 && isspace(*temp_string)){ temp_string++; } //waren innerhalb, kommen heraus, schreiben! else{ printf("in split before writing to lexikon\n"); wort_zahl++; *temp_word = 0; temp_word = zeiger_temp_word; //pointer zurücksetzen *(lexikon+lexikon_zeiger) = (char*) malloc(strlen(temp_word) +1); strcpy(*(lexikon+lexikon_zeiger),temp_word); printf("in split after copy string\n"); //lexikon++; lexikon_zeiger++; inword = 0; //sind ausserhalb temp_string++; } //nach while, muessen noch das letzte Wort schreiben printf("in split before writing last to lexikon\n"); *temp_word = 0; temp_word = zeiger_temp_word; //pointer zurücksetzen *(lexikon + lexikon_zeiger) = (char*) malloc(strlen(temp_word) +1); strcpy(*(lexikon+lexikon_zeiger) ,temp_word); printf("in split after copy string\n"); free(temp_word); //tempword brauchen wir nicht mehr malloc Bereich freigeben wort_zahl++; lexikon_zeiger++; //lexikon++; //pufferspeicher wieder auffüllen printf("Speichergroesse Pointer to strings vorher: %d\n", speicher_groesse); printf("Anzahl eingelesener Woerter, %d\n",wort_zahl); lexikon = (char **) realloc(lexikon, (speicher_groesse +sizeof(char*) * wort_zahl)); speicher_groesse = speicher_groesse + wort_zahl * sizeof(char*); printf("Speichergrosse Pointer to strings jetzt: %d\n",speicher_groesse); return ; } 65 6.11. Aufgaben Aufgabe (Das Meisterstück!) : Bemerkung: Verwenden sie zur Lösung die Beispiele 6.8 und 6.9 im Script. 1. Lesen Sie zwei Strings ein und vergleichen Sie, die Strings mit Hilfe der Systemroutine strcmp() alphabetisch. Geben Sie aus, welcher String alphabetisch größer oder kleiner ist. Data: char string1[132]; char string2[132]; 2. Implementieren Sie die Aufgabe 1 erneut, jedoch mit Pointer of Characters. Bei diesem Programm muß der Speicher für die Strings mit malloc besorgt werden. Data: char *string1; char *string2; 3. Erweitern Sie die Aufgabe 2 um eine Vertauschfunktion: swap_strings Es sollen der Inhalt beider Strings vertauscht werden. int swap_strings(char **s1,char **s2) { char *help; printf(" swap Strings s1= %p = %s mit s2= %p = %s \n",s1,s1,s2,s2); help = *s1; printf(" save s1: %p = %s\n",help,help); *s1 = *s2; *s2 = help; return; } 3. Definieren Sie sich ein Array of Pointer to Character mit maximal 5 Einträgen und lesen Sie 5 Strings in dieses Array vom Terminal ein. Auch bei diesem Programm muß der Speicher für jedes Array Element mit malloc besorgt werden. Data: #define NUM_OF_WORDS 5 char *words[NUM_OF_WORDS]; 4. Nachdem Sie in Aufgabe 2 gesehen haben, wie Strings verglichen werden, erweitern Sie die Aufgabe 3 um eine Sortierfunktion. Der Sortieralgorithmus soll wie in der Aufgabe "Sortieren von 10 Zahlen" implementiert werden. Zum Vertauschen von Arrayelementen können Sie die Funktion swap_strings aus der Aufgabe 3 verwenden. 66 7. Strukturen Der Strukturmechanismus erlaubt Variablen verschiedenen Typs zu einem eigenen, neudefinierten Typ zusammen zufassen. Nun kann man Variablen von diesem neuen Typ deklarieren, ihnen Werte zuweisen, verändern oder herauslesen. Beispiel : Gegeben sei der Basistyp punkt, der sich mit den ganzzahligen x und y Koordinaten beschreiben läßt : struct point { int x; int y; }; Das Schlüsselwort struct kündigt einen selbstdefinierten Variablentyp an, sein Name, auch tag genannt entspricht der in geschweiften Klammern aufgeführten Definition. Die Elemente in der Typdefinition heißen member. Jeder Member kann für sich wieder eine Struktur sein : struct rect { struct point p11; struct point p12; } area ; Eine Struktur, ein Variablenname oder der Name eines Members können gleich lauten, da der Kontext im Programm genau definiert um welche Art von Variablennamen es sich handelt. 7.1. Variablendeklaration einer Struktur Nachdem ein tag definiert wurde, können Variablen von diesem Typen deklariert werden. 1. Möglichkeit : struct point { int x; int y; } p1, p2, p3; 2. Möglichkeit : struct point p1,p2,p3; /* Falls vorher point definiert wurde */ 67 7.2. Wertzuweisung an eine Strukturvariable Eine Struktur kann bei der Deklaration initialisiert werden : struct point p1 = { 100, 200 }; Ihr kann ein Wert später zugewiesen werden : p1 = { 100,200 }; Der Typ einer Funktion kann auch eine Struktur sein : struct point add_value_to_koordinate(struct point erg) { erg.x = erg.x + 1; erg.y = erg.y + 1; erg = { x,y }; return erg; } 7.3. Zugriff auf einen Member struktur_variablen_name.member Der Punkt Operator verbindet eine Variable mit einem member. Bei einem member der selbst eine Struktur darstellt, wird der Punktoperator sukzessive angewendet: Beispiel : p1.x = 300; area.p11.y = 100; 7.4. Definition eines Namens (Synonym) für eine neue Struktur Für jede definierte Struktur kann man mit typedef ein Synonym einführen. Nach dieser Definition kann man Variablen von diesem Typ unter Angabe des Synonyms deklarieren. Beispiel : typedef int length; typedef char * string; length a,b,c; string str; 68 7.5. Pointer auf Strukturen Wie bei primitiven Variablen kann man auch Pointer auf Strukturen definieren. Der Adressoperator & liefert die Anfangsadresse einer Struktur. Um auf den Wert eines members zu zeigen verwendet man den Dereferenzierungsoperator -> (zwei Zeichen : minus gefolgt von einem größer Zeichen). Beispiel : struct point *pp; pp->member; /* entspricht (*pp).member) */ 7.6. Pointer als Funktionsargument Werden bei Funktionen Variablen von Strukturen als Argument übergeben, so wird beim Aufruf der Funktion der gesamte Inhalt der Struktur auf den Stack kopiert. Aus Performance Gründen sollen deshalb die Adressen der Strukturvariablen übergeben werden. Beispiel : struct point { int x; int y; } *pp; int init_point(struct point *) { pp->x = 0; pp->y = 0; return; } Aufruf : init_point(pp); 7.7. Ausdrucken von Strukturen Es ist nicht möglich Strukturen direkt mit ihrem Variablennamen auszudrucken. Die printf Funktion kann nur den Wert primitiver Objekte ausdrucken (int, float, char). Deshalb müssen die member der Struktur vom Programmierer direkt spezifiert, deren Wert er ausdrucken will. Beispiel : printf(" x Wert = %d, y Wert = %d \n",p1.x,p1.y); 7.8. Operationen auf Strukturen Zulässige Operationen auf Strukturen sind : a) Kopieren ganzer Strukturen b) Zuweisung an die gesamte Struktur c) Bestimmen seiner Anfangsadresse mit dem & Operator d) Zugreifen auf seine Member. 69 Beispiel : struct point { int x; int y; } x1,x2,x3,*px1; x1.x = x1.y = 100; x2 = x1; px1 = &x1; printf(" x1 : x Wert = %d, y Wert = %d \n",x1.x,x1.y); printf(" x1 : x Wert ueber Pointer = %d, y Wert = %d \n",px1->x,px1->y); 7.9. Beispiele für Strukturen : #include <stdio.h> #define XMAX 512 #define YMAX 360 struct point {int x; int y;}; struct rect {struct point pt1; struct point pt2;}; typedef struct point stpoi; typedef struct rect * pstrec; typedef struct rect strec; main() { struct point middle,tripple,sum; struct rect screen,screen1; pstrec pointer_screen; /* Prototypen : */ stpoi makepoint(int,int); stpoi addpoint(struct point, struct point); pstrec screen_relative(pstrec); screen.pt1 = makepoint(0,0); screen.pt2 = makepoint(XMAX,YMAX); screen1 = screen; pointer_screen = &screen; middle = makepoint((screen.pt1.x + screen.pt2.x)/2, (screen.pt1.y + screen.pt2.y)/2); tripple = makepoint((screen.pt1.x + screen.pt2.x)/3, (screen.pt1.y + screen.pt2.y)/3); sum = addpoint(middle,tripple); screen_relative(&screen); } printf(" Das Ergebnis = \n"); printf(" middle, x = %d, y = %d \n",middle.x,middle.y); printf(" tripple, x = %d, y = %d \n",tripple.x,tripple.y); printf(" sum, x = %d, y = %d \n",sum.x,sum.y); printf(" screen p1(x,y) = (%d %d), p2(x,y) = (%d %d)\n", screen.pt1.x,screen.pt1.y,screen.pt2.x,screen.pt2.y); stpoi makepoint(int x,int y) { stpoi temp; 70 temp.x = x; temp.y = y; return temp; } stpoi addpoint(struct point p1, struct point p2) { p1.x += p2.x; p1.y += p2.y; return p1; } pstrec screen_relative(pstrec screen) { strec help_screen; int maximumx,maximumy; float relationx,relationy; maximumx = screen->pt2.x; maximumy = screen->pt2.y; relationx = 512./ maximumx; relationy = 256./ maximumy; screen->pt2.x = (int) screen->pt2.x * relationx; screen->pt2.y = (int) screen->pt2.y * relationy; 71 8. Dynamische Datenstrukturen Die elementaren Datenstrukturen legen mit der Deklaration einer Variable den Wertebereich und seine Struktur im Speicher fest. Solche Datenstrukturen nennt man statisch. Strukturen, die sich zur Ausführungszeit verändern können, nennt man dynamisch. Das Schema der Rekursion ermöglicht dynamische Strukturen zu realisieren, indem der Wert einer Strukturkomponente einem Verweis auf die gleiche Datenstruktur entspricht. Beispiel Ausdrücke: Ein Ausdruck bestehe aus einem Term, gefolgt von einem Operanden und einem weiteren Term. Ein Term ist entweder ein Variable oder ein geklammerter Ausdruck: z.B.: x + y, oder (x + y) * z Die Struktur dazu kann lauten: struct term { struct term *op1; operator op; struct term *op2; } 8.1. Strukturart: Verkettete Liste Jedes Element zeigt auf seinen Nachbarn. Somit besteht jedes Listenelement aus seinem Listenwert und einer Adresse, die auf das nächste Element zeigt. Dazu gibt es Operationen, die ein neues Listenelement schaffen, bzw. löschen. Vorteile: - Die Dimension der Liste muß vorher nicht festgelegt werden - Umorganisation der Liste ist einfacher Nachteile: - Direkter Zugriff auf einzelne Elemente ist schwerer, da die verkettete Liste nur von Element zu Element durchlaufen werden kann. Bsp Konstruktion: struct knoten { int inhalt; struct knoten *nachfolger; }; struct knoten *kopf,*z,*x; Selektion: kopf.inhalt = 100; kopf->next = z; 72 8.2. Beispiel für Listen /* D(yn)amische Datenstrukturen Max */ #include <stdio.h> struct knoten { int inhalt; struct knoten *nachfolger; }; int fill_queue(struct knoten *position); int print_queue(struct knoten *position); struct knoten * init_queue(); struct knoten *fuege_neues_element_an(int eingabe,struct knoten *curr_pos); int print_queue(struct knoten *curr_pos); int print_queue_rek(struct knoten *curr_pos,int number); int free_queue_rek(struct knoten *curr_pos); main() { struct knoten *kopf; puts(" Hallo, hier ist das Programm struc_rek.c "); kopf = init_queue(); fill_queue(kopf); print_queue(kopf); printf(" Ausgabe rekursiv ... \n"); print_queue_rek(kopf,1); free_queue_rek(kopf); } struct knoten * init_queue() { struct knoten *curr; curr = (struct knoten *) malloc(sizeof *curr); curr->inhalt = 1; curr->nachfolger = NULL; return curr; } int fill_queue(struct knoten *position) { struct knoten *curr_pos; int eingabe; curr_pos = position; while (printf("Enter zahl >>>") && scanf("%d",&eingabe) != EOF) { curr_pos = fuege_neues_element_an(eingabe,curr_pos); } } 73 struct knoten *fuege_neues_element_an(int eingabe,struct knoten *curr_pos) { struct knoten *neu; printf( " Fuege %d an \n",eingabe); // get new memory for a new node neu = (struct knoten *) malloc (sizeof *neu); // initialize node neu->inhalt = eingabe; neu->nachfolger = NULL; // connect current_position with new node curr_pos->nachfolger = neu; // increment current_position to created node curr_pos = neu; return curr_pos; } int print_queue(struct knoten *curr_pos) { printf(" Drucke queue ... \n"); while (curr_pos->nachfolger != NULL) { printf(" Element %d \n",curr_pos->inhalt); curr_pos = curr_pos->nachfolger; } printf(" letztes Element %d \n",curr_pos->inhalt); return; } int print_queue_rek(struct knoten *curr_pos,int number) { printf(" Elementnummer %d = %d \n",number,curr_pos->inhalt); if (curr_pos->nachfolger != NULL) print_queue_rek(curr_pos->nachfolger,number+1); } int free_queue_rek(struct knoten *curr_pos) { struct knoten *pos ; printf(" Delete %d \n",curr_pos->inhalt); if (curr_pos->nachfolger != NULL) { pos = curr_pos->nachfolger; free (curr_pos); free_queue_rek(pos); } } 8.3. Strukturart: Stapel Die wichtigste Datenstruktur mit beschränktem Zugriff ist der Stapel. Es gibt nur zwei Grundoperationen beim Stapel: a) ein Element auf den Stapel legen, das Element wird oberstes Element (Operation push) b) das obenliegende Element vom Stapel nehmen (Operation pop) Bsp: Arithmetische Ausdrücke: (3+4)*3 push(3) push(4) push (pop() + pop()) push(3) push (pop() * pop()) Vorteil: 74 - sehr effizient und auf der Hardwareebene einfach zu programmieren - kann sich Zwischenergebnisse leicht merken - für eine kleine Anzahl von Operatoren braucht man nur einen kleinen Stapel. Der Stapel wird am einfachsten mit Hilfe von verketten Listen implementiert. 8.4. Strukturart: Schlange Wie der Stapel ist die Schlange eine Datenstruktur mit beschränktem Zugriff. Sie erlaubt nur die zwei Operationen: Element am Anfang in die Schlange einfügen Element vom Ende wieder entfernen. Die Strukturart Schlange wird über verkettete Listen implementiert. 8.5. Strukturart: Bäume Bisher hatten die Elemente einer rekursiven Datenstruktur jeweils nur maximal eine Nachfolgerkomponente. Bei der Strukturart Baum kann jedes Element auf mehrere andere Elemente, nicht jedoch auf sich selbst zeigen. (siehe das Beispiel bei den rekursiven Datenstrukturen) Ein Baum besteht aus einer Menge von Knoten und Kanten. In den Knoten des Baumes liegt die Information der jeweiligen Instanz. Die Kanten stellen die Verbindung zwischen zwei Knoten dar. Ein Pfad ist eine Liste von unterschiedlichen Knoten, die über Pfade verbunden sind. Der Knoten des Baumes, von dem alle Kanten ausgehen, heißt Wurzel. Knoten ohne Nachfolger werden Blätter genannt. Es liegt dann die Struktur eines Baumes vor, wenn es - zwischen der Wurzel und den Knoten genau einen Pfad gibt. Daraus folgt: - jeder Knoten hat genau einen Vorgänger. - kein Knoten zeigt auf sich selbst Die Anzahl der Kanten eines Baums definieren die Art des Baumes: - bei zwei Kanten spricht man von einem binären Baum, - bei n Kanten spricht man von einem n-ären Baum. 75 Bsp. Konstruktion: struct node { char info; struct node *left,*right; }; struct node *x,*z; 8.6. Beispiel Bäume #include <stdio.h> #define NUM_OF_STUDENTS 20 struct type_info_student { char name[20]; char adress[100]; int age; int id; }; struct type_info_student class[NUM_OF_STUDENTS]; void fill_struct(struct type_info_student *student); void print_struct(struct type_info_student student); void init_class(struct type_info_student class[]); void fill_class(struct type_info_student class[]); void print_class(struct type_info_student class[]); void save_class(char *filename); void load_class(char *filename); int check_student(char *name,char *adress,int age); int dichot_rek(struct type_info_student class[],int laenge,char *name); int dichot_lin(int left,int right,char *name); int acces_ID(int id,struct type_info_student *student); int print_id(void); main() { /* main_ex1(); */ main_ex2(); } /* Begin example 1 ***************/ main_ex1() { struct type_info_student student; puts(" Here is structure, exercise 1 "); fill_struct(&student); print_struct(student); } void fill_struct(struct type_info_student *student) { puts(" Fill structure student "); printf(" Enter name of the student >>>"); fflush(stdin); fgets(student->name,20,stdin); student->name[strlen(student->name) -1] = 0; /* Delete newline at the end */ printf(" Enter adress of the student >>>"); fflush(stdin); fgets(student->adress,100,stdin); student->adress[strlen(student->adress) -1] = 0; /* Delete newline at the end */ printf(" Enter age of the student >>>"); fflush(stdin); scanf("%d",&student->age); /* printf(" Enter ID of the student >>>"); scanf("%d",&student->id); */ } void print_struct(struct type_info_student student) { printf(" Name of the student is : %s\n",student.name); 76 } printf(" Adress of the student is : %s\n",student.adress); printf(" Age of the student is : %d\n",student.age); printf(" ID of the student is : %d\n",student.id); /* End /* Begin example 1 ***************/ example 2 ***************/ main_ex2() { char filename[30]; char ant; } puts(" Examples structures Number 2 "); init_class(class); fill_class(class); print_class(class); ask_for_check(); print_id(); puts(" Enter Filename for output "); scanf("%s",filename); save_class(filename); void init_class(struct type_info_student class[]) { int i; for (i=0; i< NUM_OF_STUDENTS; i++) class[i].id = 0; } void fill_class(struct type_info_student class[]) { struct type_info_student student; int i=0,index=0; int field_id = 0; char ant; char filename[20]; puts(" Do you want to read the Input from a file (y/n) "); scanf("%c",&ant); if (ant == 'y' || ant == 'Y') { puts(" Enter Filename for input "); scanf("%s",filename); load_class(filename); return; }; /* read from the terminal */ student.name[0] = 0; fill_struct(&student); while ((student.name[0]) && (field_id < NUM_OF_STUDENTS)) { while ((class[index].id) && (strcmp(class[index].name,student.name)<0) ) index++; printf(" For the new Student I found the index %d \n",index); student.id = index + 1; /* start to shift every class entry */ for (i=field_id;i > index; i--) { class[i+1] = class[i]; class[i+1].id ++; } field_id ++; class[index] = student; } index = 0; student.name[0] = 0; fill_struct(&student); 77 } void print_class(struct type_info_student class[]) { } int i=0; while ((class[i].id) && (i<NUM_OF_STUDENTS)) { printf(" \n\n ----> Student number = %d <-----\n",i); print_struct(class[i]); i++; } void save_class(char *filename) { FILE *out; int i; out = fopen(filename,"w"); i=0; while ((class[i].id) && (i<NUM_OF_STUDENTS)) { fprintf(out,"%s\n",class[i].name); fprintf(out,"%s\n",class[i].adress); fprintf(out,"%d\n",class[i].age); fprintf(out,"%d\n",class[i].id); i++; }; fclose(out); } void load_class(char *filename) { FILE *in; int i; in = fopen(filename,"r"); i=0; while (! feof(in) && (i<NUM_OF_STUDENTS)) { fgets(class[i].name,20,in); class[i].name[strlen(class[i].name) -1] = 0; /* Delete newline at the end */ fgets(class[i].adress,100,in); class[i].adress[strlen(class[i].adress) -1] = 0; /* Delete newline at the end */ fscanf(in,"%d\n",&class[i].age); fscanf(in,"%d\n",&class[i].id); i++; }; fclose(in); } int check_student(char *name,char *adress,int age) /* Check if a student is present in the class */ { int i=0; int laenge; while (class[i].id && (i<NUM_OF_STUDENTS) && strcmp(class[i].name,name) && strcmp(class[i].adress,adress) && class[i].age != age) i++; if (class[i].id && (i<NUM_OF_STUDENTS)) printf("Found id=%d\n",class[i].id); else printf("Not Found \n"); puts(" Now look with rekursion "); /* decide numbers od allready stored students */ for (laenge=0;class[laenge].id && (laenge < NUM_OF_STUDENTS);laenge++); if (i=dichot_rek(class,laenge,name)) printf("Found id=%d\n",i); else printf("Not Found \n"); 78 puts(" Now look iterative "); if (i=dichot_lin(0,laenge,name)) printf("Found id=%d\n",i); else printf("Not Found \n"); } ask_for_check() { struct type_info_student student; char ant; int id; puts(" Do you want to check a new student (y/n) "); fflush(stdin); scanf("%c",&ant); while (ant == 'y' || ant == 'Y') { fill_struct(&student); if (id=check_student(student.name,student.adress,student.age)) { printf(" Student found\n"); } else { printf(" Student not found \n"); }; puts(" Do you want to check a new student (y/n) "); fflush(stdin); scanf("%c",&ant); }; } int dichot_rek(struct type_info_student class[],int laenge,char *name) { int mitte; mitte = laenge/2; if (!strcmp(class[mitte].name,name)) return class[mitte].id; else if (laenge == 1) return 0; } if (strcmp(name,class[mitte].name) < 0) return dichot_rek(class,mitte,name); if (strcmp(name,class[mitte].name) > 0) return dichot_rek(&class[mitte],laenge-mitte,name); int dichot_lin(int left,int right,char *name) { int mitte; mitte = (left + right)/2; while ((left <= right ) && (strcmp(name,class[mitte].name)) ) { if (strcmp(name,class[mitte].name) < 0) right = mitte - 1; if (strcmp(name,class[mitte].name) > 0) left = mitte + 1; mitte = (left + right)/2 ; }; if (! strcmp(name,class[mitte].name) ) return class[mitte].id; else return 0; } int acces_ID(int id,struct type_info_student *student) { int i; for (i=0;class[i].id && class[i].id != id && (i < NUM_OF_STUDENTS) ;i++); if (class[i].id == id) 79 } { *student = class[i]; return 1; } else return 0; int print_id() { int i; struct type_info_student student; for (i=1;i < NUM_OF_STUDENTS;i++) { if (acces_ID(i,&student)) print_struct(student); }; } 80 9. Standard Ein/Ausgabe Im Sprachumfang von C sind keine Befehle für die E/A enthalten. In der standardisierten Run-Time Library werden jedoch eine große Zahl von E/A Funktionen zur Verfügung gestellt, unter denen der Programmierer die für sein E/A Problem geeignetste auswählen kann. Das der Run-Time Library zugrundeliegende Konzept entstammt der UNIX-Umgebung. Dort wird eine Datei als geordnete Folge oder "Strom" stream von Zeichen (Bytes) betrachtet. Die einzelnen Datensätze sind durch ein Zeilentrennzeichen ('\n') getrennt. Eine Unterscheidung zwischen Text- und Binärdateien existiert nicht. Auch ein physikalisches Gerät kann ein stream sein. 9.1. Überblick über die Standard Ein/Ausgabe 9.1.1. Die Datenstruktur stream Das der Run-Time Library zugrundeliegende Konzept entstammt der UNIX-Umgebung. Dort wird eine Datei als geordnete Folge oder "Strom" stream von zwei Bytes betrachtet. Die einzelnen Datensätze sind durch ein Zeilentrennzeichen ('\n') getrennt. Eine Unterscheidung zwischen Text- und Binärdateien existiert nicht. Auch ein physikalisches Gerät kann ein stream sein. 9.1.2. Die Datenstruktur File Jede Datei wird über seinen Dateinamen eindeutig identifiziert. Innerhalb eines Programms kann eine Datei nur als stream bearbeitet werden. Die Zuordnung eines stream zu einer Datei erfolgt beim Öffnen der Datei. Die in stdio.h definierte Datenstruktur FILE entspricht innerhalb eines C - Programms der Datenstruktur des stream. In der Datenstruktur FILE werden alle Informationen abgespeichert, die für den Zugriff auf die Datei notwendig sind Positionszeiger (File position indicator), Zeiger auf den mit der Datei verknüpften Puffer, Platz für das Registrieren des Fehlerstatus. Beispiel : FILE eingabe,ausgabe; /* Es werden 3 Filepointer auf streams definiert */ 9.1.3. Vordefinierte Dateien Drei bestimme Typen von Ein/Ausgabestreams sind bei jedem C-Programm geöffnet und in stdio.h vordefiniert. Die Ein/Ausgabestreams sind vom Typ FILE und ihre Variablennamen lauten : stdin, stdout, stderr. Sie sind die Standardkanäle für Eingabe, Ausgabe und Fehlermeldungen. Defaultmäßig zeigen alle drei Kanäle auf das Terminal. 81 9.1.4. Möglichkeiten der Organisation und des Zugriffs In Standard C werden streams sequentiell organisiert und als lückenloser stream von Zeichen (Bytes) sequentiell zeichenweise (byteweise) bearbeitet. Darüber hinausgehend stellt die Run-Time Library Funktionen zur Verfügung, die es erlauben, auf eine bestimmte Stelle des stream zu positionieren Der Zugriff auf einen stream kann ungepuffert (unbuffered) oder gepuffert ablaufen. Bei der Pufferung wird noch einmal unterschieden zwischen zeilenweiser Pufferung (linebuffered) und voller Pufferung (fully buffered). Bei ungepufferter E/A wird jedes Zeichen (Byte) getrennt verarbeitet, bei gepufferter E/A dagegen werden die Zeichen (Bytes) in einem Puffer so lange angesammelt, bis dieser gefüllt ist. Bei zeilenweiser Pufferung wird das Zwischenspeichern abgebrochen, bevor der Puffer vollständig gefüllt ist, oder wenn ein Zeilenende-Zeichen auftritt. Auf die Pufferung kann der Programmierer evtl. durch die Wahl der E/A- Funktionen Einfluß nehmen, innerhalb des Standards mit Hilfe der Funktion setvbuf. Das Ausmaß der möglichen Beeinflussung hängt vom Betriebssystem ab. Der Standard schlägt zwar diese drei Mechanismen vor, zwingt eine konkrete Implementierung aber lediglich, entsprechende Anweisungen zu akzeptieren, nicht jedoch, sie auch tatsächlich zu realisieren 9.1.5. Terminal-E/A Da stdin und stdout Pointennamen sind die auf streams zeigen, kann man auf sie prinzipiell mit allen Funktionen zur Streambearbeitung zugreifen, es sei denn, physikalische Merkmale verbieten dies (so ist etwa der wahlfreie Zugriff mit Hilfe von fseek oder lseek bei einem Terminal nicht möglich). Folgende Funktionen gibt es für die Terminal-E/A: Zeichen-E/A: String-E/A Formatierte E/A getc,getchar, putc,putchar gets,puts scanf,printf Häufig sind sie in stdio.h als Macros implementiert, mit denen sie auf andere E/A Funktionen für Dateiverarbeitung zurückgeführt werden, z.B.: #define getchar() #define putchar(x) fgetc(stdin) fputc(x,stdout) 82 9.1.6. Interne E/A Bei der internen E/A geht es nicht um Ein- oder Ausgabe von Daten, sondern um das formatierte "Herauslesen" von Daten aus einem String oder das formatierte "Hineinschreiben" beliebiger Daten in einen String. Dazu bietet die Standardlibray zwei E/A Funktionen, deren Namen sich von denen der Standard E/A Routinen nur durch den Anfangsbuchstaben 's' unterscheiden. int int sscanf (const char *str, const char *format_spec, ...) sprintf (char *str, const char *format_spec, ...) Die interne E/A wird hauptsächlich verwendet, um Strings in numerische Datentypen zu konvertieren und umgekehrt, außerdem, um einzelne Teile aus größeren Datensätzen herauszulösen. Für solche Typumwandlungen gibt es jedoch neben den 's' Funktionen meist spezielle Konvertierroutinen. Bsp.: 1 2 3 4 5 6 7 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 /************************************************************* * Das Programm liest beliebig viele reelle Zahlen ein und errechnet * das arithmetische Mittel. Als Abbruchkriterium dient die Eingabe * von "ENDE". **************************************************************/ #include #define INPUT main () { double char int printf ("Betrag: "); \ gets (string) zahl, summe = 0; string[20]; count = 0; INPUT; while ( strcmp(string,"ENDE") != 0 ) { sscanf (string,"%f",&zahl); summe += zahl; /* oder für Zeile 161 und 162 : summe += atof (string) */ count++; INPUT; } printf ("Mittelwert: %5.2f\n",summe/count); } 83 9.1.7. Standard-E/A Die Standard E/A umfaßt Funktionen, die die Daten im allgemeinen gepuffert bearbeiten. Die Art und Größe der Puffer kann mit speziellen Routinen beeinflußt werden. Für spezielle Anwendungen kann die Pufferung sogar ausgeschalten werden. 9.2. Allgemeine Dateioperationen 9.2.1. Öffnen und Schließen von Dateien Innerhalb eines Programms werden bei der Standard E/A alle Dateien über sogenannte Filepointer angesprochen. Die Funktion fopen ordnet einer externen Datei einen internen Filepointer zu : FILE *fopen (const char *file_spec, const char *a_mode) Die Funktion fopen ordnet einer externen Datei einen internen Filepointer zu : Die möglichen Argumente für die Zugriffsweise (a_mode) ergeben sich folgendermaßen: 1. Grundsätzlich gibt es die drei Möglichkeiten "r" (read), "w" (write) und "a" (append). Öffnet man eine Datei mit einem dieser Argumente, so ist ausschließlich der angegebene Zugriffsmodus möglich. 2. Will man zugleich lesend und schreibend auf eine Datei zugreifen, muß man dies durch Anfügen eines Pluszeichens bekanntmachen: "r+", "w+", "a+". Die Unterschiede zwischen diesen Argumenten zeigt die folgende Tabelle. 3. Die bisher aufgeführten Argumente sind beim Zugriff auf text streams zu verwenden. Beim Zugriff auf binary streams wird jeweils noch ein "b" angehängt. So entstehen "rb", "wb", "ab", "r+b", "w+b", "a+b" (oder "rb+", "wb+", "ab+"). Die Unterschiede zwischen diesen Zugriffsarten zeigt folgende Tabelle : Datei muß existieren Alter Inhalt verloren Lesen möglich Schreiben möglich Anfügen möglich r ja w a r+ ja ja ja ja ja ja ja ja w+ ja ja ja a+ ja ja ja Die bisher aufgeführten Argumente sind beim Zugriff auf text streams zu verwenden. Beim Zugriff auf binary streams wird jeweils noch ein "b" angehängt. So entstehen : "rb", "wb", "ab", "r+b", "w+b", "a+b" (oder "rb+", "wb+", "ab+"). 84 9.2.2. Schließen von Dateien Geöffnete Dateien werden geschlossen durch die Funktion int fclose (FILE *file_ptr) wobei file_ptr der file pointer ist, den fopen geliefert hat. Da fopen den Nullpointer als Ergebnis liefert, wenn ein Fehler aufgetreten ist, sehen die Anweisungen typischerweise so aus: Beispiel : FILE *fp; if ( (fp = fopen("datei_x.txt","r")) == NULL ) { printf ("Fehler beim Öffnen von DATEI_X.TXT\n"); exit (1); } fclose (fp); 9.2.3. Kombination von Öffnen und Schließen von Dateien Eine Kombination aus fopen und fclose stellt die Funktion freopen dar : FILE *freopen (const char *file_spec, const char *a_mode, FILE *file_ptr) dar. Sie verknüpft den file pointer mit der Datei file_spec und schließt die Datei, die evtl. vorher mit dem file pointer verknüpft war (z.B. zum Umlenken eines E/A-Kanals benutzt). 9.2.4. Anlegen von temporären Dateien Temporäre Dateien sind Arbeitsdateien, die zum Speichern von Zwischenergebnissen verwendet werden und nach dem Programmende wieder gelöscht werden. Die Funktion tmpfile kreiert eine temporäre Datei, die im binary update Mode geöffnet wird, da man davon ausgeht, daß auf die Datei zuerst geschrieben und dann von ihr gelesen wird. Einen Pointer auf einen solchen temorären FILE stream erhält man mit der Funktion tmpfile : FILE *tmpfile (void) Reicht dem Benutzer der binary update Mode einer temporären Datei nicht aus, kann er sich mit der Funktion tmpnam einen Filenamen generieren lassen, der mit keinem existierenden Filenamen übereinstimmt und eine Datei unter diesem Namen mit der Funktion fopen im gewünschten Modus öffnen. Damit die Datei jedoch nach dem Programmende nicht permanent gespeichert bleibt, muß der Programmierer die Datei explizit löschen. Dazu hat er die Funktion remove. char *tmpnam (char *str) Die Funktion tmpnam generiert einen Filenamen, der in str abgelegt wird. Der String str muß mindestens die in stdio.h definierte Länge L_tmpnam haben. 85 Beispiel : FILE char *my_tmp_file,*fp; my_file_name[132]; my_tmp_file = tmpfile(); /* oeffnet temporaere Datei */ tmpnam(my_tmp_file_name); /* generiert neuen Filenamen */ fp = fopen(my_tmp_file_name,"r")) == NULL /* oeffnet Datei */ fclose (fp); 9.2.5. Dateiverwaltung Mit Hilfe der Funktion int remove (const char *file_spec) kann eine Datei vom Programm aus gelöscht werden. Wie sich die Funktion verhält, wenn die angegebene Datei geöffnet ist, legt der Standard nicht fest. Zum Umbenennen einer Datei dient die Funktion rename. int rename (const char *old_file_spec, const char *new_file_spec) Zu beachten ist, daß beide Funktionen nicht mit dem file pointer arbeiten, sondern mit dem Dateinamen. Das nachfolgende Beispiel zeigt wie man mit tmpnam und remove arbeiten kann. Beispiel : #include <stdio.h> char filnam[L_tmpnam]; FILE *file; tmpnam(filnam); file=fopen(filnam,"w+"); fclose(file); remove(filnam); 86 9.3. Text-E/A Der Standard beschreibt Funktionen, mit denen Textdateien entweder zeichenweise oder in String-Einheiten bearbeitet werden können. Bedingt sind für diesen Zweck auch noch andere Funktionen geeignet 9.3.1. Zeichenorientierte E/A Für die zeichenweise Bearbeitung von Textdateien stehen zwei Funktionen zur Verfügung: int fgetc (FILE *file_ptr) int fputc (int c, FILE *file_ptr) Als Alternative für die beiden Funktionen gibt es zwei Macros, die im allgemeinen schneller als die Funktionen arbeiten : int getc (FILE *file_ptr) int putc (int c, FILE *file_ptr) Zu beachten ist, daß das bearbeitete Zeichen vom Typ int ist. Dies ist notwendig, weil fgetc beim Erreichen des Dateiendes den als char nicht darstellbaren Wert EOF (definiert in stdio.h) liefert. Auch fputc liefert als Ergebnis den Wert EOF, wenn ein Fehler aufgetreten ist. 9.3.2. Stringorientierte E/A auf Datei Ganze Strings können mit den Funktionen char int *fgets (char *str, int maxchar, FILE *file_ptr) fputs (const char *str, FILE *file_ptr) eingelesen und ausgegeben werden. Die Funktion fgets versucht alle Zeichen bis zum nächsten ZeilenendeZeichen in die Stringvariable str zu übertragen, bricht jedoch ab, falls sie dieses nach maxchar-1 übertragenen Zeichen noch nicht erreicht hat. An das eingelesene Zeichenkette hängt fgets automatisch den stringterminierenden Nullcharacter an und wandelt diese Zeichenkette somit in einen string. Da der Ergebnistyp ein Zeiger auf char ist, kann fgets im Gegensatz zu fgetc beim Erreichen des Dateiendes nicht EOF liefern. In diesem Fall gibt fgets vielmehr den Nullpointer zurück, 87 9.3.3. Stringorientierte E/A auf das Terminal Ganze Strings können mit den Funktionen char int *gets (char *str) puts (const char *str) vom Terminal eingelesen und ausgegeben werden. Die Funktion gets liest vom Terminal bis zu einem EOF Buchstaben oder zum nächsten Zeilenende-Zeichen in die Stringvariable str. Das Zeilenende-Zeichen wird nicht in den String übernommen, jedoch der stringterminierende Nullcharacter an den String angehängt. Der Vorteil bei der Benutzung der Funktion gets liegt daran, daß die Funktion nicht mehr Buchstaben vom Terminal liest, als der String inklusiv dem Terminierungsbuchstaben speichern kann. Achtung : Die Funktion puts schreibt den String str auf das Terminal und hängt automatisch einen newline Buchstaben an. 9.3.4. Löschen von Zwischenbuffern bei der Eingabe Beim Lesen von Strings kommt es öfters vor,daß Zeichen noch auf dem Zwischenbuffer gespeichert sind, da sie in keine Variablen übertragen wurden. Damit diese Zeichen vor den erneuten Lesen gelöscht werden gibt es die Funktion fflush : int fflush(FILE *stream) Sie löscht eventuell gepufferte oder noch nicht geschriebene Daten werden vom Eingabestrom. 9.3.5. Formatierte String - E/A Von formatierter String - E/A spricht man, wenn ein String eingelesen, aber nicht als solcher abgelegt, sondern vorher in eine andere Darstellungsform (z.B. binäre int-Darstellung) umgewandelt wird. Entsprechend werden bei der formatierten Ausgabe binär abgespeicherte Daten nicht als solche ausgegeben, sondern zuvor in Strings umgewandelt. Die C Run-Time Library stellt dafür zwei Funktionen bereit: int int fscanf (FILE *file_ptr, const char *format_spec, ...) fprintf (FILE *file_ptr, const char *format_spec, ...) Die Funktion fscanf erwartet, wenn mehrere Elemente bei einem Aufruf einzulesen sind, Trennzeichen zwischen diesen. Was Trennzeichen sein können, legt das Macro isspace fest : int isspace (char character) Da das Leerzeichen ebenfalls ein Trennzeichen ist, sollte man Strings nicht mit fscanf einlesen, wenn man nicht sicher ist, daß diese keine Leerzeichen enthalten, bzw. man ihre Länge nicht vorhersagen kann. Für Strings eignen sich die Funktionen fgets und fputs oder gets und fputs. Die Funktion fscanf liefert den Wert EOF, falls sie auf das Dateiende stößt. In allen anderen Fällen liefert sie die Anzahl der Argumente, denen beim Einlesen Werte zugewiesen wurden. 88 9.4. Allgemeine Formatierte Ausgabe Zur formatierten Ausgabe von Variablen gibt es die zwei Funktionen fprintf und printf. Sie unterscheiden sich dadurch, daß mit fprintf auf jede beliebige Datei geschrieben werden kann, printf nur auf stdout schreibt. int fprintf(FILE *datei_stream, char *format, ...variablen...); int printf(char *format, ...variablen......); Die Funktionen wandeln die variablen in einen Ausgabestring unter der Kontrolle von format um und schreiben den string auf datei_stream bzw. auf stdout. Als Resultat liefern die Funktionen die Anzahl der geschriebenen Zeichen; es ist negativ, wenn ein Fehler passiert ist. Die Format-Zeichenkette enthält zwei Arten von Objekten: gewöhnliche Zeichen, die in die Ausgabe kopiert werden, und Umwandlungsangaben, die jeweils die Umwandlung und Ausgabe des nächstfolgenden Arguments von fprintf veranlassen. Jede Umwandlungsangabe beginnt mit dem Zeichen % und endet mit einem Umwandlungszeichen. Beispiel : printf("Die Ausgabe ist %d",erg); printf(" Die Ergebnisse sind a= %d und b = %f\n",a,b); fprintf(datei,"Das Ergebnis ist %f\n",x); 89 9.4.1. Formatanweisungen für die Ausgabe Es gibt folgende Möglichkeiten eine Variable in einen Ausgabestring zu konvertieren : Zeichen Eingabedaten; Argumenttyp d,i int; dezimal mit Vorzeichen. o, int; oktal ohne Vorzeichen. x, X int; hexadezimal ohne Vorzeichen u int; dezimal ohne Vorzeichen c int; einzelnes Zeichen, nach Umwandlung in unsigned char. s char *; aus der Zeichenkette werden Zeichen ausgegeben bis vor '\0' oder so viele Zeichen, wie die Genauigkeit verlangt. f double; dezimal als [-]mmm.ddd, wobei die Genauigkeit die Anzahl der d festlegt. Voreinstellung ist 6; bei 0 entfällt der Dezimalpunkt. e,E double; dezimal als [-]m.dddddde±xx oder [-]m.ddddddE±xx, wobei die Genauigkeit die Anzahl der d festlegt. Voreinstellung ist 6; bei 0 entfällt der Dezimalpunkt. g,G double; %e oder %E wird verwendet, wenn der Exponent kleiner als -4 oder nicht kleiner als die Genauigkeitist; sonst wird %f benutzt. Null und Dezimalpunkt am Schluß werden nicht ausgegeben. p void *; als Zeiger (Darstellung hängt von Implementierung ab). n int *; die Anzahl der bisher von diesem Aufruf von printf ausgegebenene Zeichen wird im Argument abgelegt. Ein Argument wird nicht umgewandelt. % kein Argument wird umgewandelt; ein % wird ausgegeben. 90 9.4.2. spezielle escape Character In Ausgabestrings können sogenannte escape Character eingestreut werden um die Ausgabe zu formatieren oder spezielle Vorschub oder Notationszeichen auszugeben. Es gibt folgende escape Character : Name newline horizontal tab vertical tab backspace carriage return formfeed bell backslash question mark single quote double quote octal number hex number Kürzel Notation in C \n \t \v \b \r \f \a \\ (alt shift 7 ) \? \' (alt ´ ) \" \ooo \xhh NL HT VT BS CR FF BEL \ ? ' " ooo hh Beispiel : printf("\a\a Achtung ! Fehler "); */ 9.5. /* gibt zwei Bell-Töne vor dem Text aus Allgemeine Formatierte Eingabe Zur formatierten Eingabe von Variablen gibt es die zwei Funktionen fscanf und scanf. Sie unterscheiden sich dadurch, daß mit fscanf von jeder Textdatei lesen kann, scanf nur von stdin liest. int fscanf(FILE *stream, char *format, &variablen,...); int scanf(char *format, &variablen,...); liest vom stream unter Formatierungskontrolle von format die Variablen ein und speichert die konvertierten Werte unter den nachfolgenden Argumenten ab, die alle Zeiger sein müssen. Die Funktion wird beendet, wenn format abgearbeitet ist. fscanf liefert EOF, wenn vor der ersten Umwandlung das Dateiende erreicht wird oder ein Fehler passiert. Andernfalls liefert die Funktion die Anzahl der umgewandelten und abgelegten Eingaben. Die Format-Zeichenkette enthält normalerweise Umwandlungsangaben, die zur Interpretation der Eingabe verwendet werden. Die Format-Zeichenkette kann folgendes enthalten: - Leerzeichen oder Tabulatorzeichen, die ignoriert werden. - gewöhnliche Zeichen (nicht aber %), die dem nächsten Zeichen nach Zwischenraum im Eingabestrom entsprechen müssen. - Umwandlungsangaben, bestehend aus %, einem optionalen Zeichen *, das die Zuweisung an ein Argument verhindert, - einer optionalen Zahl, die die maximale Feldbreite festlegt, - einem optionalen Buchstaben h, l oder L, der die Länge des Ziels beschreibt; - einem Umwandlungszeichen. Das Umwandlungszeichen gibt die Interpretation des Eingabefelds an. Das zugehörige Argument muß ein Zeiger sein. Die erlaubten Umwandlungszeichen zeigt die nächste Tabelle. Den Umwandlungszeichen d, i, n, o, u und x kann h vorausgehen, wenn das Argument ein Zeiger auf short statt int ist, oder der Buchstabe l, wenn das Argument ein Zeiger auf long ist. Vor den Umwandlungszeichen e, f und g kann der Buchstabe l stehen, wenn ein Zeiger auf double und nicht auf float in der Argumentliste steht, und L, wenn es sich um einen Zeiger auf long double handelt. 91 9.5.1. Formatanweisungen für die Eingabe Buchstabe Eingabe Daten, Argument Typ d dezimal, ganzzahlig; int *. i ganzzahlig; int *. Der Wert kann oktal (mit 0 am Anfang) oder hexadezimal (mit 0x oder 0X am Anfang) angegeben sein. o oktal ganzzahlig (mit oder ohne 0 am Anfang); int *. u dezimal ohne Vorzeichen; unsigned int *. x hexadezimal ganzzahlig (mit oder ohne 0x oder 0X am Anfang); int *. c ein oder mehrere Zeichen; char *. Die nachfolgenden Eingabezeichen werden im angegebenen Vektor abgelegt, bis die Feldbreite erreicht ist;Voreinstellung ist 1. '\0' wird nicht angefügt. In diesem Fall wird Zwischenraum nicht überlesen; das nächste Zeichen nach Zwischenraum liest man mit %1s. s Folge von Nicht-Zwischenraum-Zeichen (ohne Anführungszeichen); char *,der auf einen Vektor zeigen muß, der die Zeichenkette und das abschließende '\0' aufnehmen kann, das dazukommt. e,f,g Gleitpunkt; float *. Das Eingabeformat erlaubt für float ein optionales Vorzeichen, eine Ziffernfolge, die auch einen Dezimalpunkt enthalten kann, und einen optionalen Exponenten, bestehend aus E oder e und einer ganzen Zahl, optional mit Vorzeichen. Zeiger, wie ihn printf("%p") ausgibt; void *. p n legt im Argument die Anzahl Zeichen ab, die bei diesem Aufruf bisher gelesen wurden; int *.Vom Eingabestrom wird nicht gelesen, die Zählung der Umwandlungen bleibt unbeeiflußt. erkennt die längste nicht-leere Zeichenkette aus den Eingabezeichen in der angegebenen Klasse; char *. Dazu kommt '\0' . Die Klasse []...] enthält auch ]. [...] erkennt die längste nicht-leere Zeichenkette aus den Eingabezeichen nicht in der angegebenen Klasse; char *. Dazu kommt '\0' . Die Klasse [^]...] enthält auch ]. [^...] erkennt %; eine Zuweisung findet nicht statt. % 92 9.6. Blockorientierte E/A Bei der blockorientierten E/A wird eine vom Programmierer festgelegte Anzahl von Bytes ohne Rücksicht auf deren Inhalt übertragen. Sie eignet sich einerseits für das Bearbeiten von Binärdateien, die aus Datensätzen eines struct-Typs aufgebaut sind, andererseits für das bloße übertragen großer Datenblöcke, bei dem der Inhalt keine Rolle spielt. Für diese Art der E/A benötigt man die Funktionen size_t size_t fread (void *ptr, size_t size_of_item, size_t number_items, FILE *file_ptr) fwrite(void *ptr, size_t size_of_item, size_t number_items, FILE *file_ptr) Das Argument ptr ist ein Zeiger auf einen Bereich im Arbeitsspeicher (z.B. ein Array), in den das Gelesene kopiert bzw. aus dem das zu Schreibende geholt werden soll. Die Größe dieses Bereichs (in Byte) gibt das zweite Argument an. Bei Dateien, die aus Komponenten eines struct-Typs bestehen, arbeitet man hier sinnvollerweise mit dem sizeof-Operator. Das dritte Argument legt fest, wieviele Elemente zu übertragen sind. Der file pointer schließlich gibt Auskunft darüber, mit welcher Datei gearbeitet werden soll. Beim Erreichen des Dateiendes liefert fread den Wert 0, in allen anderen Fällen liefert fread als Ergebnis die Anzahl der erfolgreich gelesenen Elemente. (nicht EOF!). 9.7. Positionierung und wahlfreier Zugriff Wie bereits erwähnt, kennt Standard-C nur stream-Dateien, d.h. Dateien, in denen lückenlos aufeinanderfolgende Zeichen abgelegt sind. Auf der Grundlage dieses Konzepts lassen sich Relativdateien im eigentlichen Sinne nicht realisieren. Geboten ist lediglich die Möglichkeit, in einem stream frei zu positionieren. Nachdem man in einer Datei an eine bestimmte Stelle positioniert hat, kann je nach erlaubtem Dateizugriff ab dieser Stelle gelesen oder geschrieben werden. Dies geschieht mit Hilfe der Funktionen fseek, fgetpos oder fsetpos. int fgetpos(FILE *stream, fpos_t *ptr) fgetpos speichert die aktuelle Position für stream bei *ptr. Der Wert kann später mit fsetpos verwendet werden. Der Datentyp fpos_t eignet sich zum Speichern von solchen Werten. Bei Fehler liefert fgetpos einen von Null verschiedenen Wert. int fsetpos(FILE *, fpos_t *) positioniert den Stream int fseek(FILE *stream, long offset, int origin) Außer dem file pointer benötigt die Funktion einen offset, d.h. einen Abstand in Byte und eine Startposition, von wo aus der offset aufaddiert werden soll, um an die spezielle Stelle zu positionieren. Der Abstand ist bei Datensätzen eines struct-Typs wiederum am einfachsten mit Hilfe des sizeof-Operators anzugeben. 93 Für die Startposition zur Positionierung gibt es drei Möglichkeiten. Argumentwerte 0 oder SEEK_SET 1 oder SEEK_CUR 2 oder SEEK_SET Position Dateianfang die aktuelle Position Dateiende Die drei symbolischen Konstanten SEEK_SET, SEEK_CUR und SEEK_END werden in stdio.h definiert. Der offset wird stets zur Startposition hinzuaddiert, es ist aber auch möglich, negative Werte anzugeben. Beispiele : 1 : Positionieren auf den Anfang einer Datei : fseek(fp,0,SEEK_SET) einfacher ist dies mit der Funktion rewind(fp) erreichbar. 2. Positionieren auf den fünften Datensatz einer Binärdatei : offset = 5 * sizeof (struct xyz); fseek (fp, offset, SEEK_SET); 3. Bestimmen der Dateilänge mit der Funktion ftell). fseek(fp,0,SEEK_END) length_in_bytes=ftell(fp) 4. Merken einer bestimmten Stelle und zurückspringen an diese Position. cur_pos=ftell(fp); /* /* Bestimmen der Fileposition */ beliebige Fileoperationen, wie fread, fwrite, fgets, fputs ... */ fseek(fp,cur_pos,SEEK_SET) /* Zurückspringen an diese Stelle */ 5. Positionieren hinter das Dateiende bei der Bearbeitung von Relativdateien fseek(fp,20,SEEK_END) Folgt auf einen solchen Aufruf eine Schreibanweisung (z.B fwrite), so wird der neue Datensatz 20 Byte hinter dem bisherigen Dateiende abgelegt. Die dabei entstehende Lücke wird mit Null-Bytes gefüllt. Dieses Verhalten wird häufig ausgenutzt, um Relativdateien zu simulieren. 9.8. Anzahl der Bytes einer Datei 94 Nur bei Dateien, die als binary stream geöffnet sind, liefert die Funktion ftell die Anzahl der Bytes vom Dateianfang bis zur jetzigen Stelle. int ftell (FILE *fp) Bei Dateien, die als text stream geöffnet sind, liefert die Funktion einen implementationsabhängigen Wert, der nur dann die richtige Bedeutung hat, wenn offset den Wert 0 hat oder offset durch einen vorhergehenden Aufruf von ftell ermittelt wurde und startposition den Wert SEEK_SET hat. 9.9. Fehler Behandlung bei E/A Operationen Es gibt eine Reihe von Fehlerfunktionen, die den Zustand eines streams abfragen lassen : 9.9.1. Feststellen und Analysieren eines E/A Fehlers Wie in früheren Abschnitten gezeigt wurde, wird jeder File, der in einem Programm bearbeitet wird, einem internen Filestream zugeordnet. Dieser Filestream entspricht einem Record vom Typen FILE und dient als Organisationseinheit zur Bearbeitung der Datei. Im nachfolgenden werden Auszüge aus dem Record von Typen FILE gezeigt : typedef#struct { ............... unsigned#std #: 1; unsigned#binary #: 1; unsigned#eof #: 1; unsigned#err #: 1; unsigned#dirty #: 1; ............... } FILE Die zwei wichtigsten Recordkomponenten der Fileorganisationseinheit sind eof, der sogenannte "end-of-file indicator" und die Komponente err, der "error indicator". Stößt eine Leseoperation auf das Fileende, wird der "end-of-file indicator" gesetzt, bei sonstigen Fehlern wird der ""error indicator" gesetzt. Bei erfolgreicher Funktionsausführung werden die Werte dieser Komponenten nicht verändert und die Lese-Routinen liefern ihre erwarteten Resultate. Die Lese-Routinen fgetc, getw, gets liefern als Rückgabewert den gelesenen Buchstaben, das gelesene Wort oder den eingelesenen String. Die Funktion fscanf liefert die Anzahl der Variablen, denen beim Einlesen ein Wert zugewiesen wurde.Die Funktionen fread und read haben die Anzahl der eingelesenen Bytes als Rückgabewert. In der folgenden Tabelle werden die Rückgabewerte der E/A Funktionen gezeigt: 95 Funktion fgetc getw fgets fscanf fread read Erfolgreich gelesener Character gelesenes Wort gelesener String Anzahl der eingelesenen Variablen n (Bytes) n (Bytes) Dateiende EOF EOF NULL EOF andere Fehler EOF EOF NULL EOF < n (Bytes) 0 < n (Bytes) -1 Bei den Routinen fread und read bezieht sich der Wert der Variablen n auf die Anzahl der einzulesenden Bytes. Um zu testen, ob eine Leseoperation an das Fileende gestoßen ist,muß der "end-of-file" Indikator getestet werden. Dazu hat man folgende Funktion : int feof (FILE *file_ptr) Sie liefert in Abhängigkeit von der E/A Funktion den in der obigen Tabelle aufgeführten Wert. Ist beim Lesen oder Schreiben irgendein anderer Fehler aufgetreten, wird der "error-indicator" gestetzt und die Art des Fehlers wird in der externen Variable errno dokumentiert. Die externe Variable errno, wird nach dem Standard im header file stddef.h definiert. Die möglichen Werte, die in errno abgespeichert werden, sind implementierungsabhängig. Zum Testen ob der "error-indicator" gestetzt wurde hat man folgende Funktion : int ferror (FILE *file_ptr) Will man die Art des Fehlers ausgeben, muß der Wert von errno interpretiert werden, was durch folgende Funktion realisiert wird : int perror (const char *str) Die Funktion greift auf die Variable errno zu und druckt auf dem Terminal die zugehörige Fehlermeldung aus. Das Argument von perror, ein String, wird automatisch vor der Fehlermeldung auf das Terminal geschrieben. Beispiel : if ( (fp_source = fopen ("test.dat","r")) == FOPEN_ERR ) perror ("fopen_error/test.dat"); Bei Nichtvorhandensein der Datei test.dat wird folgende Meldung ausgegeben: fopen_error/zahlen.dat: no such file or directory ausgegeben wird. 96 9.9.2. Beseitigen des E/A Fehlerstatus Ist ein Fehler aufgetreten, so bleibt der Fehlerstatus im Organisationsblock des File-streams erhalten, bis eines der folgenden Ereignisse eintritt: Das Programm wird beendet. Die Datei wird neu geöffnet. Die Funktion rewind wird aufgerufen Die Funktion clearerr wird aufgerufen. Dei Funktion clearerr funktioniert folgendermaßen : void clearerr (FILE *file_ptr) Durch sie wird sowohl der "end-of-file indicator"- als auch der "error indicator" beseitigt (die entsprechenden Speicherinhaltewerden auf 0 gesetzt, so daß das Programm weiter ablaufen kann. 9.10. UNIX-E/A Die in diesem Abschnitt vorgestellten Funktionen sind UNIX-spezifische Routinen. Sie sind nicht in den ANSI Standard aufgenommen worden, aber dennoch in vielen Run-Time Libraries enthalten. Innerhalb eines Programms werden bei UNIX E/A Dateistreams im Gegensatz zum Standard E/A nicht mit einem file pointer vom Typen FILE angesprochen, sondern mit einem file descriptor vom Typen int. Außerdem arbeiten die UNIX E/A Funktionen ungepuffert. 9.10.1. Öffnen und Schließen einer Datei : Die Zuordnung des Filenamens zu einem file descriptor realisiert die Funktion int open (char *file_spec, int flags, unsigned int mode ) Das Argument flags hat die gleiche Aufgabe wie die Angabe des Modus bei fopen. Die möglichen Werte lauten: flags r w a r+ w+ a+ Beschreibung O_RDONLY O_WRONLY | O_TRUNC | O_CREAT O_WRONLY | O_APPEND | O_CREAT O_RDWR O_RDWR | O_TRUNC | O_CREAT O_RDWR | O_APPEND | O_CREAT Aus der Tabelle läßt sich erkennen, daß die einzelnen flags durch bitweises ODER ( | ) kombiniert werden können. Der gleiche Mechanismus wird (allerdings mit Oktalzahlen, nicht mit symbolischen Konstanten) beim dritten Argument angewandt, um den Schutz (protection) einer neu geschaffenen Datei zu spezifizieren. Der folgende Programmausschnitt soll dies verdeutlichen: 97 #include #include main () { int unsigned int fdesc, flags; mode; flags = O_WRONLY | O_TRUNC | O_CREAT; mode = | | | | 0004 0040 0100 0200 0400 /* /* /* /* /* WORLD: GROUP: OWNER: OWNER: OWNER: READ */ READ */ EXECUTE */ WRITE */ READ */; if ( (open ("bsp.c",flags,mode)) == -1 ) { printf ("Datei BSP.C konnte nicht geöffnet werden!); exit (1); } Kann die Datei nicht geöffnet werden, so liefert open den Wert -1 als Ergebnis (im Unterschied zur Funktion fopen, die in diesem Fall 0 zurückgibt!). 9.10.2. Schließen einer Datei Eine geöffnete Datei wird geschlossen durch die Funktion int close (int file_desc ) 9.10.3. Lesen und Schreiben Unter den UNIX-Routinen gibt es nur zwei E/A-Funktionen: int read (int file_desc, void *buffer, size_t nbytes)) int write (int file_desc, void *buffer, size_t nbytes)) Diese beiden Funktionen sind vergleichbar mit fread und fwrite, insofern auch sie nur Blöcke von Bytes übertragen, ohne sich um deren Inhalt zu kümmern (allerdings kann dies unter Nicht-UNIX- Systemen bei text streams anders sein!). Beide Funktionen liefern den Wert -1, wenn ein Fehler aufgetreten ist, read den Wert 0, wenn das Dateiende erreicht wurde. Bei erfolgreicher Lese- bzw. Schreiboperation liefern die Funktionen die Anzahl der tatsächlich übertragenen Bytes, (siehe file_result) 98 9.10.4. Positionierung in der Datei int lseek (int file_desc, int offset, int direction)) Diese Funktion verhält sich wie fseek, ermöglicht also ebenfalls die Positionierung über das Dateiende hinaus und somit die Simulation von Relativdateien. 9.11. Untersuchung des Laufzeitverhaltens bei E/A Funktionen 9.11.1. Bearbeitung von Textdateien Zur Bearbeitung von Textdateien bieten sich folgende Funktionen an : int fread (void *ptr, size_t size, size_t numobj,FILE *stream) int fwrite (void *ptr, size_t size, size_t numobj,FILE *stream) char *fgets (char *s, int n, FILE *stream) int fputs (const char *s, int n, FILE *stream) int fscanf (FILE *stream, const char *format, ... ) int fprintf (FILE *stream, const char *format, ... ) int getw (FILE *stream) int putw (int word, FILE *stream) int getc (FILE *stream) int putc (char c, FILE *stream) In Tests zeigt sich folgendes Zeitverhalten : fread/fwrite ist am schnellsten. fgets/fputs sind schneller als fscanf/fprintf fscanf/fprintf sind schneller als getw/putw getw/putw und fgetc/fputc, getc/putc sind schneller als fgetc/fputc Daß das Bearbeiten einer Datei mit fgets/fputs schneller ist als das mit getw/putw, und dieses wiederum schneller als das mit fgetc/fputc, ist zu vermuten, weil die Verkleinerung des bearbeiteten Blocks zu mehr Aufrufen und damit zu längerer Laufzeit führt. Da die Funktionen fgetc/fputc intern im allgemeinen auf die beiden Macros getc/putc zurückgeführt werden, sind getc/putc schneller als fgetc/fputc. Da fread/fwrite Blocks fester Größe kopieren und sich nicht um Stringgrenzen kümmern, arbeiten diese Funktionen noch schneller als fgets/fputs. Bis auf fscanf/fprintf zeigen alle Funktionen das erwartete Zeitverhalten. Allerdings ist sowohl zu fread/fwrite als auch zu fscanf/fprintf eine Einschränkung notwendig. Die Funktionen fread und fwrite eignen sich nur bedingt für das Bearbeiten von Textdateien, da ihre eigentliche Aufgabe darin besteht, Blocks fester Größe zu bearbeiten, sie also nicht auf String-bzw. Recordgrenzen achten müssen. Ähnliches gilt für fscanf. Hier ist die Formatangabe "%s" sehr gefährlich, weil sie veranlaßt, daß bis zum nächsten Begrenzer white space gelesen wird. Wenn nun der String Leerzeichen enthält, liest fscanf jeweils nur bis zum nächsten Leerzeichen, was dazu führt, daß der String in mehreren Teilen eingelesen wird und die Laufzeit sich verschlechtert. Enthält andererseits die ganze Datei keinen einzigen Begrenzer, kann es geschehen, daß fscanf die ganze Datei als einen String zu behandeln versucht. Selbst wenn keiner dieser Fälle auftritt, erhält man aber das unerwünschte Resultat, daß die Kopie kürzer ist als das Original, weil der Begrenzer nicht in die Kopie übertragen wird. Der einzige Ausweg wäre, Blocks fester Größe einzulesen (z.B. mit "%80s"), doch geht damit wiederum die String-Struktur verloren. Die Funktion fscanf sollte also nur beim Einlesen von Datensätzen fester Länge und gleicher Struktur benutzt werden. 9.11.2. Bearbeiten von Binärdateien 99 Beim Lesen und Beschreiben von Binärdateien bieten sich folgende Schreib und Lesefunktionen an : gepufferte Standard-E/A, ungepufferte Standard-E/A, UNIX-E/A Gepufferte Standard-E/A Folgende Programmzeilen öffnen, lesen und beschreiben eine Binärdatei gepuffert : fp_source = fopen("unform.dat","rb"); fp_copy = fopen("kopie.dat","wb"); ..... while ((status = fread (memory,SIZE,REC_IN_BLOCK,fp_source)) != NULL ) fwrite (memory,SIZE,REC_IN_BLOCK,fp_copy); Ungepufferte Standard-E/A Folgende Programmzeilen öffnen, lesen und beschreiben eine Binärdatei ungepuffert : fp_source=fopen("unform.dat","rb"); fp_copy=fopen("kopie.dat","wb"); ..... setvbuf(fp_copy,0,_IONBF,0); while ((status = fread (memory,SIZE,REC_IN_BLOCK,fp_source)) != NULL ) fwrite (memory,SIZE,REC_IN_BLOCK,fp_copy); UNIX E/A Funktionen : fp_source=fopen("unform.dat","O_RDONLY,0"); fp_copy=creat("kopie.dat",0); ..... while ( (status = read (fp_source,memory,SIZE)) != NULL ) write (fp_copy,memory,SIZE); 100 Bemerkungen: Unter UNIX läuft die gepufferte Standard-E/A sehr schnell, die ungepufferte dagegen außerordentlich langsam. Auch die UNIX-E/A ist unter UNIX unerwartet schlecht. Der Grund für dieses Verhalten könnte darin zu suchen sein, daß immer nur ein Datensatz übertragen wird, der noch dazu verhältnismäßig kurz ist. Bei der Bearbeitung von Binärdateien mit Standard E/A und UNIX E/A kann die Anzahl der Bytes, die mit einem Schreib- oder Lesevorgang bearbeitet einen großen Einfluß auf die Dauer der E/A Vorgänge haben. Unter UNIX verbessern sich gepufferte und ungepufferte Standard-E/A, wenn die Buffer vergrößert werden. Die UNIX-E/A arbeitet bei größen Buffern am schnellsten, bei kleineren Buffern ist die gepufferte Standard-E/A schneller. Faustregel: Es ist besser, wenige große Blöcke zu übertragen als viele kleine. Das heißt: Je größer die Blöcke, desto kleiner die Laufzeit. Hat man aber große Blöcke zu übertragen, dann ist UNIX-E/A schneller als Standard-E/A. Ungepufferte Standard-E/A ist annähernd so schnell wie UNIX-E/A. Der Grund liegt darin, daß größere Blöcke zu weniger Funktionsaufrufen führen und damit zu geringerer Laufzeit. Das ist besonders effizient an Maschinen, die Befehle für blockweises Verschieben kennen. Bei großen Datenblöcken ist aber Pufferung nicht mehr notwendig, kann sogar die Performance verschlechtern. 9.12. Aufgabe Erzeugen Sie mit dem Editor eine beliebige Textdatei und schreiben Sie ein Programm, das eine solche Datei (zeilenweise) rückwärts auf den Bildschirm ausgibt. 101 10. Die Standard Library Die ANSI Kommision hat eine Reihe von nützlichen Bibliotheksfunktionen definiert, die der Benutzer bei ANSI C kompatiblen Kompilern benutzen kann. Diese Funktionen sind rechnerunabhängig und für jeden ANSI-C kompatiblen Kompiler genau festgelegt. Hält sich der Programmierer an die Definition der Funktionen, wird sein Programm ohne Schwierigkieten auf andere Rechner transportiert werden können. Die Funktionen sind in einer RUN-TIME-Library (RTL) gespeichert. Jede Funktion ist mit ihrem Header oder als MACRO in einem bestimmten Includefile definiert. Funktionen, die als MACRO definiert sind werden vom Praeprocessor zur Übersetzungszeit dem Programm hinzugefügt, Funktionen aus der RTL werden zur Linkzeit ins Programm gebunden. 10.1. Die Includefiles der ANSI Biblotheksfunktionen Diagnose : Character Handling : Testen eines Buchstabens Verändern eines Buchstabens <assert.h> <ctype.h> Fehler : <errno.h> Parameterwerte, die die Eigenschaften der <floats.h> reellen Zahlen definieren Parameterwerte, die die <limits.h> Programmierumgebung definieren Setzen von lokalen Variablen : <locale.h> Mathematische Funktionen : <math.h> Umgehen des normalen Aufrufs und <setjmp.h> Rückkehrmechanismus von Funktionen : Signal Handling : <signal.h> Standard Definitionen : <stddef.h> Ein/Ausgabe <stdio.h> Generelle Utilities : <stdlib.h> Speicher und Stringverarbeitung : <string.h> Datum und Zeitfunktionen <time.h> 10.2. Benutzen von Biblotheksfunktionen Um eine Bilblothektsfunktion benutzen zu können, muß der zugehörige header-file in das Programm vor der eigentlichen Verwendung der Biblotheksfunktion mit #include hereingezogen werden. In den Include Dateien werden neben den Funktionsprototypen auch Werte für Symbole definiert, die für die gesamte Programmumgebung wichtig sind. Beispiel : gets() aus stdio wird benötigt, deshalb am Anfang des Programmoduls : #include <stdio.h> 102 10.3. Funktionsnamen Alle Biblotheksfunktionsnamen sind als externe Identifiers deklariert. Deshalb ist es nicht möglich eine eigene Funktion mit dem Namen einer Biblotheksfunktion zu deklarieren. Außerdem sind alle Funktionsnamen die mit einem Underscore Buchstaben beginnen für sogenannte "behind-the-screen" Macros definiert. 10.4. Routinen zum Bearbeiten von Strings und Buffern : <string.h> Es gibt in der RTL-Library eine Reihe von Funktionen zur Bearbeitung von Strings und Buffern. Der Unterschied zwischen Stringhandling und Bufferhandling Funktionen ist, daß die Stringfunktionen nur bei Stringargumenten, das heißt bei Null-terminierten Character-arrays arbeiten. Buffer Funktionen arbeiten bei jedem Array von beliebigen Datenobjekten, dafür muß aber die Bytegröße des Arrays, bzw. die Anzahl der Bytes, spezifiziert werden. 10.4.1. Die Stringfunktionen Mit diesen Funktionen kann man : Die Länge eines Strings bestimmen Zwei Strings vergleichen Kopieren, anhängen und duplizieren von Strings Buchstaben in einem String finden Trennsymbole von einem String herausziehen In der nächsten Tabelle werden die Funktionen zur Verarbeitung von Strings beschrieben: Funktionsname size_t strlen (char *cs) Beschreibung liefert Länge von cs, ohne den terminierenden Character '\0'. int strcmp (char *cs, char *ct) Zeichenketten cs und ct vergleichen; liefert <0 wenn cs kleiner ct, 0 wenn cs gleich ct, oder >0 wenn cs größer ct. char *strncpy (char *s, char *ct, size_t n) wie die Funktion strcpy, es werden aber nur n Zeichen aus ct in s kopiert. Falls der String ct kürzer als n Characters ist, wird das Array mit '\0' aufgefüllt. Falls ct mehr als n Zeichen hat werden nur n Zeichen kopiert, es fehlt dann der terminierende Nullcharacter (Achtung !!). 103 char *strstr (char *cs, char *ct) sucht das Vorkommen des strings ct in cs. Die Funktion liefert einen Zeiger auf die erste Kopie von ct oder NULL, falls nicht vorhanden. char *strcat (char *s, char *ct) Die Zeichenkette ct wird hinten an die Zeichenkette s angefügt. Es werden so viele Character von ct kopiert, bis die Kopierfunktion auf den terminierenden Nullcharacter von ct stößt. Achtung, falls dieser Character fehlt ! Das Ergebnis der Funktion ist der String s. char *strncat (char *s, char *ct, size_t n) höchstens n Zeichen von ct werden hinten an die Zeichenkette s angefügt und s wird automatisch mit '\0' abgeschlossen. Das Ergebnis der Funktion ist der String s. char *strcpy (char *s, char *ct) Zeichenkette ct wird in den Vektor s kopiert. Der String ct muß mit einen terminierenden Nullcharacter abgeschlossen sein, der dann auch kopiert wird. Fehlt dieser Abschlußcharacter bei ct, wird das Ergebnis s ebenfalls keinen Abschlußchararcter haben. int strncmp (char *cs, char *ct, size_t n) es werden höchstens n Zeichen von cs mit der Zeichenkette ct vergliechen Die Funktion liefert einen Wert <0 wenn cs kleiner ct,0 wenn cs gleich ct, oder größer 0 wenn cs größer ct ist. char *strchr (char *cs, int*c) Sucht das Vorkommen des ersten c in cs. Die Funktion liefert Zeiger darauf oder NULL, falls nicht vorhanden. char *strrchr (char *cs, int*c) Sucht das Vorkommen des letzten c in cs. Die Funktion liefert Zeiger darauf oder NULL, falls nicht vorhanden. size_t strcspn (char *cs, char *ct) zählt die Anzahl der Zeichen am Anfang die alle nicht im String ct vorkommen. size_t strspn (char *cs, char *ct) zählt die Anzahl der Zeichen am Anfang von cs, die sämtliche im string ct vorkommen. char *strpbrk (char *cs, char *ct) Die Funktion ist die inverse Funktion von strpbrk). Sie liefert einen Zeiger auf die Position in cs, an der irgendein Zeichen aus ct erstmals vorkommt, oder NULL, falls keines vorkommt. int strcoll (char *t, size_t n, char *f) Die Funktion transformiert den string f, so daß es ein passendes Argument für memcmp() und strcmp() ist. Das Ergebnis wird in t abgespeichert(nur maximal n characters). char *strtok (char *s, char *c) strtok durchsucht s nach Zeichenfolgen, die durch Zeichen aus ct begrenzt sind. 104 von cs, char *strerror (int*n) liefert Zeiger auf Zeichenkette, Implementierung für Fehler der Nummer n definiert ist. 105 die in der Routinen zum Bearbeiten von Buffern : void *memchr (void *cs, int *c, size_tn) liefert Zeiger auf das erste c in cs oder NULL, wenn das Zeichen in den ersten n Zeichen nicht vorkommt. void *memcpy (void *s, void *ct, size_t n) kopiert n Zeichen von ct nach s; liefert s. Die Strings dürfen nicht überlappen. void *memmove (void *s, void *ct, size_t n) wie memcpy. Da der zu kopierende Buffer intern in einem temporären Buffer zwischengespeichert wird, funktioniert die Funktion auch wenn die Objekte überlappen. int memcmp (void *cs, void *ct, size_t n) vergleicht die ersten n Zeichen von cs mit ct. Das Ergebnis ist analog zu dem von strcmp). Die Funktion ist primär für den Vergleich von Strings entwickelt worden, deshalb arbeitet sie nicht bei Strukturen, die "Löcher" (Null-Werte) in ihrer Struktur haben. Die Funktion arbeitet ebenfalls fehlerhaft bei "high-order-bit" gesetzten Objekten. void *memset (void *s, int*c, size_t n) #Die Funktion initialisiert die ersten n Zeichen von s mit dem Wert c. Das Ergebnis ist ein Pointer s auf den initialiserten Buffer. 10.5. Routinen zur Diagnose : <assert.h> Dieses Macro wird gebraucht um Diagnosepunkte in das Programm einzubauen. void assert(int expr) Wenn der Ausdruck expr gleich Null ist (entspricht FALSE), dann wird folgende Meldung auf stderr ausgegeben : Assertion failed: expression, file filename, line nnn Anschließend wird das Programm abgebrochen. Beispiel : assert(fx=fopen("bac","r")) Falls die Datei "bac" nicht existiert, liefert die Routine fopen den Wert Null und testet den Wert fx . Da er Wert gleich Null ist, gibt es folgende Meldung aus : assert error: expression = fp = fopen("bac","r") , in file openfile.c at line 20 106 10.6. Character Bearbeitung : <ctype.h> Funktionen zum Testen und Konvertieren eines Characterarguments. Jede Funktion hat einen int Wert als Argument. Das Ergebnis ist ebenfalls von Typ int. Die Funktionen liefern als Ergebnis der Wert TRUE (=Null) oder FALSE (ungleich Null). isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper Alphabetische oder numerische Buchstaben : isalpha()oder isdigit() ist true Alphabetische Buchstaben : isupper() oder islower() ist true, oder iscntrl(), isdigit(), ispunct(), und isspace() ist false control character, Asciicode von 0 bis 1F, dazu 7F Dezimalzahl : 0 bis 9 sichtbares Zeichen, außer Leerzeichen Kleinbuchstabe Druckbarer Buchstabe, mit Leerzeichen Druckbarer Buchstabe, außer Leerzeichen, Buchstaben oder Zahlen für die isalnum() ist wahr Leerzeichen (' '), form feed ('\f'), newline ('\n'), carriage return ('\r'), horizontal tab ('\t') oder vertical tab ('\v') Großbuchstabe oder irgendeine implementationsdefinierte Untermenge von Buchstaben, für die iscntrl(), isdigit(), ispunct(), und isspace() false ist Hexadezmalzahl Konvertiert Buchstabe in Kleinbuchstaben Konvertiert Buchstabe in Großbuchstaben Bemerkung : Im 7 Bit ASCII-Zeichensatz haben die sichtbaren Zeichen ASCII-Codes zwischen 0x20 (' ') und 0x7E , die Steuerzeichen liegen zwischen 0 und 0x1F, sowie das Zeichen 0x7F. 10.7. Error Funktionen : <error.h> Alle beim Programmablauf sich ereignenden Fehler setzen entsprechend dem eingetretenen Fehler einen entsprechenden Wert in der globalen Variablen errno. Innerhalb des Include-Files <error.h> werden eine Reihe von Errornummern für bestimmte Fehler definiert: Beispiel : #define EDOM 33 #define ERANGE 34 /* Verlassen des Definitionsbereichs einer Funktion,z.B. sqrt(c) nur falls c>0 */ /* Funktionsergebnis zu groß für eine double Zahl */ 107 10.8. Floating Point Funktionen : <float.h> In diesem Modul werden Konstanten, die sich auf Gleitpunktarithmetik beziehen, definiert. #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define FLT_ROUNDS FLT_RADIX FLT_DIG FLT_EPSILON FLT_MANT_DIG FLT_MIN FLT_MIN_EXP FLT_MAX FLT_MAX_EXP DBL_DIG DBL_EPSILON DBL_MANT_DIG DBL_MIN DBL_MIN_EXP DBL_MAX DBL_MAX_EXP 2 6 1 1E-5 1E-37 1E+37 10 1E-9 1E-37 1E+37 10.9. Impementationsabhängige Grenzwerte : <limits.h> Diese Include-Datei definiert Konstanten für den Wertumfang der ganzzahligen Typen. #define #define #define #define #define #define CHAR_BIT CHAR_MIN CHAR_MAX SCHAR_MAX SCHAR_MIN UCHAR_MAX 8 (~127) 127 #define SHRT_MIN #define SHRT_MAX #define USHRT_MAX (~32767) 32767 0xFFFF #define INT_MIN #define INT_MAX #define UINT_MAX (~32767) 32767 0xFFFF #define LONG_MIN #define LONG_MAX #define ULONG_MAX (~2147483647) 2147483647 0xFFFFFFFF 108 10.10.Routinen für nationale Eigenheiten : <locale.h> Diese Include Datei liefert Routinen, die die Programmierumgebung auf nationale Eigenheiten einer Sprache einstellt, bzw. abfrägt, welche Eigenheiten der Programmierumgebung auf nationale Eigenheiten rücksicht nimmt. Beispiel : Das deutsche Datumsfomat ist ein anderes als das Amerikanische. char *setlocale(int, char *) setzt, bzw, frägt nach der lokalen Programmierumgebung. Einstellung der 10.11.Mathematische Bibliothek : <math.h> #define HUGE_VAL double sin(double x) double cos(double x) double tan(double x) double asin(double x) double acos(double x) double double x) double double double double double double atan(double x) atan2(double y,double sinh (double x) cosh (double x) tanh (double x) exp (double x) log (double x) log10 (double x) double pow (double x,double y) double sqrt (double x) double ceil (double x) double floor (double x) double fabs (double x) double ldexp (double x,int n) double frexp (double x, int *exp) double modf (double x, double *ip) (* (double *) __HUGE_VAL) Sinus von x Kosinus von x Tangens von x arcsin(x) im Bereich [-•/2, •/2], x Œ[1, 1]. arccos(x) im Bereich [0, •], x Œ [-1, 1]. arctan(x) im Bereich [-•/2, •/2] arctan(y / x) im Bereich [-•, •] Sinus Hyperbolicus von x Cosinus Hyperbolicus von x Tangens Hyperbolicus von x Exponentialfunktion ex natürlicher Logarithmus Ln(x), x > 0 Logarithmus zur Basis 10 log10(x), x > 0 xy. Ein Argumentfehler liegt vor bei x=0 und y•0, oder bei x<0 und y ist nicht ganzzahlig •x, x•0 kleinster ganzzahliger Wert, der nicht kleiner als x ist, als double größter ganzzahliger Wert, der nicht größer als x ist, als double absoluter Wert | x | x * 2n zerlegt x in eine normalisierte Mantisse im Bereich [1/2, 1], die als Resultat geliefert wird, und eine Potenz von 2, die von *exp abgelegt wird. Ist x null, sind beide Teile des Resultats null. zerlegt x in einen ganzzahligen Teil und einen Rest, die beide das gleiche Vorzeichen wie x besitzen. Der ganzzahlige Teil wird bei *ip abgelegt, der Rest ist das Resultat 109 double fmod (double x, double y) Gleitpunktrest von x/y, mit dem gleichen Vorzeichen wie x. Wenn y null ist, hängt das Resultat von der Implementierung ab. 10.11.1.Lokale Sprünge : <setjmp.h> Mit den Vereinbahrungen in <setjmp.h> kann man die normale Folge von Funktionsaufruf und -ende umgehen. Typischerweise erlaubt das einen direkten Rücksprung von einem tief verschachtelten Funktionsaufruf. int setjmp(jmp_buf env) Der Makro setjmp speichert Zustandsinformation in env zur Auswertung durch longjmp. void longjmp(jmp_buf env, int value) stellt den Zustand wieder her, der beim letzten Aufruf von setjmp gespeichert wurde. 10.11.2.Signal Handling : <signal.h> In diesem File sind Konstruktionen, die es dem Programmierer ermöglichen, bestimmte Bedingungen oder Signale zu behandeln. Außerdem werden Werte für Signalsymbole definiert. #define #define #define #define #define #define SIGABRT SIGFPE SIGILL SIGINT SIGSEGV SIGTERM 22 11 15 /* Abbruch */ 8 /* Floating point exeption */ 4 /* Illegale Konstruktion */ 2 /* Interrupt */ /* Segmentverletzung */ /* asynchroner Abbruch */ Die nachfolgenden Macros können als zweites Argument in der Funktion signal benutzt werden : #define SIG_DFL #define SIG_ERR #define SIG_IGN ((void (*) (int)) 0) ((void (*) (int)) -1) ((void (*) (int)) 1) /* default */ /* error */ /* ignore */ void (*signal(int sig, void (*func) (int))) (int) ) Diese Funktion ordnet dem Signal sig zum Signal Handler func(). Die Funktion func() wird mit dem Argument sig aufgerufen. int raise(int) Bei dieser Funktion wird das signal sig ausgelöst. 10.11.3.Standarddefinitionen : <stddef.h> In diesem Includefile werden Systemweite Größen definiert. Es enthält die Definition von vier Macros : ptrdiff_t size_t NULL errno /* Speicherplatz um die Differenz zweier Pointer zu speichern */ /* unisgned integer type als Ergebnis des sizeof Operators */ /* Wert für den Error Fall */ /* globale Variable für den Errorcode */ 110 10.11.4.Variable Argumentsanzahl bei Funktionsaufrufen : <stdarg.h> Diese Include-Datei bietet die Möglichkeit, portable Funktionen zu schreiben, die eine Liste von Funktionsargumenten besitzen, deren Anzahl und Datentyp nicht bekannt ist. (vgl die Funktion printf( ... ) ). Angenommen, lastarg ist der letzte benannte Parameter einer Funktion f, die mit einer variablen Anzahl von Argumenten aufgerufen wird. Dann vereinbart man in f eine Varialbe ap vom Datentyp va_list, die der Reihe nach auf jedes Argument zeigen wird: typedef char * va_list; Als erstes muß ap mit dem Makro va_start initialisiert werden, bevor auf die unbekannten Argumente zugegriffen wird: va_start(va_list ap, lastarg) Daran anschließend liefert jede Ausführung des Makros va_arg einen Wert, der den Datentyp und den Wert des nächsten unbenannten Arguments besitzt. Außerdem ändert der Makro den Wert von ap so, daß der nächste Aufruf von va_arg das nächste Argument liefert: va_arg(va_list ap, type) Der Makro void va_end(va-List ap) muß einmal aufgerufen werden, nachdem die Argumente bearbeitet wurden. Dieses Macro entspricht einer Art "Cleanup" Funktion. Beispiel : void f (arg1, arg2,. . . . ) int arg1, arg2; 111 10.11.5. Generelle Utilities : <stdlib.h> Diese Gruppe von Funktionen fällt in folgende Untergruppen : Dynamisches Beschaffen von Speicher Such und Sortierverfahren Zufallszahlen Kommunikation mit der Programmierumgebung Ganzzahlige Arithmetik Verlassen des Programms Multibyte - Character - Funktionen Multibyte - String - Funktionen In stdlib.h werden folgende Typen definiert : div_t ... Eine Struktur, als Ergebnis der div() Funktion ldiv_t ... Eine Struktur, als Ergebnis der ldiv() Funktion size_t ... Datentyp als Ergebnis des sizeof-Operators wchar_t ... Datentyp, der alle vom Standard unterstützten Buchstaben speichern kann Außerdem werden folgende MACROS definiert : ERANGE Wert der Errorvariable errno, falls der Wertebereich einer Variablen verlassen wurde. HUGE_VAL Wert der überschritten wird verlassen wurde. RAND_MAX Größter Wert der rand() Funktion EXIT_FAILURE Rückgabewert der exit - Funktion bei einem auftretenden Fehler EXIT_SUCCESS Rückgabewert der exit - Funktion bei erfolgreicher Durchführung 112 10.11.5.1. Datenkonvertierroutinen double atof (const char *s) wandelt s in double um; die Funktion ist äquivalent zu: strtod(s, (char**) NULL) int atoi (const char *s) wandelt s in int um; die Funktion ist äquivalent zu: (int)strtol(s, (char**) NULL, 10) long atol (const char *s) wandelt s in long um; die Funktion ist äquivalent zu: strtol(s, (char**) NULL, 10) double strtod (const char *s, char **endp) wandelt den Anfang der Zeichenkette s in double um, dabei wird Zwischenraum am Anfang ignoriert. Die Funktion speichert einen Zeiger auf einen nicht umgewandelten Rest der Zeichenkette bei *endp, falls endp nicht Null ist. Falls das Ergebnis zu groß ist, (also bei overflow), wird als Resultat HUGE_VAL mit dem korrekten Vorzeichen geliefert; liegt das Ergebnis bei Null (also bei underflow), wird Null geliefert. In beiden Fällen erhält errno den Wert ERANGE. long strtol (const char *s, char **endp, int base) wandelt den Anfang der Zeichenkette s in long um, dabei wird Zwischenraum am Anfang ignoriert. Die Funktion speichert einen Zeiger auf einen nicht umgewandelten Rest der Zeichenkette bei *endp, falls endp nicht Null ist. Hat base einen Wert zwischen 2 und 36, erfolgt die Umwandlung unter der Annahme, daß die Eingabe in dieser Basis repräsentiert ist. Hat base den Wert Null, wird als Basis 8, 10 oder 16 verwendet; eine führende Null bedeutet dabei oktal und 0x oder 0X zeigen eine hexadezimale Zahl an. In jedem Fall stehen Buchstaben für die Ziffern von 10 bis base-1; bei Basis 16 darf 0x oder 0X am Anfang stehen. Wenn das Resultat zu groß werden würde, wird je nach Vorzeichen LONG_MAX oder LONG_MIN geliefert und errno erhält den Wert ERANGE. unsigned long strtoul (char *, char **, int) funktioniert wie strtol, nur ist der Resultattyp unsigned long und der Fehlerwert ist ULONG_MAX. 10.11.5.2. Zufallsgenerator int rand (void) liefert eine ganzzahlige Pseudo-Zufallszahl im Bereich Null bis RAND_MAX; dieser Wert ist mindestens 32767. void srand (unsigned int seed) benutzt seed als Ausgangswert für eine neue Folge von Pseudo-Zufallszahlen. Der erste Ausgangswert ist 1. 10.11.5.3. Memory - Management Funktionen 113 void *calloc (size_t nobj, size_t size) liefert einen Zeiger auf einen Speicherbereich für einen Vektor von nobj Objekten, jedes mit der Größe size, oder NULL, bei Fehler. void free (void *p) void *malloc (size_t) gibt den Bereich frei, auf den p zeigt. void *realloc (void *p, size_t size) liefert einen Zeiger auf einen Speicherbereich für ein Objekt der Größe size, oder NULL bei Fehler. ändert die Größe des Objekts, auf das p zeigt in size ab. 10.11.5.4. Programmierumgebungsfunktionen void abort (void) sorgt für eine abnormale Beendigung des Programms. int atexit (void (*fcn) (void) ) hinterlegt die Funktion fcn, damit sie aufgerufen wird und das Programm normal endet, und liefert einen von Null verschiedenen Wert, wenn die Funktion nicht hinterlegt werden kann. void exit (int status) beendet das Programm normal. Die Werte EXIT_SUCCESS und EXIT_FAILURE können ebenfalls angegeben werden. char *getenv(char *name) liefert die zu Name gehörige Zeichenkette aus der Programmierumgebung oder Null, wenn keine Zeichenkette existiert. int system(char *s) liefert die Zeichenkette s an die Betriebssystem-Umgebung zur Ausführung. 114 10.11.5.5. Suchen und Sortieren void *bsearch (void *key, void *base, size_t n, size_t size, int (*cmp) (const void *keyval, const void *datum) ) durchsucht base[0] ... base[n-1] nach einem Eintrag, der gleich *key ist. void qsort (void *base, size_t n, size_t size, int (*cmp)(const void *, const void *) ) sortiert einen Vektor base[0] ... base[n-1] von Objekten der Größe size in aufsteigender Reihenfolge. 10.11.5.6. Integer Arithmetik int abs (int n) liefert den Absolutwert eines intArguments. long labs (long) liefert den Absolutwert eines longArguments. div_t div (int num, int denom) berechnet Quotient und Rest von num/denum. Die Resultate werden in int Komponenten quot und rem einer Struktur vom Typ div_t abgelegt. ldiv_t ldiv(long num, long denom) berechnet Quotient und Rest von num/denum. Die Resultate werden in long Komponenten quot und rem einer Struktur vom Typ ldiv_t abgelegt. 115 10.11.6.Datum und Zeitfunktionen : <time.h> struct tm { int int int int int int int int int }; tm_sec; /*Sekunden nach der vollen Minute (0 bis 59)*/ tm_min; /*Minuten nach der vollen Stunde (0 bis 59)*/ tm_hour; /*Stunden seit Mitternacht (0 bis 23)*/ tm_mday; /*Tage im Monat (1 bis 31)*/ tm_mon; /*Volle Monate seit Januar (0 bis 11)*/ tm_year; /*Jahre seit 1900*/ tm_wday; /*Tage seit Sonntag (0 bis 6)*/ tm_yday; /*Tage seit dem 1. Januar (0 bis 365)*/ tm_isdst; /*Kennzeichen für Sommerzeit positiv, wenn Sommerzeit gilt, Null, wenn Sommerzeit nicht gilt, negativ, wenn die Information nicht zur Verfügung steht.*/ 116 Dazu gibt es folgende Routinen : clock_t clock (void) liefert die Rechnerzeit, die das Programm seit Beginn seiner Ausführung verbraucht hat, oder -1, wenn diese Information nicht zur Verfügung steht. clock()/CLK_TCK ist eine Zeit in Sekunden double difftime (time_t time2, time_t time1) liefert time2-time1 ausgedrückt in Sekunden time_t mktime (struct tm *) wandelt die Ortszeit in der Struktur *tp in Kalenderzeit um, die so dargestellt wird wie bei time. mktime liefert -1, wenn die Kalenderzeit nicht dargestellt werden kann time_t time (time_t *tp) liefert die aktuelle Kalenderzeit oder -1, wenndiese nicht zur Verfügung steht. Wenn tp von NULL verschieden ist, wird der Resultatwert auch bei *tp abgelegt char *asctime (struct tm *tp) konstruiert aus der Zeit in der Struktur *tp eine Zeichenkette in der Form Sun Jan 3 15:14:13 1988\n\0 char *ctime (time_t *tp) verwandelt die Kalenderzeit *tp in Ortszeit; dies ist äquivalent zu asctime(localtime(tp)) struct tm *gmtime (time_t *tp) verwandelt die Kalenderzeit *tp in Coordinated Universal Time (UTC). Die Funktion liefert NULL, wenn UTC nicht zur Verfügung steht struct tm *localtime (time_t *tp) verwandelt die Kalenderzeit *tp in Ortszeit. size_t strftime (char *s, size_t size, char *format, struct tm *timepr) Erlaubt einen String zu erzeugen mit der Spezifischen Information aus timeptr 117 11. Der C Praeprocessor Zu Beginn des Übersetzungsvorgangs wird ein C-Programm von einem Praeprocessor bearbeitet, der das CProgramm zur Kompilierung aufbereitet. Der Praeprocessor sucht nach bestimmten Direktiven und verändert entsprechend dieser Anweisungen die Programmzeilen des C-Programms. Die zwei meist genutzten Anweisungen für den Praeprocessor sind das Einfügen (include) eines Fileinhalts an eine bestimmte Stelle des zu kompilierenden Files und die Ersetzung (define) beliebiger Buchstabenfolgen durch vorher definierte Buchstabenfolgen. Jede Praeprocessordirektive beginnt mit dem Zeichen # (Hash). 11.1. Fileinhalt einfügen : #include #include "filename" sucht die Datei filename im selben Dateiverzeichnis wie das Sourceprogramm oder #include <filename> sucht die Datei filename nach Implementationsabhäniger Art in verschiedenen Dateiverzeichnissen, meist im Library Verzeichnis. Der Default filetyp einer Include Datei ist .h (z.B.:stdio.h). Beispiel : #include <stdio.h> #include "header" 118 11.2. MACRO Ersetzung : #define, #undefine #define #undef name name zu ersetzender Text nimmt die Definition von name zurück Im ganzen Sourcefile wird name durch zu ersetzender Text ersetzt. Normalerweise entspricht der zu ersetzende Text dem Rest der Zeile, eine Zeile kann jedoch durch das Zeichen \ am Ende einer Zeile verlängert werden. Ersetzungen von name werden nur bei sogenannten Tokens vorgenommen, nicht innerhalb von quoted Strings. Beispiel : #define Fehler #define maximal #define ewig im Programm : "Achtung ein Fehler ist aufgetreten" 128 for (;;) x = maximal + 10; printf(Fehler); ewig; /* Achtung :*/ printf("Fehler"); Ein MACRO kann auch mit Argumenten definiert werden. Beim Aufruf werden die Argumente ihrer Definition entsprechend eingesetzt. Beispiel : #define max(A,B) ((A) > (B) ? (A) : (B)) im Programm : ergibt : x = max(p+q,r+s); x = ((p+q) > (r+s) ? (p+q) : (r+s)) Mit dem # Operator gibt es eine Möglichkeit, formale Parameter als strings ausdrucken zu lassen oder mit dem ## Operator an ein anderes Argument anzuhängen : Beispiel : #define im Programm : ergibt : druck(expr) printf("#expr=%f\n",expr) druck(x/z) printf("x/y=%f\n",expr); und daraus : printf("x/y=%f\n",expr); #define paste (one,two,three) im Programm : paste(a,b,c) ergibt : Definition des Eintrags abc. 119 one ## two ## three 11.3. Konditionales Praeprocessing #if In Abhängigkeit des Wertes eines Ausdrucks (Null = FALSE, Ungleich Null = TRUE), werden die nachfolgend geklammerten Direktiven vom Praeprocessor ausgeführt. #if expression ..... #endif #if expression ..... #elif ... #endif #if expression ..... #else ... #endif wobei : expression : ==, !=, <=, <, >, >=, defined(name) statt #if gibt es auch : #ifdef name if name is defined #ifndef name if name is not defined Beispiel : #if SYSTEM == SYSV #define HDR "sysv.h" #elif SYSTEM == BSD #define HDR "bsd.h" #elif SYSTEM == MSDOS #define HDR "msdos.h" #else #define HDR "default.h" #endif #include HDR 120 Beispiel : /* demonstration von Praeprocessordirektiven */ #include "pre-header.h" #include <stdio.h> #ifdef CIS #define TEXT "guten Tag am CIS \n" #define ANZAHL 3 #endif #ifndef CIS #define CIS 0 #define TEXT "Alles schläft am CIS \n" #define ANZAHL 0 #endif #define max(a,b) ((a) > (b) ? (a) : (b)) main() { if (CIS) { int a,b,i; puts("wir arbeiten"); printf("Eingabe von zwei Zahlen >>>"); scanf("%d %d",&a,&b); for (i=0;i<ANZAHL;i++) printf(TEXT); printf("die Größere Zahl ist >%d\n",max(a,b)); }; if (!CIS) { puts("wir arbeiten nicht "); printf(TEXT); } } 121 12. Lösungen 12.1. Beispiele und Lösungen zur Stringverarbeitung 12.1.1. Beispiel zum Ausgeben von Strings: /* Strings und Arrays */ #include <stdio.h> char s1[5] = "Hallo"; char s2[4] = "Hallo"; char s3[] = "Hallo"; main_str() { puts(s1); puts(s2); puts(s3); } 12.1.2. Beispiel zum Initialisieren von Strings: /*************************** filename : string_array_init.c autor : max Version : 1.0 Datum : jun 94 Thema : Strings und Arrays, C - Kurs ***************************/ #include <stdio.h> char s1[6] = "Hallo"; char s2[5] = "Hallo"; char s3[5] = "H"; char s4[] = "Hallo"; int a1[5] = {1,2,3,4,5}; int a2[] = {1,2,3,4,0}; int a3[5] = {1,2}; main_str_array_init() { int i; char s5[] = "Hallo"; int a4[] = {1,2}; puts(s1); puts(s2); puts(s3); puts(s4); puts(s5); printf("\n a1 >>"); for (i=0;i<5;i++) printf("%d ",a1[i]); printf("\n a2 >>"); for (i=0;i<5;i++) printf("%d ",a2[i]); printf("\n a3 >>"); for (i=0;i<5;i++) printf("%d ",a3[i]); printf("\n"); printf("\n a4 >>"); 122 for (i=0;i<5;i++) printf("%d ",a4[i]); printf("\n"); } /* Ergebnis : Hallo HalloH H Hallo Hallo a1 >>1 2 3 4 5 a2 >>1 2 3 4 0 a3 >>1 2 0 0 0 a4 >>1 2 18529 27756 28416 */ 12.1.3. Anagramm /* Projektname : anagramm Filename : anagramm.c Autor : max Datum : 22.Jun.99 Beschreibung : Es werden vom Terminal zwei strings eingelesen und getestet ob es Anagramme sind */ #include <stdio.h> #define STRSIZE 132 /* Globale Variablen */ /* Funktionsprototypen */ int str_compare(char str1[],char str2[]); int str_sort1(char str1[],char str2[]); int str_sort2(char str[]); int swap(char *i,char *j); int find_index_of_min(char str[],int Anzahl_der_Elemente,char referenz_char); /**************** Hauptprogramm ***************************/ main() { char str1[STRSIZE]; char str2[STRSIZE]; char strsort1[STRSIZE]; char strsort2[STRSIZE]; printf("Geben Sie den ersten String ein >>>"); gets(str1); printf("Geben Sie den zweiten String ein >>>"); gets(str2); strcpy(strsort1,str1); strcpy(strsort2,str2); str_sort2(strsort1); printf(" string sortiert %s ,StringlŠaenge = %d \n ",strsort1,strlen(strsort1)); str_sort2(strsort2); printf(" string sortiert %s ,StringlŠaenge = %d \n ",strsort2,strlen(strsort2)); if (str_compare(strsort1,strsort2)) { printf("die Strings %s und %s sind Anagramme\n",str1,str2); } else 123 { printf("die Strings %s und %s sind keine Anagramme\n",str1,str2); } } /**********************************************************************/ int str_sort1(char str[],char strsort[]) /* Sortiert alphabetisch buchstaben, gespiechert in str uns speichert sie in strsort Eingabe : str[] ... Array of Buchstaben Ausgabe : strsort [] ... Array of Buchstaben */ { int i,j,index_kleiner; char hlp; for(i=0;i<strlen(str);i++) { hlp = strsort[i]; index_kleiner = i; for (j=i+1;j<strlen(strsort);j++) if (strsort[j] < hlp) { hlp = strsort[j]; index_kleiner = j; } hlp = strsort[i]; strsort[i] = strsort[index_kleiner]; strsort[index_kleiner] = hlp; } return; } /**********************************************************************/ int str_compare(char str1[],char str2[]) /* vergleicht alphabetisch strings Eingabe : str[] ... Array of Buchstaben strsort [] ... Array of Buchstaben Ausgabe : Return Wert = 1 falls Strings gleich sind */ { int i,j; char hlp; if (strlen(str1) != strlen(str2)) return 0; i=0; while ((i<strlen(str1)) && (str1[i] == str2[i])) i++; if (i == strlen(str1)) return 1; else return 0; } /**********************************************************************/ int str_sort2(char str[]) /* /* Sortiert alphabetisch buchstaben, gespeichert in str Eingabe : char str[] ... gespeicherte Buchstaben */ { char kleinster_char; int i,j,Anzahl_der_Elemente; Anzahl_der_Elemente = strlen(str); 124 for (i=0; i< strlen(str); i++) { kleinster_char = find_index_of_min(&str[i],Anzahl_der_Elemente,str[i]); swap(&str[i],&str[kleinster_char+i]); Anzahl_der_Elemente -- ; } } /**********************************************************************/ int swap(char *i,char *j) /* Vertauscht die Buchstaben, die unter den Adressen i und j abgespeichert sind { int help; help = *i; *i = *j; *j = help; return; } /**********************************************************************/ int find_index_of_min(char str[],int Anzahl_der_Elemente,char referenz_char) /* Eingabe : int str[] ... Character String int Anzahl_der_Elemente ... Anzahl Elemente von liste int referenz_char ... char muss kleiner sein als referenz_char Ausgabe : return value .... Index des kleinsten Elements innerhalb von str */ { int i; int index_kleinstes_element=0; for (i=0;i<Anzahl_der_Elemente;i++) if (str[i] < referenz_char) { index_kleinstes_element = i; referenz_char = str[i]; } return index_kleinstes_element; } 125 */ 12.1.4. Vorkommen eines Strings in einem Anderen /* Projektname : strcompare Filename : strcomp.c Autor : max Datum : 10.jul.94 Beschreibung : Es werden vom Terminal zwei strings eingelesen, wobei festgestellt wird, ob der zweite String im ersten enthalten ist */ #include <stdio.h> #define NUM_OF_CHAR 80 char str[5] = "abcnsd"; /* Globale Variablen */ /* Funktionsprototypen */ int my_strcmp(char str1[],char str2[]); /********************** main ***********************************/ main() { char string1[NUM_OF_CHAR],string2[NUM_OF_CHAR]; int i; puts(" Hallo hier ist das Programm strcomp"); puts(" Lese zwei strings ein und stelle fest, ob der zweite im ersten enthalten ist"); puts(" Bitte geben Sie den ersten string ein "); fflush(stdin); scanf("%s",string1); puts(" Bitte geben Sie den zweiten string ein "); fflush(stdin); scanf("%s",string2); if (my_strcmp_pointer(string1,string2)) printf(" String2 >>>%s<<< ist in String1 >>>%s<<< enthalten\n",string2,string1); else printf(" String2 >>>%s<<< ist NICHT in String1 >>>%s<<< enthalten\n",string2,string1); } /********************** Funktionen ***********************************/ int my_strcmp(char str1[],char str2[]) /* Diese Funktion ueberprueft ob str2 in str1 vorhanden ist. Eingabe : char str1[] ... String1 char str2[] ... String2 Ausgabe : return - value = 1, falls str2 in str1 return - value = 0, sonst */ { int ist_enthalten,i,j; ist_enthalten = 0; i=0; do { if (str1[i] != str2[0]) i++; /* finde Vorkommen des ersten Elements aus str2 in str1 */ 126 else { j=0; /* erstes Element str2 in str1 gefunden */ while ( str1[i] && str2[j] && str1[i] == str2[j] ) /* vergleiche Reststring str2 */ { i++; j++; }; if (str2[j] == 0) ist_enthalten = 1; /* Falls str2 am Ende dann gefunden */ }; } /* Falls str1 nicht abgearbeitet dann Suche fortsetzen */ while (str1[i] && ! ist_enthalten); return ist_enthalten; } /********************** Funktionen ***********************************/ int my_strcmp_pointer(char *str1,char *str2) /* Diese Funktion ueberprueft ob str2 in str1 vorhanden ist. Eingabe : char *str1 ... String1 char *str2 ... String2 Ausgabe : return - value = 1, falls str2 in str1 return - value = 0, sonst */ { int ist_enthalten,i,j; char *beg_of_str2; ist_enthalten = 0; i=0; beg_of_str2 = str2; do { if (*str1 != *str2) str1++; /* finde Vorkommen des ersten Elements aus str2 in str1 */ else { str2 = beg_of_str2;; /* erstes Element str2 in str1 gefunden, postioniere str2 wieder auf den Anfang */ while ( *str1 && *str2 && *str1 == *str2 ) /* vergleiche Reststring str2 */ { str1++; str2++; }; if (*str2 == 0) ist_enthalten = 1; /* Falls str2 am Ende dann gefunden */ }; } /* Falls str1 nicht abgearbeitet dann Suche fortsetzen */ while (*str1 && ! ist_enthalten); return ist_enthalten; } 127 12.1.5. Einlesen von 10 Buchstaben ohne Mehrfachnennungen /* Projektname : einlesen_zehn_buchstaben Filename : readtenchar.c Autor : max Datum : 5.jul.94 Beschreibung : Es werden vom Terminal 10 Buchstaben eingelesen, wobei mehrfachnennungen abgelehnt werden */ #include <stdio.h> #define NUM_OF_CHAR 10 /* Globale Variablen */ /* Funktionsprototypen */ int ist_in_liste(char liste[],int Anzahl_der_bisherigen_Listenelemente,char Buchstabe); char Einlesen(void); /********************** main ***********************************/ main() { char liste[NUM_OF_CHAR]; int Anzahl_der_bisherigen_Listenelemente; int i; char neuer_buchstabe; puts(" Hallo hier ist das Programm readtenchar"); puts(" Lese 10 verschiedene Buchstaben von Terminal und gebe sie wieder aus"); Anzahl_der_bisherigen_Listenelemente=0; do { neuer_buchstabe=Einlesen(); if (! ist_in_liste(liste,Anzahl_der_bisherigen_Listenelemente,neuer_buchstabe)) liste[Anzahl_der_bisherigen_Listenelemente++] = neuer_buchstabe; else puts(" Buchstabe ist doppelt, Bitte neue Eingabe "); } while (Anzahl_der_bisherigen_Listenelemente < NUM_OF_CHAR); for (i=0;i<NUM_OF_CHAR;i++) printf("Buchstabe Nummer %d = %c\n",i,liste[i]); } /********************** Funktionen ***********************************/ int ist_in_liste(char liste[],int Anzahl_der_bisherigen_Listenelemente,char Buchstabe) /* Diese Funktion ueberprueft ob in der liste[0]...liste[Anzahl_der_bisherigen_Listenelemente] der Buchstabe vorhanden ist Eingabe : char liste[] ... bisher eingelesene Buchstaben int Anzahl_der_bisherigen_Listenelemente ... Anzahl der bisher eingelesenen Elemente char Buchstabe ... zu testender Buchstabe, ob in liste Ausgabe : return - value = 1, falls Buchstabe in der Liste ist return - value = 0, sonst */ { int gefunden,i; gefunden = 0; for (i=0;i< Anzahl_der_bisherigen_Listenelemente;i++) if (liste[i] == Buchstabe) gefunden = 1; 128 return gefunden; } char Einlesen(void) /* Liest einen Alpha - Character vom Terminal ein Ausgabe : return - value : eingelesener Buchstabe */ { char Buchstabe; do { printf(" Bitte naechsten Buchstaben eingeben "); fflush(stdin); Buchstabe = getchar(); if (! isalpha(Buchstabe)) puts(" Bitte eine Buchstaben und keinen Numerischen Wert eingeben"); } while (! isalpha(Buchstabe)); return Buchstabe; } 129 12.1.6. Zählt Sätze in der Eingabe /*************************** filename : sentence.c autor : max Version : 1.0 Datum : jun 94 Thema : programm zum zählen von eingegebenen Sätzen ***************************/ #include <stdio.h> void check_sent(char ch); /* funktions declaration (=prototyp) */ int num_of_sentence = 0; /* globale Variable */ /* ======================================================== */ main_sentence() { char ch; puts(" Bitte geben Sie beleibige Saetze ein, Ende der Eingabe ^D "); while ( (ch=getchar() ) != EOF) { check_sent(ch); }; printf(" Die Anzahl der gelesenen Saetze ist %d \n",num_of_sentence); } /* ======================================================== */ void check_sent(char ch) /* funktions definition */ { } if ((ch == '.') || (ch == '!') || (ch == '?')) num_of_sentence ++; 130 12.2. Sortiert zehn Zahlen /* Projectname : sort_zehn_zahlen Filename : sort_zehn_zahlen.c Autor : max Datum : 17.jul.94 Beschreibung : Es werden vom Terminal 10 Zahlen eingelesen, wobei Mehrfachnennungen abgelehnt werden, dann werden diese zehn Zahlen numerisch sortiert */ #include <stdio.h> #define NUM_OF_ZAHLEN 10 /* Globale Variablen */ /* Funktionsprototypen */ int int int int ist_in_liste(int liste[],int Anzahl_der_bisherigen_Listenelemente,int zahl); find_index_of_min(int liste[],int Anzahl_der_Elemente,int referenz_wert); swap(int *i,int *j); my_sort(int liste[]); int Einlesen(void); /********************** main ***********************************/ main() { int liste[NUM_OF_ZAHLEN]; int Anzahl_der_bisherigen_Listenelemente; int i; int neue_zahl; puts(" Hallo hier ist die Routine readtennumbers"); puts(" Lese 10 verschiedene Zahlen von Terminal und gebe sie wieder aus"); Anzahl_der_bisherigen_Listenelemente=0; do { neue_zahl=Einlesen(); if (! ist_in_liste(liste,Anzahl_der_bisherigen_Listenelemente,neue_zahl)) liste[Anzahl_der_bisherigen_Listenelemente++] = neue_zahl; else puts(" Zahl ist doppelt, Bitte neue Eingabe "); } while (Anzahl_der_bisherigen_Listenelemente < NUM_OF_ZAHLEN); for (i=0;i<NUM_OF_ZAHLEN;i++) printf("Zahl Nummer %d = %d\n",i,liste[i]); my_sort(liste); puts(" Sortiertes Array "); for (i=0;i<NUM_OF_ZAHLEN;i++) printf("Zahl Nummer %d = %d\n",i,liste[i]); } /********************** Funktionen ***********************************/ int ist_in_liste(int liste[],int Anzahl_der_bisherigen_Listenelemente,int zahl) /* Diese Funktion ueberprueft ob in der liste[0]...liste[Anzahl_der_bisherigen_Listenelemente] die Zahl vorhanden ist Eingabe : int liste[] ... bisher eingelesene Zahlen int Anzahl_der_bisherigen_Listenelemente ... Anzahl der bisher eingelesenen Elemente int zahl ... zu testende Zahl, ob in liste Ausgabe : return - value = 1, falls Zahl in der Liste return - value = 0, sonst 131 */ { int gefunden,i; gefunden = 0; for (i=0;i< Anzahl_der_bisherigen_Listenelemente;i++) if (liste[i] == zahl) gefunden = 1; return gefunden; } int Einlesen(void) /* Liest eineganze Zahl vom Terminal ein Ausgabe : return - value : eingelesene Zahl */ { int zahl; printf(" Bitte naechste Zahl eingeben "); fflush(stdin); scanf("%d",&zahl); return zahl; } /*********************************************/ int find_index_of_min(int liste[],int Anzahl_der_Elemente,int referenz_wert) /* Eingabe : int liste[] ... Liste aller Zahlen int Anzahl_der_Elemente ... Anzahl Elemente von liste int referenz_wert ... Element muss kleiner sein als referenz_wert Ausgabe : return value .... Index des kleinsten Elements innerhalb von liste */ { int i; int index_kleinstes_element=0; for (i=0;i<Anzahl_der_Elemente;i++) if (liste[i] < referenz_wert) { index_kleinstes_element = i; referenz_wert = liste[i]; } return index_kleinstes_element; } /*********************************************/ int swap(int *i,int *j) /* Vertauscht die Werte unter den Adressen i und j abgespeichert sind { int help; help = *i; *i = *j; *j = help; return; } /*********************************************/ int my_sort(int liste[]) /* Eingabe : int liste[] ... gespeicherte Zahlen */ { int kleinstes_element,i,j,Anzahl_der_Elemente; Anzahl_der_Elemente = NUM_OF_ZAHLEN; 132 */ for (i=0; i< NUM_OF_ZAHLEN; i++) { kleinstes_element = find_index_of_min(&liste[i],Anzahl_der_Elemente,liste[i]); swap(&liste[i],&liste[kleinstes_element+i]); Anzahl_der_Elemente -- ; } } 133 12.3. FileIO : Zählt Wörter in einem String /* count number of words in a string */ #include <ctype.h> char zeile[132] = {0}; int wrd_count(char *s); main_wrd_count() { int i,wrd=0; char filename[132]; FILE *input; } puts(" Eingabe Filename "); gets(filename); input=fopen(filename,"r"); while ((fgets(zeile,132,input) != EOF) && (wrd <= 100)) { puts(" nŠchste Zeile "); wrd =+ wrd_count(zeile); for (i=0;zeile[i];i++) zeile[i]=0; }; printf(" Die Anzahl der Wšrter ist : %d",wrd); int wrd_count(char *s) { int cnt = 0; while (*s != '\0') { while (isspace(*s)) ++s; /* skip white space */ if (*s != '\0') { ++cnt; while (!(isspace(*s) && *s != '\0' ) ++s; /*skip the word */ } } return cnt; } 134 12.4. Erzeugen Sie mit dem Editor eine beliebige Textdatei und schreiben Sie ein Programm, das eine solche Datei (zeilenweise) rückwärts auf den Bildschirm ausgibt. 1 2 3 4 5 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 /********************************************************************* * Lösung zu Kapitel File I/O *********************************************************************/ #include <stdio.h> #define CLEARSCREEN main () { FILE char char int printf ("\033[2J\033[3;1H") *input; datnam[80]; *file_in_array; offset; /****************************************************************** * Erfragen und Öffnen der Eingabedatei ******************************************************************/ printf ("Eingabedatei: "); scanf ("%s", datnam); if ((input = fopen(datnam,"r")) == NULL) { perror ("Fehler beim ÷ffnen der Datei"); exit (1); } /****************************************************************** * Positionieren auf das Dateiende und Ermitteln des Abstands * vom Dateianfang. Zurücksetzen auf den Anfang der Datei. ******************************************************************/ fseek (input, 0, SEEK_END); offset = ftell (input); rewind (input); /****************************************************************** * Einlesen der gesamten Datei in den Arbeitsspeicher. ******************************************************************/ file_in_array = (char *) malloc (offset); fread (file_in_array, offset-1, 1, input); /****************************************************************** * Durchsuchen der im Arbeitsspeicher befindlichen Datei auf * '\n' vom Ende her und Ausgabe der isolierten Strings. ******************************************************************/ } file_in_array += offset; CLEARSCREEN; while (offset >= 0) { while (*file_in_array != '\n' && offset >= 0) { file_in_array--; offset--; } *file_in_array = '\0'; printf ("%s\n", file_in_array + 1); } printf ("\n\n"); 135