Grundlagen der Programmiersprache C für Studierende der Naturwissenschaften Teil 4: Funktionen, Header und Bibliotheken Patrick Schreier Abteilung für Angewandte Mathematik Vorlesung vom 11. Mai 2015 Gliederung Funktionen Aufteilen von Programmteilen auf mehrere Dateien Bibliotheken Motivation I Gegeben seien natürliche Zahlen n, k ∈ N mit k ≤ n. Der Binominalkoeffizient „n über k “ n k entspricht der Anzahl der k -elementigen Teilmengen einer Menge der Mächtigkeit n. Der Binominalkoeffizient lässt sich zum Beispiel nach der folgenden Formel berechnen: n! n . = k k !(n − k )! Motivation II Zwar wissen wir seit letzter Woche, wie die Fakultät n! ausgewertet werden kann: int i, n = ...; double factorial = 1.; for(i = 1; i <= n; ++i) factorial *= i; Zur Berechnung des Binominalkoeffizienten nach der Formel n! n = k k !(n − k )! müssen allerdings drei Fakultäten ausgewertet werden. Natürlich wollen wir die obige for-Schleife nicht drei Mal schreiben müssen. Gliederung Funktionen Aufteilen von Programmteilen auf mehrere Dateien Bibliotheken Funktionen Funktionen sind Unterprogramme, die einzelne, abgegrenzte Aufgaben erfüllen. Funktionen können überall im Programm aufgerufen werden und können einen Wert zurückgeben. (C-)Programme werden in der Regel so weit wie möglich in Funktionen gegliedert: I Die Aufteilung in Funktionen erhöht die Lesbarkeit und Sicherheit des Programms. I Funktionen können in separate Dateien (Header, Bibliotheken) ausgelagert und wiederverwendet werden. Definition von Funktionen I Mit der Definition einer Funktion werden die Anweisungen festgelegt, die beim Funktionsaufruf ausgeführt werden sollen. Syntax: return-type function-name(parameter-list) { function-body } Zur Definition einer Funktion gehört die Angabe. . . I ihres Rückgabetyps, I Funktionsnamens, I ihrer Parameterliste, I sowie des sog. Funktionenrumpfs, das sind die Funktionsanweisungen in einem Block. Beispiele für Funktionen Beispiel (Quadrat einer Gleitpunktzahl): double sqr(double x) { return x*x; } Beispiel (Fakultät): double factorial(int n) { int i; double result = 1.; for(i = 1; i <= n; ++i) result *= i; return result; } Definition von Funktionen II Syntax: return-type function-name(parameter-list) { function-body } → Funktionen können einen Wert zurückgeben. Der Typ des Rückgabewerts ist der gleich dem Rückgabetyp return-type der Funktion. Definition von Funktionen II Syntax: return-type function-name (parameter-list) { function-body } → Funktionen können einen Wert zurückgeben. Der Typ des Rückgabewerts ist der gleich dem Rückgabetyp return-type der Funktion. → Für den Namen function-name einer Funktion gelten dieselben Regeln wie für Variablennamen (s. Teil 2). Definition von Funktionen II Syntax: return-type function-name( parameter-list ) { function-body } → Funktionen können einen Wert zurückgeben. Der Typ des Rückgabewerts ist der gleich dem Rückgabetyp return-type der Funktion. → Für den Namen function-name einer Funktion gelten dieselben Regeln wie für Variablennamen (s. Teil 2). → Die Parameterliste besteht aus einer kommaseparierten Liste von Typen und Namen für die Argumente einer Funktion. Sie kann auch leer sein (s. u.). Der Prototyp einer Funktion Rückgabetyp, Name und Parameterliste einer Funktion bilden den Prototyp der Funktion: return-type function-name(parameter-list); Der Prototyp enthält sämtliche Informationen, die beim Kompilieren für die Syntaxprüfung erforderlich sind (siehe „Deklaration von Funktionen“ unten). Beispiele: double sqr(double x); double factorial(int n); int binomial(int n, int k); Der Funktionsrumpf I Der Rumpf einer Funktion besteht aus einer oder mehreren Anweisungen, die in einem Block {...} zusammengefasst sind. I Bei jedem Funktionsaufruf werden die Anweisungen des Funktionsrumpfs ausgeführt. I Innerhalb einer Funktionsrumpf dürfen auch Variablen definiert werden. I Eine Funktion wird in der Regel (falls die Funktion einen Rückgabewert hat) mit einer return-Anweisung beendet. Die return-Anweisung Die return-Anweisung beendet eine Funktion: return expr; Dabei ist expr ein Ausdruck, der sog. Rückgabewert der Funktion. Sein Typ muss mit dem Rückgabetyp der Funktion übereinstimmen. Beispiel: double sqr(double x) { return x*x; } Funktionsaufruf und -wert Eine Funktion wird über ihren Namen mit allen nötigen Parametern aufgerufen: function-name(parameter-list); Funktionsaufrufe gehören zu den elementaren Ausdrücken (s. Teil 3). Typ und Wert des Ausdrucks stimmen mit Rückgabetyp und -wert der Funktion zusammen. Beispiele: double x = sqr(5.); int a = binomial(3, 2); /* x = 25. */ / * a = 3 */ Vereinbarung von Funktionen Eine Funktion muss zunächst vereinbart werden, ehe sie das erste Mal verwendet werden kann. Eine Funktion kann durch Angabe ihrer Definition vereinbart werden: #include <math.h> double sqr(double x) { return x*x; } int main(void) { double x = sqr(sqrt(2.)); /* ... */ } Des Weiteren ist es möglich, eine Funktion zunächst nur durch eine Deklaration zu vereinbaren und später zu definieren. Deklaration von Funktionen Eine Deklaration besteht aus Angabe des Funktionsprototyps: return-type function-name(parameter-list); Mit einer Deklaration werden die Eckdaten einer Funktion (Rückgabetyp, Name, Parameterliste) vereinbart. Es genügt, eine Funktion zunächst zu deklarieren, um sie im Weiteren zu verwenden: double sqr(double x); /* declaration of function sqr */ int main(void) { double x = sqr(sqrt(2.)); /* function call to sqr */ return 0; } double sqr(double x) { return x*x; } /* definition */ Deklaration vs. Definition von Funktionen I Die Deklaration vereinbart den Prototypen der Funktion. Die Definition einer Funktion legt die Anweisungen fest, die bei Funktionsaufruf ausgeführt werden. I Die Deklaration einer Funktion genügt zum Kompilieren. Erst beim Linken muss eine Definition der Funktion gefunden werden, um eine ausführbare Datei zu erstellen. I Die Definition einer einmal deklararierten Funktion kann an einer beliebigen Stelle im Programm (z. B. am Ende von main) oder auch in einer anderen Datei stehen. I Eine Funktion kann mehrfach (konsistent) deklariert werden, darf aber nur ein Mal definiert werden. Programmbeispiel Binominalkoeffizient Quelltext #include <stdio.h> double factorial(int n) { int i; double factorial = 1.; for(i = 1; i <= n; ++i) factorial *= i; return factorial; } int binomial(int n, int k) { double binomial = factorial((double) n ); binomial /= factorial((double) k)*factorial((double) n-k); return (int) binomial; } int main(void) { int n = 4, k = 2; printf("%d over %d = %d\n", n, k, binomial(4, 2)); return 0; } Implizite Deklaration von Funktionen Eine vergessene Funktionendeklaration führt (leider) nicht in jedem Fall zu einem Compiler-Fehler: 1 2 3 4 5 int main(void) { int s = sign(-1); return 0; } 6 7 8 9 10 int sign(int a) { return (a < 0 ? -1 : 1); } Der Compiler nimmt eine implizite Deklaration der Funktion sign vor: anhand des ersten Funktionsaufrufs wird die Parameterliste ermittelt; als Rückgabetyp wird immer int (!) angenommen. Im Beispiel sind also zufällig implizite Deklaration und Definition von sign konsistent. Implizite Deklaration von Funktionen II Implizite Deklaration der Funktion sign in Zeile 3 mit Rückgabetyp int. Die Definition, in Zeile 7, sieht als Rückgabewert allerdings ein double vor. 1 2 3 4 5 int main(void) { double s = sign(-1); return 0; } 6 7 8 9 10 double sign(int a) { return (a < 0 ? -1. : 1.); } Übersetzen des Codes führt zu der Fehlermeldungen deklarition.c:7:8: error: conflicting types for ‘sign’ double sign( int s ) dekl.c:3:10: note: previous implicit declaration of ‘sign’ was here double s = sign(-1); Der leere Datentyp void Funktionen können, müssen aber nicht unbedingt einen Wert zurückgeben. Funktionen, die keinen Wert liefern, haben einen speziellen Rückgabetyp, den leeren Typ void: void funtion-name(parameter-list); Eine void-Funktion kann durch eine optionale return-Anweisung ohne Argument beendet werden. Beispiele: void print(int a) { printf("%d\n", a); } void print(int a) { printf("%d\n", a); return; } Leere Parameterlisten Es gibt auch Funktionen, denen keine Parameter übergeben werden. Eine leere Parameterliste wird durch Angabe von void gekennzeichnet: return-type function-name(void); Beispiele: int main(void) { return 0; } Parameterlisten variabler Länge Es gibt auch Funktionen, denen eine beliebige Anzahl von Parametern übergeben werden können: printf("some text"); printf("some integers: %d, %d, %d", 1, 2, 3); Solche Funktionen heißen variadische Funktionen. Variadische Funktionen lassen sich am Prototyp erkennen. Nach den ersten bekannten Parametern folgen Auslassungspunkte (engl. ellipsis) "...": int printf (const char *format, ...); Die Übergabe und Verarbeitung von Parametern unterscheidet sich bei variadischen Funktionen grundlegend vom oben beschriebenen „Normalfall“ und wird hier nicht weiter behandelt. 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. → Wie die Parameter an das Programm übergeben und verarbeitet werden, lernen wir später. 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> Rekursive Funktion Funktionen dürfen sich auch selber aufrufen. Damit lassen sich rekursive Algorithmen einfach implementieren: /* n must be a positive integer value */ double power(double x, int n) { if(n == 0) /* termination condition */ return 1.; else return x*power(x, n-1); } Eine Rekursion braucht eine Abbruchbedingung (im Beispiel Zeilen 4–5)! Eine endlose Rekursion führt zum Programmabsturz. Rekursive Funktionen sind im allgemeinen weniger effizient als die Implementierung mit einer geeigneten Schleifenanweisung. Rekursion für Binominalkoeffizienten Die Binominalkoeffizienten berechnen sich auch nach der Rekursionsformel: n n−1 n−1 n n = + , = = 1. k k −1 k 0 n Diese lässt sich in wenigen Zeilen als rekursive Funktion implementieren: 1 2 3 4 5 6 int binomial(int n, int k) { if( k == 0 || k == n ) return 1; return binomial(n-1, k-1) + binomial(n-1, k); } Gültigkeitsbereiche I Jede Funktion, sogar jeder Block bildet einen eigenen sog. Gültigkeitsbereich. Innerhalb eines Gültigkeitsbereich können lokale Variablen definiert werden: int i, n = ...; double sum = 0.; for(i = 1; i <= n; ++i) { double x = 1./i; sum += x; } printf("x = %f", x)/* Error! Can not access x here. */ Gültigkeitsbereiche II Variablennamen dürfen in verschachtelten Gültigkeitsbereichen neu verwendet werden. Dann überdeckt die lokale Variable jene aus dem umliegenden Gültigkeitsbereich. { } /* outer namespace */ double x = 1.; { double x = 2.; /* inner namespace */ printf("x = %f", x); /* x = 2. */ } printf("x = %f", x); /* x = 1. */ Parameterübergabe per Wert (call by value) Im folgenden Beispiel sieht es so aus, als würde initialize die übergebene Variable a mit einem neuen Wert belegen: void initialize (int a) { a = 0; } int main(void) { int a = 1; initialize(a); printf("a = %d\n", a); return 0; } /* a = 1 */ Übergeben wird nicht die Variable a, sondern lediglich ihr Wert. Man spricht in diesem Zusammenhang von der Übergabe per Wert (engl. call by value). In initialize gibt es eine neue Variable (die zufällig auch a heißt). Ändert sich ihr Wert, hat das keine Auswirkung auf die Variable a in main. Parameterübergabe per Wert (call by value) Quelltext 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> void swap(int a, int b) /* wrong! */ { int c = a; a = b; b = c; } int main(void) { int a = 1, b = 2; printf("a = %d, b = %d\n", a, b); swap(a, b); printf("a = %d, b = %d\n", a, b); return 0; } /* a = 1, b = 2 */ /* a = 1, b = 2 */ Gliederung Funktionen Aufteilen von Programmteilen auf mehrere Dateien Bibliotheken Modulare Entwicklung Erreichen Programme eine gewisse Komplexität stellen sich eine Reihe von Fragen: I Wie kann ein komplexes Programm (von mehreren Entwicklern) am besten entwickelt werden? I Wie vermeidet man Fehler in komplexen Programmen? I Wie hält man Code wartbar? Modularisierung (Aufteilen in größere abgegrenzte Programmteile) stellt einen Lösungsansatz dar („divide and conquer“). Module, die unabhängig voneinander entwickelt und benutzt werden können, kann man in anderen Programm wiederverwenden oder einzeln weitergeben. Aufteilen in Dateien Die Funktionen eines C-Programms können auf mehrere Dateien aufgeteilt werden. minmax.h: main.c: int max(int a, int b) { ... } int min(int a, int b) { ... } #include ”header.h” int main(void) { int a = max(1,2); int b = min(1,2); return 0; } Dateien, die keine main-Funktion enthalten, nennt man Header-Dateien. Häufig enden Header-Dateien auf .h (stdio.h, math.h, . . . ). Eingefügt werden diese mit der include-Präprozessor-Direktive. Mehrfaches Einfügen verhindern Funktionen dürfen nur ein Mal definiert werden. Enthält eine Datei also Funktionsdefinitionen, darf sie nicht mehrfach eingebunden werden. Dies verhindert man mit sog. Header-Guards unter Verwendung von Präprozessor-Direktiven zur bedingten Übersetzung (s. spätere Vorlesung). Quelltext 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #ifndef MINMAX_H #define MINMAX_H int max(int a, int b) { return (a > b) ? a : b; } int min(int a, int b) { return (a < b) ? a : b; } #endif Gliederung Funktionen Aufteilen von Programmteilen auf mehrere Dateien Bibliotheken Bibliotheken Bibliotheken sind vorkompilierte Module, die Funktionen enthalten. In einer zugehörigen Header-Datei sind die in der Bibliothek enthaltenen Funktionen dokumentiert. Bibliotheken verringern den Zeitaufwand zum Kompilieren großer Programme und können die Ausführungsgeschwindigkeit eines Programms erhöhen. Wir illustrieren die Erstellung einer Bibliothek anhand eines kleinen Beispiels mit zwei kleinen Funktionen max und min. Erstellen einer Bibliothek I (Header) Der Header besteht aus den Deklarationen der in der Bibliothek enthaltenen Funktionen: #ifndef MINMAX_H #define MINMAX_H /* return max{ a, b } */ int max(int, int); /* return min{ a, b } */ int min(int, int); #endif In der Regel enthält ein Header eine kurze Dokumentation der Funktionen. Erstellen einer Bibliothek II (Implementierungen) Die Implementierungen der Funktionen max, min können in separaten Dateien stehen. max.c: min.c: int max(int a, int b) { return (a > b) ? a : b; } int min(int a, int b) { return (a < b) ? a : b; } Die Dateien max.c und min.c werden mit der Kompileroption -c kompiliert, um die zugehörigen Objektdateien zu erstellen: $ gcc -c max.c -o max.o $ gcc -c min.c -o min.o Erstellen einer Bibliothek III (Kompilieren) Eine Bibliothek besteht aus mehreren Objektdateien, die in einer Datei zusammengefügt sind. Zum Zusammenfassen mehrer Objektdateien gibt es unter Unix das Programm ar. Es wird wie folgt verwendet: $ ar rcs libminmax.a min.o max.o Wichtig: Eine Bibliothek beginnt min lib und hat die Dateiendung .a. Header einbinden Im Hauptprogramm wird nur den Header minmax.h eingebunden. Die Funktionen max, min können wie gewohnt aufgerufen werden: Quelltext (Inhalt von main.c) #include <stdio.h> #include "minmax.h" int main(void) { int a = 1, b = 2; printf("max{%d, %d} = %d\n", a, b, max(a, b)); printf("min{%d, %d} = %d\n", a, b, min(a, b)); return 0; } Linken von Bibliothek Der Übersetzungsvorgang besteht aus mehreren Schritten. Im engeren Sinne lässt sich das Hauptprogramm main.c ohne Weiteres kompilieren. Im finalen Übersetzungsschritt muss die Bibliothek minmax gelinkt werden, sonst sind die Funktionen max und min nicht definiert. Den Pfad zur Bibliothek und ihren Namen muss man beim Compiler-Aufruf explizit angeben: $ gcc -static -o main main.c -L. -lminmax Der Punkt "." nach -L gibt an, dass sich die Bibliothek im selben Verzeichnis wie das Hauptprogramm befindet. Automatisiertes Erstellen der Bibliothek Quelltext (Inhalt des Makefile) all: main main: main.c minmax.h libminmax.a gcc -static -o main main.c -L. -lminmax %.o: %.c gcc -c -o $@ $< libminmax.a: max.o min.o ar rcs libminmax.a max.o min.o clean: rm -f libminmax.a main *.o Autoren Autoren die an diesem Skript mitgewirkt haben: I 2011–2014 : Christoph Gersbacher I 2014 : Patrick Schreier 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