Grundlagen der Programmiersprache C für Studierende der Naturwissenschaften Teil 10: Eigene Bibliotheken Patrick Schön Abteilung für Angewandte Mathematik Vorlesung vom 14. Juli 2014 Gliederung Organisatorisches zur Klausur eigene Bibliotheken Zeiger und Felder (Wdh.) Zeichenketten (Wdh.) Strukturierte Datentypen (Wdh.) Funktionen und Funktionsaufrufe (Wdh.) Dynamische Speicherverwaltung (Wdh.) Ausdrücke, Anweisungen (Wdh.) Operatoren (Wdh.) Typkonversion (Wdh.) Testfragen Klausur I Die Klausur findet Montag (20. Juli) um 16:15 Uhr in diesem Raum statt. Die Bearbeitungszeit ist 60 Minuten. I Eine Anmeldung zur Klausur ist nicht erforderlich. I Bringen Sie zur Klausur Ihren Studierendenausweis oder Personalausweis mit. I Hilfsmittel wie Skripte oder Taschenrechner werden nicht benötigt und sind auch nicht erlaubt. I Stoffumfang der Klausur entspricht den Folien und Übungsaufgaben auf der Kurs-Homepage. Zulassungsvoraussetzungen Zur Klausur wird zugelassen. . . I wer den Kurs über das Zentrum für Schlüsselqualifikationen (ZfS) belegt hat, I wer höchstens zwei Mal in den Übungen gefehlt hat, I und wer mindestens 50% der Punkte aus den Aufgaben erhalten hat. Korrektur und Ergebnis I Die Korrektur wird einige Tage nach der Klausur beendet sein. Die Ergebnisse werden in anonymisierter Form online veröffentlicht. I Die Klausur ist nicht benotet, d. h. es wird nur zwischen bestanden und nicht bestanden unterschieden. I Die Klausureinsicht wird auf der Kurs-Homepage bekannt gegeben I Die endgültige Leistungsverbuchung nimmt das ZfS vor. Dies kann einige Wochen in Anspruch nehmen. Nachklausur Die Nachklausur findet voraussichtlich am Montag, den 24. August 2015 statt. Raum und Uhrzeit werden auf der Kurs-Homepage bekannt gegeben. Zur Nachklausur ist automatisch zugelassen,. . . I wer die Zulassungsvoraussetzungen erfüllt hat (s. o.), I wer die Klausur im ersten Versuch nicht bestanden hat I oder wer beim ersten Klausurtermin verhindert oder aus anderen Gründen nicht anwesend war. Gliederung Organisatorisches zur Klausur eigene Bibliotheken Zeiger und Felder (Wdh.) Zeichenketten (Wdh.) Strukturierte Datentypen (Wdh.) Funktionen und Funktionsaufrufe (Wdh.) Dynamische Speicherverwaltung (Wdh.) Ausdrücke, Anweisungen (Wdh.) Operatoren (Wdh.) Typkonversion (Wdh.) Testfragen Eigene Bibliotheken Wenn einzelne Module ausreichend getestet und für gut befunden wurden, kann man sie in eigenen Bibliotheken zusammenfassen. Eigene Bibliotheken kann man erzeugen, indem man Objektdateien unter der Verwendung des Kommandos $ ar zusammenfügt. Eigene Bibliotheken Wir möchten als Beispiel einen Bibliothek erstellen, die algebraische Operationen mit vollbesetzten Matrizen vom Datentyp double realisiert. Für eine Matrix führen wir eine Struktur ein: struct double_matrix{ double **index; double *values; int n, m; }; I n und m speichert die Dimension der Matrix I index soll den Zugriff auf die Matrix realisieren I values speichert die Einträge ai,j der Matrix I es soll gelten ai,j = Matrix.index[i][j] Wdh: Matrix und Konsekutiver Speicher Zunächst wir ein Speicherbereich für die ganze Matrix angelegt: /* allocate memory first */ buffer = (double *) malloc(m*n*sizeof(double)); if(!buffer) return 1; Dann initialisieren wir A als Doppelzeiger: /* initialize A */ A = (double **) malloc(m*sizeof(double *)); if(!A) return 1; for(i = 0; i < m; ++i) A[i] = &buffer[i*n] ; Indexzugriff über ai,j =A[i][j]. Das Freigeben des Speichers erfolgt durch: /* free storage */ free(A); free(buffer); Header matrix.h Die Definition der Struktur schreiben wir in eine neue Header-Datei matrix.h. Um Schreibarbeit zu ersparen definieren wir den neuen Datentyp Matrix, durch struct double_matrix{ double **index; double *values; int n, m; }; typedef struct double_matrix Matrix; Header matrix.h Wir möchten für unseren Datentyp noch einige grundlegende Operationen festlegen: int initDoubleMatrix( Matrix *A, int m, int n); void freeDoubleMatrix( Matrix *A ); void printDoubleMatrix( Matrix *A); Diese Funktionen soll das Initialisieren einer Matrix unseres Datentyps, die Rückgabe des angelegten Speichers und eine einfache Ausgabe auf der Konsole realisieren. Die tatsächliche Implementierung der Funktionen (Definition) steht dann später in der Datei matrix.c. matrix.c int initDoubleMatrix( Matrix *A, int m, int n) { int i, j; // Speicher allozieren: A->values = (double*)malloc(m*n*sizeof(double)); if( A->values == NULL ) return 1; A->index = (double**)malloc(m*sizeof(double*)); if( A->index == NULL ) return 1; A->m = m; A->n = n; //Speicherbloecke zuweisen: for(i = 0; i < m; i++) A->index[i] = &A->values[i*n]; // Matrix mit 0 Eintraegen initialisieren for( i = 0; i < m; i++ ) for( j = 0; j < n; j++ ) A->index[i][j] = 0.; return 0; } matrix.c void freeDoubleMatrix( Matrix *A ) { free(A->index); free(A->values); } void printDoubleMatrix( Matrix * A) { int i,j; printf(" m = %d, n = %d \n", A->m, A->n); //Ausgabe der Matrix: for( i = 0; i < A->m; i++){ for( j = 0; j < A->n; j++ ) printf("%lf ", A->index[i][j] ); printf("\n"); } } Header einbinden Die Quelltextdatei matrix.c muss dann neben den Header aus der Standardbibliothek: #include<stdio.h> #include<stdlib.h> auch den selbsterstellten Header matrix.h einbinden: #include"matrix.h" damit innerhalb von matrix.c der Datentyp Matrix bekannt ist. Weitere Module erstellen Wir möchten nun die Bibliothek um eine Funktion erweitern, die Matrix-Vektor-Multiplikation mit unserem Datentyp realisiert. Als n-Vektor verstehen wir ein double-Array der Länge n: double *x = (double*)malloc( n * sizeof(double) ); Die Funktion können wir also folgendermaßen deklarieren: void matrix_vector( Matrix *A, double *x, double *y); Dabei soll eine Matrix A mit einem Vektor x multipliziert werden. Das Ergebnis wird in y gespeichert. Die Funktion soll dann in einer neuen Datei matrix_vektor.h deklariert werden. Header Guards Um die Funktion void matrix_vector( Matrix *A, double *x, double *y); in matrix _ vektor.h zu deklarieren, müssen wir den Datentyp Matrix kennen. Wir möchten also matrix.h einbinden, um sicher zu sein, dass der Datentyp bekannt ist. Um bei mehrfachem Einbinden, mehrfache Definitionen von Funktion und Datentypen zu verhindern, baut man sogenannte Header Guards ein: #ifndef EXAMPLE_H #define EXAMPLE_H //some code #endif // EXAMPLE_H example.h Header Guards Header Guards (oder auch Include Guard, Include Wachter ) sorgen dafür, dass eine Headerdatei nur einmal eingebunden wird. Für jede Headerdatei wird ein eindeutiges Makro verwendet, hier: EXAMPLE_H. #ifndef EXAMPLE_H fragt ob EXAMPLE_HH schon definiert wurde, falls ja ist die Datei schon kompiliert und der Präprozessor überspringt den #ifndef ... #endif Block. #ifndef EXAMPLE_H #define EXAMPLE_H //some code #endif // EXAMPLE_H matrix.h mit Header Guard Die fertige Headerdatei matrix.h mit Header Guard, könnte dann so aussehen: 1 2 #ifndef MATRIX_H #define MATRIX_H 3 4 5 6 7 8 9 struct double_matrix{ double **index; double *values; int n, m; }; typedef struct double_matrix Matrix; 10 11 12 13 int initDoubleMatrix( Matrix *A, int m, int n); void freeDoubleMatrix( Matrix *A ); void printDoubleMatrix( Matrix *A); 14 15 #endif // MATRIX_H matrix_vektor.h Für die Matrix-Vektor-Multiplikation vereinbaren wir den Header matrix_vektor.h: 1 2 #ifndef MATRIX_VECTOR_H #define MATRIX_VECTOR_H 3 4 #include "matrix.h" 5 6 void matrix_vector( Matrix *A, double *x, double *res); 7 8 #endif // MATRIX_VECTOR_H matrix_vektor.c Die tatsächliche Implementierung der Matrix-Vektor-Multiplikation erfolgt dann in matrix_vektor.c: 1 2 3 #include<stdio.h> #include<stdlib.h> #include "matrix.h" 4 5 6 7 8 9 10 11 12 void matrix_vector( Matrix *A, double *x, double *res) { int i, j; for( i = 0; i < A->m; i++) res[i] = 0.; for( i = 0; i < A->m; i++) for( j = 0; j < A->n; j++) res[i] += A->index[i][j] * x[j]; } Bibliothek erstellen Insgesammt haben wir nun die Module I matrix.c mit Header matrix.h I matrix_vektor.c mit Header matrix_vektor.h Wir wollen nun diese Module zu einer Bibliothek verbinden. Zunächst werden die Objektdateien matrix.c und matrix_vektor.o erstellt über $ gcc -c matrix.c $ gcc -c matrix_vektor.c Mit ar -r können sie diese zu einer Bibliothek libDoubleMatrix.a zusammenfassen: $ ar -r libDoubleMatrix.a matrix.o matrix_vector.o Bibliothek verwenden Möchten man nun die Funktionalität von libDoubleMatrix.a in einem Programm main.c verwenden, müssen zunächst beide Header eingebunden werden: #include "matrix.h" #include "matrix_vektor.h" und linkt das Programm durch $ gcc -o prog_name main.c libDoubleMatrix.a Bibliothek verwenden Alternativ kann man einen Header doubleMatrix.h erstellen, der die Präprozessor-Direktiven : #include "matrix.h" #include "matrix_vektor.h" enthält. Dann muss im Hauptprogramm nur doubleMatrix.h eingebunden werden. ist die Bibliothek in einem anderen Verzeichnis untergebracht, muss das beim Linken mit der Option -L angegeben werden und die Bibliothek wird mit -l angegeben. $ gcc -o prog_name main.c -L<Verzeichnis> -lDoubleMatrix Der Präfix lib und der Suffix .a werden dabei weggelassen. einfaches Makefile einfaches Makefile, um die Bibliothek libDoubleMatrix.a zu erstellen: 1 2 install: matrix.o matrix_vector.o ar -r libDoubleMatrix.a matrix.o matrix_vector.o 3 4 5 matrix.o: matrix.c gcc -c matrix.c 6 7 8 matrix_vector.o: matrix_vector.c gcc -c matrix_vector.c 9 10 11 clear: rm -f *.o *.a Gliederung Organisatorisches zur Klausur eigene Bibliotheken Zeiger und Felder (Wdh.) Zeichenketten (Wdh.) Strukturierte Datentypen (Wdh.) Funktionen und Funktionsaufrufe (Wdh.) Dynamische Speicherverwaltung (Wdh.) Ausdrücke, Anweisungen (Wdh.) Operatoren (Wdh.) Typkonversion (Wdh.) Testfragen Zeigertypen und -variablen Für jeden Typ typename erhalten wir einen Zeigertyp typename * Ein Zeiger ist eine Variable von einem Zeigertyp. Der Typ typename gibt an, auf welchen Typ ein Zeiger verweist: typename *identifier; Beispiele: int *p; double *q; /* pointer to int */ /* pointer to double */ Der Adressoperator & Jedes Objekt (Variablen, Konstanten, Funktionen, etc.) belegt Platz im Speicher. Der Speicher eines Systems ist adressiert, d. h. jede Position im Speicher hat eine Adresse. Jedem Objekt ist damit eine eindeutige Adresse zugeordnet. Die Adresse eines Objekts ermitteln wir mit dem Adressoperator &: double x; &x; Der Typ des Ausdrucks &x ist ein Zeigertyp: double *p = &x; Initialisierung von Zeigern In einem Zeiger kann die Adresse eines Objekts gespeichert werden. double x; double *p = &x; int a; int *q = &a; Die Typen der Ausdrücke links und rechts einer Zuweisung müssen übereinstimmen! Ein besonderer Zeigerwert ist die Null: double *p = 0; int *q = 0; Jeder Zeiger (egal welchen Typs) kann mit diesem Wert belegt werden. Dereferenzieren von Zeigern Soll auf das Objekt zugegriffen werden, auf das ein Zeiger verweist, spricht man vom Dereferenzieren des Zeigers. Mit dem Inhaltsoperator * spricht man den Speicherbereich an, auf den ein Zeiger verweist: double x = 1.; double *p = &x; *p = 2.; printf("x = %f\n", x); /* x = 2. */ Das Dereferenzieren eines ungültigen Zeigers ist verboten und führt in der Regel zum sofortigen Programmabbruch: double *p = 0; *p; /* error: p not properly initialized */ Übung zu Zeigern Sei a vom Typ int und p vom Typ int *. Welche der folgenden Anweisungen lässt p auf a zeigen? 1. *p = a; 2. p = *a; 3. &p = a; 4. p = &a; Übung zu Zeigern Sei a vom Typ int und p vom Typ int *. Welche der folgenden Anweisungen lässt p auf a zeigen? 1. *p = a; 2. p = *a; 3. &p = a; 4. p = &a; Übung zu Zeigern Sei b vom Typ int und q vom Typ int *. Welche der folgenden Anweisungen weist b den Wert des Objekts zu, auf das q zeigt? 1. *q = b; 2. &b = q; 3. b = *q; 4. b = &q; Übung zu Zeigern Sei b vom Typ int und q vom Typ int *. Welche der folgenden Anweisungen weist b den Wert des Objekts zu, auf das q zeigt? 1. *q = b; 2. &b = q; 3. b = *q; 4. b = &q; Felder Für jeden Datentyp (elementar oder benutzerdefiniert) kann ein Feld angelegt werden. Ein Feld ist eine zusammenhängende Folge von Objekten gleichen Typs: a[0] a[1] a[2] 120 124 128 132 136 Zur Vereinbarung muss neben Typ und Name die Länge des Feldes angegeben werden: int a[3]; Zugriff auf Feldelemente Auf die einzelnen Elemente eines Felds wird über einen Index zugegriffen: int a[3]; a[0], a[1], a[2]; /* access all entries */ In C beginnen Indizierungen immer mit dem Wert 0. Der letzte gültige Index eines Felds der Länge N ist dann N − 1. Achtung: Beim Elementzugriff findet keine Bereichsüberprüfung statt: int a[3]; a[99]; /* wrong, invalid index */ Zusammenhang von Zeigern und Felder Sei int a[3]; ein Feld. Sein Name a ist ein Ausdruck vom Typ int *. Sein Wert entspricht der Adresse des ersten Elements von a: a == &a[0] Die folgenden Zuweisungen sind daher identisch: int *p = &a[0]; int *p = a; Unterschiede von Zeigern und Feldern Ein Zeiger ist eine Variable, deren Wert sich (durch Zuweisungen, Inkrementoperatoren, etc.) ändern kann: int a[3]; int *p = a; ++p; /* p points to a[0] */ Ein Feldname ist keine Variable. Ausdrücke wie a = p oder ++a sind daher nicht erlaubt. Gliederung Organisatorisches zur Klausur eigene Bibliotheken Zeiger und Felder (Wdh.) Zeichenketten (Wdh.) Strukturierte Datentypen (Wdh.) Funktionen und Funktionsaufrufe (Wdh.) Dynamische Speicherverwaltung (Wdh.) Ausdrücke, Anweisungen (Wdh.) Operatoren (Wdh.) Typkonversion (Wdh.) Testfragen Zeichenketten Eine Zeichenkette ist eine zusammenhängende Folge von char-Einträgen. char str[32]; sprintf(str, "hello"); stdio.h */ /* sprintf is contained in Ein besonderes Zeichen, das Terminierungszeichen ’\0’, markiert das Ende einer Zeichenkette: 'h' 'e' 'l' 'l' 'o' '\0''X' 'Y' 'Z' 0 1 2 3 4 5 6 7 8 Stringkonstanten Stringkonstanten sind Zeichenketten, die in doppelte Anführungszeichen gesetzt sind: "hello" Ein Feld vom Typ char lässt sich mit einer Stringkonstanten initialisieren: char str[] = "hello"; Bei der Initialisierung eines char-Feldes mit einer Stringkonstanten wird automatisch das Terminierungszeichen hinzugefügt. Das Feld str belegt also 6 Byte: sizeof(str) = 6. Stringhandling Die C-Standardbibliothek stellt eine Reihe von Funktionen zum Arbeiten mit Strings bereit, z. B.: size_t strlen(const char *str); char *strcpy(char *dest, const char *src); char *strncpy(char *dest, const char *src, size_t n); Alle hier beschriebenen Funktionen sind in der Datei string.h enthalten. Übung zu Vereinbarungen Welche der folgenden Vereinbarungen sind korrekt? Aus welchen Gründen sind die übrigen falsch? 1. double größe; 2. unsigned int u; 3. char c = ’z’; 4. char str[] = ’hallo’; 5. double b[ 2 ] = { 9., 3. }; Übung zu Vereinbarungen Welche der folgenden Vereinbarungen sind korrekt? Aus welchen Gründen sind die übrigen falsch? 1. double größe; 2. unsigned int u; 3. char c = ’z’; 4. char str[] = ’hallo’; 5. double b[ 2 ] = { 9., 3. }; Gliederung Organisatorisches zur Klausur eigene Bibliotheken Zeiger und Felder (Wdh.) Zeichenketten (Wdh.) Strukturierte Datentypen (Wdh.) Funktionen und Funktionsaufrufe (Wdh.) Dynamische Speicherverwaltung (Wdh.) Ausdrücke, Anweisungen (Wdh.) Operatoren (Wdh.) Typkonversion (Wdh.) Testfragen Vereinbarung von Strukturen Strukturen gehören zu den benutzerdefinierten Datentypen. Eine Struktur wird definiert durch Angabe des Schlüsselworts struct, ihres Namens und ihrer Elemente in geschweiften Klammern mit abschließendem Strichpunkt. Beispiel: struct Student { char name[64]; char matrikelnr[16]; }; Variablen von strukturierten Datentypen Eine Variable vom Typ struct Student wird wie folgt definiert: struct Student student; Mit der Definition kann eine Variable von strukturiertem Datentyp initialisiert werden: struct Student student = {"Max Mustermann", "1234567890"}; Zeiger auf Objekte von strukturiertem Datentyp kann man so anlegen: struct Student *pStudent = &student; Merke: Das Schlüsselwort struct muss immer mit angegeben werden. Auswahloperatoren Die Auswahloperatoren . und -> erlauben den Zugriff auf die Elemente einer Struktur. Beispiel: struct Student student; sprintf(student.name, "Max Mustermann"); sprintf(student.matrikelnr, "1234567890"); Der Auswahloperator -> darf nur auf Zeiger angewandt werden: student->name; /* equals: (*student).name */ Gliederung Organisatorisches zur Klausur eigene Bibliotheken Zeiger und Felder (Wdh.) Zeichenketten (Wdh.) Strukturierte Datentypen (Wdh.) Funktionen und Funktionsaufrufe (Wdh.) Dynamische Speicherverwaltung (Wdh.) Ausdrücke, Anweisungen (Wdh.) Operatoren (Wdh.) Typkonversion (Wdh.) Testfragen Funktionen vereinbaren Bei Funktionen unterscheidet man zwischen Deklaration und Definition: I Die Deklaration vereinbart lediglich Rückgabetyp, Namen und Parameterliste der Funktion. I Erst die Definition legt die Aktionen fest, die bei Funktionsaufruf ausgeführt werden sollen. Übung zu Funktionsdeklarationen Wie lauten die Deklarationen der folgenden Funktionen? I square ist eine Funktion, die eine double-Größe erwartet und ihr Quadrat zurückgibt: Übung zu Funktionsdeklarationen Wie lauten die Deklarationen der folgenden Funktionen? I square ist eine Funktion, die eine double-Größe erwartet und ihr Quadrat zurückgibt: double square(double x); Übung zu Funktionsdeklarationen Wie lauten die Deklarationen der folgenden Funktionen? I sort ist eine Funktion, die ein Feld elements von Integer-Zahlen und dessen Länge N als Parameter erhält und einen Zeiger auf das erste Feldelement zurückgibt. Übung zu Funktionsdeklarationen Wie lauten die Deklarationen der folgenden Funktionen? I sort ist eine Funktion, die ein Feld elements von Integer-Zahlen und dessen Länge N als Parameter erhält und einen Zeiger auf das erste Feldelement zurückgibt. int *sort(int elements[], int N); Übung zu Funktionsdeklarationen Wie lauten die Deklarationen der folgenden Funktionen? I innerProduct ist eine Funktion, die zwei konstante Zeiger auf struct Vector übergeben bekommt und eine double-Größe zurückgibt: Übung zu Funktionsdeklarationen Wie lauten die Deklarationen der folgenden Funktionen? I innerProduct ist eine Funktion, die zwei konstante Zeiger auf struct Vector übergeben bekommt und eine double-Größe zurückgibt: double innerProduct(const struct Vector *x, const struct Vector *y); Funktionszeiger Ist der Prototyp einer Funktion (bestehend aus Rückgabetyp, Namen und Parameterliste) bekannt, lässt sich ein Zeiger auf die Funktion definieren. Beispiel: double square(double x); double (*f)(double) = square; Um eine Funktion über einen Zeiger auszuwerten, gibt es mehrere Möglichkeiten: double x = f(2.); double x = (*f)(2.); /* x = 4. */ /* x = 4. */ Übergabe per Wert Betrachten wir die folgende einfache Funktion: void setToZero(double x) { x = 0; } Beim Funktionsaufruf double x = 1.; setToZero(x); printf("x = %f\n", x); /* x = 1. */ wird lediglich der Funktionswert des Arguments an die Funktion übergeben. Übergabe per Referenz Als Übergabe per Referenz bezeichnet man die Übergabe von Zeigern an Funktionen: void setToZero(double *x) { *x = 0; } ... double x = 1.; setToZero(&x); printf("x = %f\n", x); /* x = 0. */ Zwar wird wieder eine Kopie an die Funktion übergeben (die des Zeigers!). Die Funktion greift aber auf den Speicherplatz von x zu, und die lokalen Änderungen werden im gesamten Programm wirksam. Übergabe per Referenz Eine Funktion erhält bei ihrem Aufruf Kopien der Argumente (call by value). Das gilt auch für den Fall, dass Zeiger an eine Funktion übergeben werden: void swap(int *a, int *b); int main(void) { int a = 1, b = 2; swap(&a, &b); return 0; } Jede Kopie eines Zeigers verweist aber auf die gleiche Speicheradresse. Wird diese beschrieben, ändert sich der Zustand der dort abgelegten Variable. Programmbeispiel Variablentausch Quelltext 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdio.h> void swap ( int *p, int *q ) { int temp; temp = *p; *p = *q; *q = temp; } int main ( void ) { int a = 1, b = 2 ; int *p = &a, *q = &b; printf( "a = %d, b = %d\n", a, b ); swap( p, q ); printf( "a = %d, b = %d\n", a, b ); return 0; } /* a = 1, b = 2 */ /* a = 2, b = 1 */ Übergabe per Referenz II Felder werden stets per Referenz (nicht als Kopie) an Funktionen übergeben: void initialize(double x[], int len) { int i; for(i = 0; i < len; ++i) x[i] = 0.; } int main(void) { double x[3]; initialize(double x[3]); return 0; } / * x = 0 */ Die obige Funktion ändert tatsächlich die in x gespeicherten Werte. Die Funktion main Ein C-Programm besteht immer aus mindestens einer Funktion: der Funktion main. Sie markiert den Programmanfang. Wir verwenden als Rückgabetyp von main immer int. Die Parameterliste von main kann leer sein: int main(void); Der Funktion können aber auch Parameter übergeben werden: int main(int argc, char *argv[]); → Das erste Argument gibt die Anzahl der Parameter an (inklusive des Namens des Executables). → Beim zweiten Argument argv handelt es sich um ein Feld von Zeichenketten. Parameter übergeben an main 1 #include <stdio.h> 2 3 4 int main(int argc, char *argv[]) { int i; 5 for(i=0; i < argc; i++) { printf("argv[%d] = %s ", i, argv[i]); printf("\n"); } return 0; 6 7 8 9 10 11 } Rückgabewerte von main Der Rückgabewert von main gibt üblicherweise Auskunft über den Zustand des Programms bei Programmende: → Typischerweise bedeutet einer der Werte 0 oder EXIT_SUCCESS, dass das Programm normal beendet wurde: return 0; return EXIT_SUCCESS; → Ein Rückgabewert ungleich 0 oder EXIT_FAILURE hingegen weist auf einen fehlerhaften oder ungewöhnlichen Abbruch hin. return 1; return EXIT_FAILURE; Die Konstanten EXIT_SUCCESS und EXIT_FAILURE sind in der Datei stdlib.h definiert. Die Datei wird eingebunden mit: #include <stdlib.h> Gliederung Organisatorisches zur Klausur eigene Bibliotheken Zeiger und Felder (Wdh.) Zeichenketten (Wdh.) Strukturierte Datentypen (Wdh.) Funktionen und Funktionsaufrufe (Wdh.) Dynamische Speicherverwaltung (Wdh.) Ausdrücke, Anweisungen (Wdh.) Operatoren (Wdh.) Typkonversion (Wdh.) Testfragen Funktionen zur dynamischen Speicherverwaltung Die folgenden Methoden zur dynamischen Speicherverwaltung sind in der Datei stdlib.h enthalten: /* allocate memory */ void *malloc(size_t size); /* allocate and initialize memory */ void *calloc(size_t number, size_t size); /* deallocate memory */ void free(void *ptr); /* try to resize memory at given address */ void *realloc(void *ptr, size_t size); Anfordern von Speicher In der Regel müssen wir Speicher für eine Zahl n von Objekten von bekanntem Typ typename anlegen. Der Speicherbedarf hierfür beträgt gerade n*sizeof(typename); Beispiele (Speicher für 100 Integer-Werte reservieren): int *p = (*int) malloc(100*sizeof(int)); int *q = (*int) calloc(100,sizeof(int)); Was ist der Unterschied zwischen malloc und calloc? Fehlerbehandlung Der Rückgabewert der Methoden zum Anfordern von Speicher void *malloc(size_t size); void *calloc(size_t number, size_t size); void *realloc (void *ptr, size_t size); ist jeweils ein Zeiger vom Typ void *. Im Fehlerfall, d. h. konnte der Speicher nicht angelegt werden, hat der Rückgabewert den Wert Null,sprich der Zeiger ist ungültig. Beispiel: size_t size = 4; void *p = malloc(size); if(!p) printf("error"); Zugriff auf dynamisch erzeugten Speicher Mit malloc oder calloc angelegter Speicher ist zusammenhängend. Auf die Elemente des erzeugten Speicherblocks wird über den Operator [] und einen Index zugegriffen. Beispiel: int i, size = 4; int *p = (int*) malloc(size*sizeof(int)); assert(p); /* remember to include assert.h */ for(i = 0; i < size; ++i) p[i] = 0; Freigeben von Speicher Dynamisch angelegter Speicher muss freigegeben werden, wenn er nicht mehr verwendet wird und anderen Prozessen zur Verfügung stehen soll. Die Funktion void free(void *ptr); gibt den Speicherbereich, auf den ptr zeigt und der über malloc oder calloc angelegt wurde, wieder frei. Beispiel: int *p = (int*) malloc(4*sizeof(int)); ... free(p); Auf freigegebenen Speicher darf nicht mehr zugegriffen werden. Gliederung Organisatorisches zur Klausur eigene Bibliotheken Zeiger und Felder (Wdh.) Zeichenketten (Wdh.) Strukturierte Datentypen (Wdh.) Funktionen und Funktionsaufrufe (Wdh.) Dynamische Speicherverwaltung (Wdh.) Ausdrücke, Anweisungen (Wdh.) Operatoren (Wdh.) Typkonversion (Wdh.) Testfragen Ausdrücke Ein Ausdruck ist eine Kombination aus Variablen, Konstanten, Operatoren und Rückgabewerten von Funktionen. I Die Auswertung eines Ausdrucks ergibt einen Wert I ein Ausdruck hat immer einen (Daten-) Typ. I Ausdrücke können durch Operatoren zu neuen Ausdücken zusammengesetzt werden. I Ein Ausdruck wird durch ein nachfolgendes Semikolon zur Anweisung. Der Zuweisungsausdruck y = sqrt(x) wird zur Zuweisungsanweisung y = sqrt(x);. Ausdrücke Beispiel: int a, b; double x, y; a = 4 + 3; x = 4.0; y = sqrt(x); I Konstante 4 hat den Wert 4 und den Typ int. Die Konstante 4.0 hat den Wert 4 und den Typ double. I der Ausdruck 4 + 3 hat den Wert 7 und den Typ int. I Die Variable b den Typ int und unbestimmten Wert. I Der Zuweisungsausdruck (Beispiel (y = sqrt(x))) hat den Typ und den Wert des linken Operanten nach Auswertung der Zuweisung. Seiteneffekte Jeder Zuweisungsausdruck darf selbst wieder in einem Ausdruck auftauchen: int a = 1, b; b = (a += 1) + 1; /* a = 2, b = 3 */ Solche verschachtelten Ausdrücke verursachen sog. Seiten- oder Nebeneffekte, d. h. neben der Auswertung des Ausdrucks wird durch die Zuweisung auch der Wert einer Variablen geändert. Seiteneffekte machen den Code schwer lesbar und sind damit potentielle Fehlerquellen. Beispiel (Inkrement-Operatoren): int i = j = k = i, j, k; 1; i++; /* j = 1, side effect: i = 2 */ ++i; /* k = 3, side effect: i = 3 */ Gliederung Organisatorisches zur Klausur eigene Bibliotheken Zeiger und Felder (Wdh.) Zeichenketten (Wdh.) Strukturierte Datentypen (Wdh.) Funktionen und Funktionsaufrufe (Wdh.) Dynamische Speicherverwaltung (Wdh.) Ausdrücke, Anweisungen (Wdh.) Operatoren (Wdh.) Typkonversion (Wdh.) Testfragen Unäre, binäre und ternäre Operatoren Durch Operatoren werden Ausdrücke (Argumente, Operanden) verknüpft und zu zusammengesetzten Ausdrücken. Operatoren können ein, zwei oder drei Argumente haben. Ein Operator heißt. . . unär, falls der Operator auf nur einen Operanden wirkt. Beispiel: +, - (Vorzeichenoperatoren), binär, falls der Operator auf zwei Operanden wirkt. Beispiel: +, -, *, / % (arithmetische Operatoren), ternär, falls der Operator auf drei Operanden wirkt. Beispiel: ?: (Konditionaloperator). Priorität und Assoziativität von Operatoren Typ und Wert eines zusammengesetzten Ausdrücks müssen eindeutig bestimmt sein. Ihre Auswertung erfolgt unter Berücksichtigung von. . . Priorität Rangfolge, die festlegt, in welcher Reihenfolge Operatoren innerhalb eines Ausdrucks ausgewertet werden Assoziativität gibt an, in welcher Richtung Operatoren und Operanden zusammengefasst werden Operatoren in C Operator Assoziativität () [] -> . ! ∼ ++ −− + - * & (type) sizeof * / % + << >> < <= > >= == != & ˆ | && || ?: = += -= *= /+ %= &= ˆ= |= <<= >>= , von links nach rechts von rechts nach links von links nach rechts von links nach rechts von links nach rechts von links nach rechts von links nach rechts von links nach rechts von links nach rechts von links nach rechts von links nach rechts von links nach rechts von rechts nach links von rechts nach links von links nach rechts Tabelle : Operatoren absteigend nach Priorität gelistet1 1 Quelle: Kernighan, Ritchie: Programmieren in C Beispiele: Priorität und Assoziativität I Das Assoziativgesetz, Punkt vor Strich, soll gelten. Die arithmetischen Operatorn +,- binden schwächer als *,/. Der Audruck 1.0 + 2.0 * 3.0 hat den Wert 7. I Der Klammeroperator () hat höhere Prioriät als +,-,*,/. Der Ausdruck (1.0 + 2.0 ) * 3.0 hat den Wert 9. I Vergleichsoperatoren < werden von links nach rechts gelesen. Beispiel: double a = .1, b = .2, c = .3; printf("%d\n", a < b < c ); printf("%d\n", a < ( b < c ) ); Beispiele: Priorität und Assoziativität I Das Assoziativgesetz, Punkt vor Strich, soll gelten. Die arithmetischen Operatorn +,- binden schwächer als *,/. Der Audruck 1.0 + 2.0 * 3.0 hat den Wert 7. I Der Klammeroperator () hat höhere Prioriät als +,-,*,/. Der Ausdruck (1.0 + 2.0 ) * 3.0 hat den Wert 9. I Vergleichsoperatoren < werden von links nach rechts gelesen. Beispiel: double a = .1, b = .2, c = .3; printf("%d\n", a < b < c ); printf("%d\n", a < ( b < c ) ); \\ 0 \\ 1 Zuweisungsoperator Zuweisungsoperatoren haben mit die geringste Priorität aller Operatoren. So wird sichergestellt, dass zuerst der Ausdruck auf der rechten Seite der Zuweisung ausgewertet wird. Beispiel: double x = (1.0 + 2.0 ) * 3.0 ; Entscheidend ist, dass auf der linken Seite einer Zuweisung nur Variablen zugelassen sind: x = 1. /* Okay. */ 1. = 1. /* Error! */ a*x = 1. /* Error! */ Zuweisungen sind nicht kommutativ! Die Variable, die mit einem Wert belegt werden soll, steht links: a = b b = a /* assign value of b to a */ /* assign value of a to b */ Gliederung Organisatorisches zur Klausur eigene Bibliotheken Zeiger und Felder (Wdh.) Zeichenketten (Wdh.) Strukturierte Datentypen (Wdh.) Funktionen und Funktionsaufrufe (Wdh.) Dynamische Speicherverwaltung (Wdh.) Ausdrücke, Anweisungen (Wdh.) Operatoren (Wdh.) Typkonversion (Wdh.) Testfragen Implizite Typkonversion Operatoren können Variablen verschiedener Datentypen verbinden. int x = 1; double y = 2.5; double sumDouble = x + y; int sumInt = x + y; printf("sumInt = %d\n", sumInt); /* Output: sumInt = 3 */ printf("sumDouble = %lf\n", sumDouble); /* Output: sumDouble = 3.500000 */ I x + y hat den Datentyp double, bei der Addition wird x in ein double konvertiert. I sumInt hat den Typ int, d.h. x+y wird in ein int konvertiert. Implizite Typkonversion I Haben die Operanden eines arithmetischen Operators den gleichen Typ, so ist auch das Ergebnis von diesem Datentypen. I Sind die Operanden von unterschiedlichem Typ, so wird der Operand mit ”ungenauerem” Typ, zuerst zum ”genaueren” Typ konvertiert, bspw. ist 1 / 5. eine Konstante vom Typ double. I Konvertierung von double nach int erfolgt durch Abschneiden, nicht durch Rundung! int n = 3.7; printf("n = %d\n", n); /* Output: n = 3 */ Implizite Typkonversion I Klammern entscheiden die Reihenfolge in der die Operatoren angewendet werden. double double double double x_1 x_2 x_3 x_4 = = = = printf("x_1 printf("x_2 printf("x_3 printf("x_4 = = = = 2 / 2 / 10. 10. 4; 4.; * 2 / 4; * (2 / 4); %lf\n", %lf\n", %lf\n", %lf\n", x_1); x_2); x_3); x_4); Implizite Typkonversion I Klammern entscheiden die Reihenfolge in der die Operatoren angewendet werden. double double double double x_1 x_2 x_3 x_4 = = = = printf("x_1 printf("x_2 printf("x_3 printf("x_4 I = = = = 2 / 2 / 10. 10. 4; 4.; * 2 / 4; * (2 / 4); %lf\n", %lf\n", %lf\n", %lf\n", x_1); x_2); x_3); x_4); Der Code produziert den Output: x_1 x_2 x_3 x_4 = = = = 0.000000 0.500000 5.000000 0.000000 Explizite Typkonversion I Man kann dem Compiler mitteilen, in welcher Form eine Variable interpretiert werden muss. I Man stellt dazu den Ziel-Typ in Klammern voran double x_1 = (double) (2 / 4); double x_2 = (double) 2 / 4; double x_3 = 2 / (double) 4; Explizite Typkonversion I Man kann dem Compiler mitteilen, in welcher Form eine Variable interpretiert werden muss. I Man stellt dazu den Ziel-Typ in Klammern voran double x_1 = (double) (2 / 4); double x_2 = (double) 2 / 4; double x_3 = 2 / (double) 4; I Die Variablen haben den Wert: x_1 = 0.000000 x_2 = 0.500000 x_3 = 0.500000 Gliederung Organisatorisches zur Klausur eigene Bibliotheken Zeiger und Felder (Wdh.) Zeichenketten (Wdh.) Strukturierte Datentypen (Wdh.) Funktionen und Funktionsaufrufe (Wdh.) Dynamische Speicherverwaltung (Wdh.) Ausdrücke, Anweisungen (Wdh.) Operatoren (Wdh.) Typkonversion (Wdh.) Testfragen Aufgabe 1: Sei a vom Typ int und p vom Typ int * . Welche der folgenden Anweisungen lässt p auf a zeigen? 1. *p = a; 2. p = *a; 3. &p = a; 4. p = &a; Aufgabe 1: Sei a vom Typ int und p vom Typ int * . Welche der folgenden Anweisungen lässt p auf a zeigen? 1. *p = a; 2. p = *a; 3. &p = a; 4. p = &a; Aufgabe 2: Sei a vom Typ int und p vom Typ int * . Welche der folgenden Anweisungen weist a den Wert des Objekts zu, auf das p zeigt? 1. *p = a; 2. &a = p; 3. a = *p; 4. a = &p; Aufgabe 1: Sei a vom Typ int und p vom Typ int * . Welche der folgenden Anweisungen lässt p auf a zeigen? 1. *p = a; 2. p = *a; 3. &p = a; 4. p = &a; Aufgabe 2: Sei a vom Typ int und p vom Typ int * . Welche der folgenden Anweisungen weist a den Wert des Objekts zu, auf das p zeigt? 1. *p = a; 2. &a = p; 3. a = *p; 4. a = &p; Aufgabe 3: Durch int a[5]; wurde ein Feld von Integer-Werten vereinbart. Sei p vom Typ int * . Eine der folgenden Zuweisungen ist zu p = &a[2] äquivalent, welche? 1. p = a[2] 2. &p = a[2] 3. *p = a + 2 4. p = a + 2 Aufgabe 3: Durch int a[5]; wurde ein Feld von Integer-Werten vereinbart. Sei p vom Typ int * . Eine der folgenden Zuweisungen ist zu p = &a[2] äquivalent, welche? 1. p = a[2] 2. &p = a[2] 3. *p = a + 2 4. p = a + 2 Aufgabe 4: Zum Speichern einer Matrix werde die Variable double **A verwendet. Welche der folgenden Ausdrücke sind zu A[i][j] äquivalent? 1. *(A[i]+j) 2. *&A[i][j] 3. *(*(A+i)+j) 4. **(A+i+j) Aufgabe 3: Durch int a[5]; wurde ein Feld von Integer-Werten vereinbart. Sei p vom Typ int * . Eine der folgenden Zuweisungen ist zu p = &a[2] äquivalent, welche? 1. p = a[2] 2. &p = a[2] 3. *p = a + 2 4. p = a + 2 Aufgabe 4: Zum Speichern einer Matrix wurde die Variable double **A verwendet. Welche der folgenden Ausdrücke sind zu A[i][j] äquivalent? 1. *(A[i]+j) 2. *&A[i][j] 3. *(*(A+i)+j) 4. **(A+i+j) Autoren Autoren die an diesem Skript mitgewirkt haben: I 2011–2014 : Christoph Gersbacher I 2014–2015 : Patrick Schön This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) License. http://creativecommons.org/ licenses/by-sa/4.0/legalcode