Funktionen, Header und Bibliotheken

Werbung
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
Herunterladen