Sie sind auf Seite 1von 66

Kurze Einfhrung in die Programmiersprache C

Prof. Dr. Ralf Mller

Skript zur Vorlesung im Wintersemester 2004 / 2005

Universitt Bielefeld Postfach 100131 Universittsstrae 25 33615 Bielefeld

Prof. Dr. Ralf Mller


AG Technische Informatik
Technische Fakultt
Universitt Bielefeld
www.ti.uni-bielefeld.de
Version 1.9 vom 26. November 2004, WS 2004/2005

Inhaltsverzeichnis
1 Einfhrung

2 Ein erstes C-Programm

3 Compilieren und Linken (gcc)

4 Bibliotheken

4.1

Wichtige Bibliotheken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

4.2

Bibliotheken erzeugen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

5 Prprozessor-Anweisungen

10

5.1

Include-Direktiven . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

10

5.2

Symbol- und Makrodefinitionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

11

5.3

Bedingte Einbindung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

12

5.4

Prprozessor-Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

13

6 Lexikalische Struktur von C-Programmen

14

7 Typen und Variablen

14

7.1

Grunddatentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

14

7.2

Definition von Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

15

7.3

Literale in Grunddatentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

16

7.4

Nutzerdefinierte Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

17

7.4.1

Aufzhlungstypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

17

7.4.2

Zeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

17

7.4.3

Felder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

20

7.4.4

Zeichenketten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

24

7.4.5

Strukturen und Unions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

24

7.4.6

Bitfelder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

25

7.5

Typdefinitionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

26

7.6

Export, Lebensdauer und Sichtbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

27

7.6.1

Export . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

27

7.6.2

Lebensdauer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

28

7.6.3

Sichtbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

29

Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

30

7.7

8 Ausdrcke und Operatoren

30

8.1

Ausdrcke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

30

8.2

Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

31

8.3

Prioritten und Assoziativitt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

35

8.4

Typ-Umwandlungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

36

8.5

Mathematische Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

36

9 Anweisungen

37

9.1

Einfache Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

37

9.2

Leere Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

37

9.3

Verbundanweisung (Block) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

37

9.4

Verzweigungsanweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

38

9.4.1

Verzweigungen mit if-then-else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

38

9.4.2
9.5

9.6

Mehrfachverzweigungen mit switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

38

Schleifen-Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

40

9.5.1

Schleifen mit Test am Anfang: while . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

40

9.5.2

Zhlschleifen mit Test am Anfang: for . . . . . . . . . . . . . . . . . . . . . . . . . . . .

41

9.5.3

Schleifen mit Test am Ende: do-while . . . . . . . . . . . . . . . . . . . . . . . . . . . .

42

9.5.4

Schleifen-Abbruch und bergang zur nchsten Iteration . . . . . . . . . . . . . . . . . .

42

Sprung-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

43

10 Funktionen

43

10.1 Wert- und Referenz-Argumente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

44

10.2 Rckgabewerte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

46

10.3 Prototypen und Export . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

46

10.4 Zeiger auf Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

47

10.5 C-Funktionen und der Argument-Stapel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

48

10.6 Rekursive Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

50

10.7 Die main-Funktion und Kommandozeilen-Parameter . . . . . . . . . . . . . . . . . . . . . . . .

52

11 Ausgewhlte Funktionen der Standard-C-Bibliothek

53

11.1 Systemrufe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

54

11.2 Verarbeitung von Zeichenketten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

54

11.3 Erzeugung von Zufallszahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

54

11.4 Heap-Verwaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

54

11.5 Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

55

11.5.1 Standard-Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

55

11.5.2 ffnen und Schlieen eines Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

55

11.5.3 Einfache Ausgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

55

11.5.4 Einfache Eingaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

56

11.5.5 Ein- und Ausgabe von Blockdaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

56

11.5.6 Formatierte Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

56

11.5.7 Formatierte Eingabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

57

11.5.8 Test auf Dateiende . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

57

11.5.9 Positionierung in der Datei . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

57

12 Makefiles

57

13 Aufgaben

61

13.1 Game of Life . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

61

13.2 Doppelt verkettete Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

62

1 Einfhrung
Die Programmiersprache C ist traditionell (seit Anfang der 70er Jahre) eng mit dem Unix-Betriebssystem
verknpft, da groe Teile des Betriebssystems (mit Ausnahme von Assembler-Routinen fr sehr hardwarenahe Aufgaben) in C programmiert sind. C bietet hochsprachliche Konstrukte, gestattet aber auch
die maschinennahe Manipulation von Daten. Zudem produzieren C-Compiler sehr effiziente Programme tatschlich wre es meist extrem mhsam, durch Assembler-Programmierung effizienteren Code
schreiben zu wollen. C erlaubt es aufgrund einiger (in der Regel sehr praktischer) Sprachkonstrukte
allerdings auch, extrem unlesbare Programme zu schreiben1 ; zudem ist C insbesondere aufgrund seines Zeiger-Konzeptes anfllig fr Programmierfehler. Wir werden uns mit dem C-Standard ANSI-C
(American National Standards Institute, auch ISO-C, ISO fr International Standardization Organization)
befassen, einem Standard, der 1989 festgelegt wurde und weite Geltung hat (auch als C89 bekannt).
Dessen Weiterentwicklung ist der aktuellste C-Standard, der als C99 bezeichnet wird; auf einige Spezialitten von C99 wird im Skript speziell hingewiesen2 .

2 Ein erstes C-Programm


Unser erstes C-Programm heit schraeg.c und gibt alles, was wir dem Programm auf der Kommandozeile beim Start mitgeben, schrg aus, drum der Name. Der Programmcode sieht so aus:
#include <stdio.h>
#include <string.h>

1
2
3

int
main(int argc, char *argv[])
{
int i, j, k;

4
5
6
7
8

for (i = 1; i < argc; i++)


for (j = 0; j < strlen(argv[i]); j++) {
for (k = 0; k < j; k++)
printf(" ");
printf("%c\n", argv[i][j]);
}
return 0;

9
10
11
12
13
14
15

16

In Zeile 1 und 2 bindet unser Programm zwei Header-Dateien ein, in denen Funktions-Prototypen
definiert sind, die wir bentigen. Wir benutzen zwei Funktionen: strlen() (in Zeile 10) ermittelt die
Lnge einer Zeichenkette (der Prototyp steht in string.h) und printf() (in Zeile 12 und 13) druckt
Daten auf dem Bildschirm aus (der Prototyp steht in stdio.h).
In Zeile 4 und 5 beginnt unsere einzige eigene Funktion mit dem Namen main(). Diese Funktion liefert
einen Rckgabewert vom Typ int (vorzeichenbehaftete ganze Zahl, Zeile 4) und erhlt zwei Argumente argc (vom Typ int) und argv (vom Typ Zeiger (*) auf ein Feld ([]) von Zeichen (char)).
1
Ziel beim Obfuscated C Code Contest ist es, compilierbare und lauffhige C-Programme mit mglichst undurchsichtigem
Programmcode zu schreiben, siehe http://ioccc.org.
2
Nach http://www.schellong.de/better_c99.htm hat sich der Standard C99 allerdings scheinbar noch nicht
vollstndig durchgesetzt.

Jedes ausfhrbare C-Programm braucht eine main()-Funktion: diese Funktion wird als erste aufgerufen, wenn das Programm gestartet wird. Die Shell bergibt dem Programm dabei KommandozeilenParameter dies sind alle Angaben, die auf der Kommandozeile der Shell hinter dem Namen des
Programms angefhrt werden. Die Anzahl der Parameter steht in argc und die Parameter selbst in
argv.
Unser Programm benutzt drei Variablen i, j, k vom Typ int, um drei ineinander verschachtelte
Zhlschleifen (for) zu durchlaufen. Jede Zhlschleife hat eine Anfangs-Anweisung, eine BedingungsAnweisung und eine Weiterstell-Anweisung. Die Anfangs-Anweisung wird vor Beginn der Schleife ausgefhrt. Die Schleife wird wiederholt solange ausgefhrt, wie die Bedingungs-Anweisung gilt, und nach
jedem Durchlauf der Schleife wird die Weiterstell-Anweisung ausgefhrt.
Das Programm durchluft in der ueren Schleife (Zeile 9-14) alle Kommandozeilen-Parameter von 1
bis einschlielich argc-1. In der zweiten Schleife (Zeile 10-14) wird jeder einzelne KommandozeilenParameter (eine Zeichenkette mit strlen(argv[i]) Zeichen) durchlaufen. Die dritte Schleife (Zeile
11-12) gibt mit printf() vor jedem Zeichen aus dem Kommandozeilen-Parameter eine Anzahl von
Leerzeichen aus; erst danach wird ein Zeichen des Kommandozeilen-Parameters gedruckt (Zeile 13).
Zum Schluss (in Zeile 15) gibt das Programm dem Betriebssystem zu verstehen, dass alles in Ordnung
war angezeigt dadurch, dass main() den Rckgabewert (vom Typ int) 0 zurckgibt.
Bevor unser Programm laufen kann, muss es compiliert werden, in diesem Fall mit dem GNU-CCompiler gcc (> ist der Eingabeprompt der Shell):
> gcc -o schraeg schraeg.c
>

Aus schraeg.c (unserem Quelltext) ist eine ausfhrbare Datei schraeg entstanden. Rufen wir
diese auf (drei Versuche!), so passiert folgendes:
> schraeg
> schraeg Hallo!
H
a
l
l
o
!
> schraeg Hallo Welt!
H
a
l
l
o
W
e
l
t
!
>

3 Compilieren und Linken (gcc)


Wir verwenden zum Compilieren und Linken den GNU-C-Compiler gcc. Das Programm gcc selbst ist
nur eine Hll-Applikation, aus der heraus der eigentliche Compiler, Assembler und Linker aufgerufen
werden.
4

HeaderDateien

cDatei

myinc1.h

Zwischen

Objekt

datei

datei

myinc1.h
myfile.o

myfile.c
myinc2.h

#include
#include

myinc2.h

Prproz.

Compiler
myfile.c

"Quelldateien"

"Modul"

Abbildung 1: Vorgnge beim Compilieren.

Nachdem man mit einem Text-Editor (z.B. emacs) die entsprechenden Quelltext-Dateien erzeugt hat
(hier mit der Endung .h fr Header-Dateien, welche mit #include eingebunden werden, und .c fr
den eigentlichen C-Code), kann man aus jeder einzelnen c-Datei mit
gcc -Wall -c myfile.c

eine Objekt-Datei (Endung .o) erzeugen, in diesem Fall myfile.o. Wie wir unten sehen werden,
bindet der Prprozessor vor dem Compilieren alle mit #include angegeben Header-Dateien in den Text
ein, erzeugt also einen Zwischen-Quelltext bestehend aus einer c-Datei und mehreren Header-Dateien.
Wir werden nachfolgend diesen Zwischenquelltext und die daraus entstehende Objekt-Datei als Modul
bezeichnen; siehe Abbildung 1. Die Option -Wall schaltet alle Warnungen des Compilers ein; dies
wird empfohlen, da es wertvolle Hinweise auf Fehler im Code liefert. Die entstandene Objekt-Datei
enthlt teilweise unaufgelste Bezge (Verweise, Links, Referenzen) auf andere Objekt-Dateien, z.B.
aus Standard-Bibliotheken. Dies sind Variablen und Funktionen aus anderen Modulen, deren Adresse
der Compiler noch nicht kennt. Mit dem Befehl
nm -o myfile.o

kann man sich alle in der Objekt-Datei definierten oder verwendeten Symbole (Variablen, Funktionen)
anzeigen lassen. Wir konzentrieren uns zunchst auf die Symbole von Funktionen (Variablen werden
auf hnliche, aber etwas kompliziertere Art und Weise behandelt). Ein T zeigt dabei an, dass eine
C-Funktion in dieser Datei definiert wird und ein globales Symbol darstellt, wohingegen bei einem definierten lokalen (verborgenen) Symbol ein t angezeigt wird; ein U hingegen taucht bei allen unaufgelsten Funktionen auf, also Funktionen, die in diesem Modul benutzt werden, aber in einem anderen
Modul definiert sind.
Fr das Programm testcode.c
1

#include <stdio.h>

2
3
4
5
6
7

static int
my_other_function(float x)
{
return (int)(x + 9.876);
}

8
9

int

my_function(float a, int i)
{
printf("a = %g, i = %d\n", a, i);
return my_other_function(a) * i;
}

10
11
12
13
14

erhalten wir bspw.:


> nm -o testcode.o
testcode.o:0000002c T my_function
testcode.o:00000000 t my_other_function
testcode.o:
U printf
>

Hier wre my_other_function also ein lokales Symbol, auf welches nicht von anderen ObjektDateien Bezug genommen werden kann (dies wird durch das Schlsselwort static festgelegt). Hingegen ist my_function in allen Modulen verwendbar. Das Modul benutzt die Funktion printf aus
der Standard-C-Bibliothek; diese ist hier undefiniert.
Jedes ausfhrbare Programm braucht einen Eintrittspunkt, bei C immer die Funktion main(). So knnte
in der Datei testmain.c folgender Code stehen:
#include "testcode.h"
#include <stdlib.h>

1
2
3

int
main()
{
my_function(1.234, 5678);
exit(0);
}

4
5
6
7
8
9

wobei folgende Symbole in der Objekt-Datei vorhanden sind:


> nm -o testmain.o
testmain.o:
U exit
testmain.o:00000000 T main
testmain.o:
U my_function
>

Hier wird my_function benutzt, welches aber nicht in dieser Objektdatei (sondern in der Objektdatei
testcode.o) definiert ist. Ebenso ist die Funktion exit in dieser Objektdatei undefiniert; sie ist in
der Standard-C-Bibliothek definiert.
Wir sehen, dass die Datei testmain.c eine Header-Datei testcode.h einbindet; dies geschieht
ber die Prprozessor-Direktive #include. Header-Dateien enthalten in der Regel keine Anweisungen,
sondern nur Typ-Definitionen, Prprozessor-Direktiven und Prototypen. In dieser Header-Datei steht nur
der Prototyp der in main() genutzten Funktion my_function. Der Prototyp muss beim Compilieren bekannt sein, damit der Compiler die korrekte Argument-bergabe an die Funktion kontrollieren
kann. Um nicht jedes Mal alle verwendeten Prototypen in der c-Datei auffhren zu mssen, werden diese
in die Header-Datei geschrieben (siehe auch Abschnitt 7.6.1). Die Header-Datei, hier z.B. testcode.h

a.h
#include <c.h>

c.h
myprog.c
#include <a.h>
#include <b.h>

b.h

struct dbrec {
int age;
char name[10];
};

#include <c.h>

struct dbrec {
int age;
char name[10];
};
struct dbrec {
int age;
char name[10];
};

error: redefinition of struct dbrec

Abbildung 2: Konflikte wegen Mehrfachdefinition bei fehlender Einbinde-Sperre.

#ifndef _TESTCODE_H_
#define _TESTCODE_H_

1
2
3

extern int
my_function(float, int);

4
5
6

#endif

wird dann vom benutzenden Programm (hier testmain.c) eingebunden.


Man beachte die Prprozessor-Anweisung zur bedingten Einbindung (#ifndef, #define, #endif).
Es kann bei groen Programm-Projekten leicht vorkommen, dass eine Header-Datei indirekt ber verschiedene andere Header-Dateien mehrfach mit #include eingebunden wird. Dies fhrt beim Compilieren zu Konflikten wegen Mehrfachdefinition (Abbildung 2). Um dies zu verhindern, wird in jeder
Header-Datei ein Symbol definiert (mit #define), meist mit Bezug zum Namen der Datei, und die
Einbindung erfolgt nur, solange dieses Symbol noch undefiniert ist d.h. in jeder c-Datei wird eine
h-Datei nur genau einmal eingebunden. Die Prprozessor-Befehle werden in Abschnitt 5 erlutert.
Zunchst werden nun beide c-Dateien compiliert:
> gcc -Wall -c testcode.c
> gcc -Wall -c testmain.c
>

Nimmt man die Compiler-Option -v mit auf, so zeigt gcc an, welche Schritte beim Compilieren durchgefhrt werden, und welche Parameter letztendlich dem Compiler bergeben werden (versuchen Sie dies
und ergrnden Sie mit Hilfe der Man-Pages (siehe Abschnitt 4) die Bedeutung der verschiedenen dabei
ausgefhrten Programme und deren Optionen). Beim Compilieren ist dies im ersten Schritt der Compiler,
welcher den C-Code in Assembler-Code bersetzt, und im zweiten Schritt der Assembler, welcher daraus
Maschinen-Code produziert (allerdings mit noch unaufgelsten Referenzen, d.h. bei Funktionsaufrufen
und Bezgen auf Variablen in anderen Objekt-Dateien sind noch Adressen unbekannt).
Nach dem Compilieren aller c-Dateien knnen wir durch Linken ein ausfhrbares Programm erstellen:
gcc -o testprog testmain.o testcode.o

Hier entsteht aus zwei Objekt-Dateien die ausfhrbare Datei testprog. Dabei werden alle unaufgelsten Referenzen aufgelst z.B. wird in den Maschinenbefehl zum Aufruf der Funktion my_function
7

C1

testcode.o

testprog
my_other_function

my_other_function

my_function

my_function

call my_other_function

call my_other_function

call printf

call printf

main
testmain.o
main
?
?

call my_function
call exit

call my_function
call exit

libc.a
printf

printf

exit

exit

Abbildung 3: Vorgnge beim Linken: 2 Objektdateien und eine Bibliothek (links und Mitte) werden
zu einem ausfhrbaren Programm (rechts) verlinkt. Dabei werden alle unaufgelsten Bezge zwischen
verschiedenen Modulen (als ? angedeutet) aufgelst. Der Unterprogrammaufruf erfolgt ber den Maschinenbefehl call.

in testmain.o die korrekte Adresse der Funktion eingetragen. Ohne dass dies explizit angegeben werden muss, werden Standard-Bibliotheken (dies sind Sammlungen von Objekt-Dateien; siehe Abschnitt 4)
mit verlinkt, so dass auch unaufgelste Bezge wie der zum Symbol printf aufgelst werden knnen.
Abbildung 3 veranschaulicht den Linkvorgang.
Die Einbindung von weiteren Bibliotheken erfolgt durch die Optionen -L und -l. So wird beim Aufruf
gcc -o testprog testmain.o testcode.o -L/usr/local/lib -lmymath

zustzlich der Pfad /usr/local/lib durchsucht und eine Bibliothek mit dem Namen
libmymath.a (oder libmymath.so) in den Link-Prozess eingebunden (das lib im Bibliotheksnamen und die Endung .a oder .so wird weggelassen). Der Linker nimmt in die ausfhrbare Datei
brigens nur den erforderlichen Code auf, nicht den gesamten Code aller Objekt-Dateien und Bibliotheken.
Besteht das Programm nur aus einer einzigen c-Datei, so gengt ein einziger Aufruf, welcher Compiler,
Assembler und Linker vereint:
gcc -Wall -o myprog myprog.c

Fr den Test eines Programms und die Fehlersuche kann ein Debugger verwendet werden. Mchte man
ein Programm im Debugger testen, so muss beim Compiler-Aufruf die Option -g angegeben werden,
damit Debug-Information in die Objekt-Dateien aufgenommen wird. Ein Debugger mit komfortablem
graphischen Interface ist ddd. Machen Sie sich mit dem Debugger ddd vertraut (Online-Hilfe im Programm selbst). Testen Sie anhand eines einfachen Programms das Setzen von Unterbrechungspunkten,
den Start des Programms, das Anzeigen von Variablen, die schrittweise Abarbeitung und die Abarbeitung von Unterbrechungspunkt zu Unterbrechungspunkt. Sie knnen vom Beispiel testcode.c und
testmain.c ausgehen; erweitern Sie die Funktion my_function um einige lokale Variablen und
Anweisungen, z.B.:

C2

int
my_function(float a, int i)
{
float b;

1
2
3
4
5

printf("a = %g, i = %d\n", a, i);


b = 3.1415 * a;
a = b / 2.0;
i = i + 10;
return my_other_function(a) * i;

6
7
8
9
10

11

4 Bibliotheken
4.1 Wichtige Bibliotheken
C selbst bietet innerhalb des Sprachumfangs relativ wenig Funktionalitt. Statt dessen werden zahlreiche
Bibliotheken angeboten, die Funktionen wie Zeichenketten-Verarbeitung oder mathematische Funktionen zur Verfgung stellen.
Beim Linken wird automatisch die Standard-C-Library (libc.a oder libc.so) eingebunden, welche z.B. das API (application program interface, die Schnittstelle zur Bibliothek) fr die Systemrufe,
Funktionen zur Verarbeitung von Zeichenketten, zur Aus- und Eingabe, zur Netzwerk-Anbindung etc.
enthlt.
Werden mathematische Funktionen (z.B. tanh()) benutzt, so muss die Header-Datei math.h eingebunden und die Bibliothek libm.a oder libm.so mit der Option -lm hinzugelinkt werden.
Informationen ber die Funktionen dieser Bibliotheken findet man in den man pages, dem OnlineHandbuch von Unix. Die Man-Pages sind in Sektionen unterteilt: Unix-Kommandos finden sich in Sektion 1 (Bsp. man 1 gcc), Systemrufe in der Sektion 2 (Bsp. man 2 write), alle anderen Funktion
in der Sektion 3 (Bsp. man 3 atanh). Die Sektion kann beim Aufruf auch weggelassen werden, allerdings werden bei Namensberschneidungen dann bisweilen die falschen Angaben (aus anderen Sektionen) geliefert.
Zu einer Bibliothek gehren ein oder mehrere Header-Dateien, welche die Typdefinitionen und Prototypen von Funktionen enthalten; bei der Mathematik-Bibliothek libmath.a ist dies z.B. die HeaderDatei math.h. Die Man-Pages liefern in der Regel zu Systemrufen und anderen Funktionen auch die
Information, welche Header-Datei eingebunden werden muss.

4.2 Bibliotheken erzeugen


Man kann mehrere Objektdateien nach dem Compilieren mit dem Programm ar zu einer statischen
Bibliothek zusammenfassen:
ar rcs libmymath.a testcode.o othercode.o

Hier entsteht aus den Objektdateien testcode.o und othercode.o die statische Bibliothek
libmymath.a, als statisch erkennbar an der Endung .a. Das Zusammenfassen erspart die Angabe
jeder einzelnen Objektdatei beim Linken; statt dessen wird nur der Name der Bibliothek angegeben.
Wird eine solche statische Bibliothek mit einem Programm verlinkt, z.B. mit
gcc -o testprog testmain.o -L/usr/local/lib -lmymath

Programm 1

Programm 1
printf

Programm 2

libc.a
exit

Programm 3
Programm 2
printf
libc.a
exit
Programm 3
printf

printf

libc.a

libc.so

exit

exit

Abbildung 4: Statische (links) und dynamische (rechts) Bibliotheken im Hauptspeicher.

so entsteht ein ausfhrbares Programm (in diesem Fall testprog), welches die bentigten Teile der
Bibliothek enthlt. Alle Verweise werden beim Linken aufgelst.
Bei Bibliotheken, die von vielen Programmen genutzt werden (wie bspw. der Standard-C-Bibliothek)
ist diese Vorgehensweise ungnstig: Jedes Programm enthlt dieselben Funktionen der Bibliothek, d.h.
derselbe Code belegt mehrfach Platz im Hauptspeicher und auf der Festplatte (siehe Abbildung 4,
links). Deswegen wurden dynamische Bibliotheken entwickelt, deren Dateiname auf .so endet (z.B.
libc.so). Diese Bibliotheken werden erst beim Laden des Programms mit dem restlichen Code verlinkt das Programm auf der Festplatte enthlt also nicht die bentigten Code-Teile aus der Bibliothek. Zudem knnen mehrere Programme bei der Ausfhrung dieselbe Bibliothek nutzen, die nur ein
einziges Mal im Hauptspeicher steht; man bezeichnet diese Bibliotheken deswegen auch als shared
libraries (Abbildung 4, rechts). Ein Nachteil von dynamischen Bibliotheken ist, dass die ausfhrbare
Datei ausfhrbar ist, wenn auch die zugehrigen Bibliotheken installiert sind. Die Erzeugung von dynamischen Bibliotheken ist etwas komplizierter als die statischer Bibliotheken; Details finden sich auf
http://www.dwheeler.com/program-library.

5 Prprozessor-Anweisungen
Der C-Prprozessor cpp wird implizit bei jedem Compiler-Lauf aufgerufen. Er fhrt vor dem eigentlichen Compilieren textliche Einfgungen und Ersetzungen durch. Alle Prprozessor-Direktiven beginnen
mit einem Doppelkreuz (#). Die wichtigsten sind Include-Direktiven, Symbol- und Makro-Definitionen
und die bedingte Einbindung.

5.1 Include-Direktiven
Die Direktive #include fgt die Datei des angegebenen Namens (eine Header-Datei) in den Programmtext ein, z.B.
#include "myheader.h"
#include <math.h>

wobei der erste Aufruf zunchst auch im aktuellen Verzeichnis nach der Datei sucht, der zweite hingegen
nur in der Standardliste der Verzeichnisse (die mit -I beim Compiler-Aufruf spezifiziert werden kann).
10

Header-Dateien (.h) werden (evtl. indirekt ber andere Header-Dateien) vor dem Compilieren in cDateien (.c) eingebunden; aus letzteren entsteht dann eine Objektdatei (.o) eine Objektdatei entsteht
niemals allein aus einer Header-Datei.

5.2 Symbol- und Makrodefinitionen


Mit der Direktive #define wird ein Symbol oder ein Makro definiert. Es ist blich, fr Makro-Bezeichner nur Grossbuchstaben zu verwenden. So wird mit
#define MAX_ARRAY_SIZE 100

jedes Auftreten von MAX_ARRAY_SIZE im C-Code durch den Text 100 ersetzt, z.B. in
float my_array[MAX_ARRAY_SIZE];

Es ist generell guter Programmierstil, Konstanten wie Feldgren nicht direkt als Literal (hier die ganze
Zahl 100) in den Code zu schreiben, sondern mithilfe des Prprozessors an geeigneter Stelle (meist in
einer Header-Datei) zu definieren und im Code das entsprechende Symbol zu verwenden.
Makros bieten die Mglichkeit, den Ersetzungstext ber Parameter zu beeinflussen. Im Linux-Kernel
wird relativ hufig vom Makros Gebrauch gemacht. So wird in im Linux-Scheduler (einem Teil des Kernels, der die CPU unter mehreren Prozessen aufteilt) z.B. ein Makro hnlich dem folgenden definiert3 :
#define WAIT_EVENT(WQ, CONDITION) \
{ \
add_wait_queue(&WQ, &wait); \
for (;;) { \
set_current_state(TASK_UNINTERRUPTIBLE); \
if (CONDITION) \
break; \
schedule(); \
} \
current->state = TASK_RUNNING; \
remove_wait_queue(&WQ, &wait); \
}

1
2
3
4
5
6
7
8
9
10
11
12

Die Backslash-Zeichen am Zeilen-Ende teilen dem Prprozessor mit, dass die nchste Zeile auch noch
zum Makro-Text gehrt. Dieses Makro definiert eine kurze Code-Sequenz, mit der ein Prozess schlafen
gelegt wird, whrend auf das Eintreten einer Bedingung gewartet wird. Der Prozess wird in eine Warteliste eingefgt, der Scheduler wird aufgerufen, um einen anderen Prozess auf die CPU zu bringen, und bei
Rckkehr aus dem Scheduler wird der Prozess geweckt und aus der Warteliste entfernt. Dieser Code wird
an sehr vielen Stellen im Kernel mit unterschiedlichen Wartelisten und Bedingungen genutzt. Verwendet
man nun das Makro im Code, z.B.
WAIT_EVENT(myqueue,!inbuf.empty || signal_caught);

so wird statt dessen der gesamte Makro-Krper an dieser Stelle in den Text eingefgt, wobei zuvor
der Parameter WQ (Name der Warteliste) im Krper des Makros durch myqueue und der Parameter CONDITION (die Weckbedingung) durch !inbuf.empty || signal_caught ersetzt wird.
(berdenken Sie den Unterschied zwischen dem Makro-Aufruf und einer Funktion: bei einer Funktion wrde die Bedingung einmalig berechnet und das Ergebnis dem Funktionsargument zugewiesen,
beim Makro wird die Bedingung jedoch textlich eingefgt und deshalb in der Schleife (Zeile 6) jeweils
11

C3

neu aktualisiert). Analysieren Sie das Ergebnis der Ersetzung nach Aufruf des Prprozessors (gcc -E
cpp_test.c fhrt nur den Prprozessorlauf durch und gibt das Ergebnis auf der Standardausgabe aus)
(Musterlsung: cpp_test.c).
In vielen Fllen ist es sinnvoll, bei Makro-Definitionen, die Code-Stcke enthalten, den Code in geschweifte Klammern einzuschliessen. Er stellt dann eine Verbundanweisung dar (Abschnitt 9.3) und
kann z.B. in Schleifen und Verzweigungen ohne Klammerung eingefgt werden.
Makros sind oft ntzlich fr das Einfgen von Debug-Ausgaben. So knnte man whrend der Programmentwicklung das Makro
#define DEBUG(TEXT) puts(TEXT)

benutzen, um an verschiedenen Stellen im Programm Ausgaben zu machen, z.B.


DEBUG("vor dem Funktionsaufruf");
result = fct(12);
DEBUG("nach dem Funktionsaufruf");

Man kann dann durch ndern der Makro-Definition in


#define DEBUG(TEXT)

alle Ausgaben abschalten (das Programm muss natrlich neu compiliert werden).
Da Makro-Definitionen textliche Ersetzungen sind, spielen Leerzeichen bei Makro-Name und Parametern eine Rolle; als Daumenregel sollte man deshalb berflssige Leerzeichen weglassen.
Mit der Direktive #undef und Angabe des Namens knnen Symbole und Makro-Definitionen gelscht
werden.

5.3 Bedingte Einbindung


Der Prprozessor kann in Abhngigkeit davon, ob ein Symbol definiert wurde (#define), Teile des
Quelltextes aussparen oder einbinden. Dies wird z.B. zur Verhinderung von Mehrfacheinbindungen von
Header-Dateien verwendet (siehe Abschnitt 3). Die entsprechenden Direktiven lauten:
#ifdef MY_SYMBOL
/* eingebundener Text, wenn MY_SYMBOL definiert ist */
#else
/* eingebundener Text, wenn MY_SYMBOL nicht definiert ist */
#endif
#ifndef MY_SYMBOL
/* eingebundener Text, wenn MY_SYMBOL nicht definiert ist */
#else
/* eingebundener Text, wenn MY_SYMBOL definiert ist */
#endif

wobei der #else-Zweig weggelassen werden kann. Es sei hier nur auf die Mglichkeit der Auswertung
von Ausdrcken mit der Direktive #if hingewiesen (H EROLD und A RNDT 2001, S. 488).
3

Siehe /usr/src/linux-2.4/include/linux/sched.h

12

C4

5.4 Prprozessor-Operatoren
Mit dem Prprozessor-Operator # wird ein Makro-Parameter in eine Zeichenkette umgewandelt (H E ROLD und A RNDT 2001, S. 503). So kann man Testausgaben mit dem Makro
#define AUSGABE(VAR) printf(#VAR " = %d\n", VAR)

realisieren, welches bspw. bei Benutzung mit


int result = 123;
AUSGABE(result);

vom Prprozessor folgendermassen ersetzt wird:


printf("result" " = %d\n", result)

Der Compiler fasst hintereinanderstehende Zeichenketten zusammen, in diesem Fall entsteht:


printf("result = %d\n", result);

Diese Anweisung gibt die ganzzahlige Variable result folgendermassen auf der Standardausgabe aus:
result = 123

Ntzlich ist bisweilen auch der Prprozessor-Operator ##. Dieser fgt Makro-Parameter zu neuen Namen zusammen. Gibt es z.B. in einem Programm an mehreren Stellen Variablen mit demselben Namensrumpf aber unterschiedlichen Nachstzen wie z.B. res_old, res_new, so wre folgendes Makro
hilfreich
#define AUSGABEN(VAR) \
printf(#VAR "_old = %d\n", VAR ## _old); \
printf(#VAR "_new = %d\n", VAR ## _new)

Angewandt auf
AUSGABEN(res);
AUSGABEN(tmp);

erzeugt der Prprozessor diesen Text:


printf("res" "_old = %d\n", res_old); printf("res" "_new = %d\n", res_new);
printf("tmp" "_old = %d\n", tmp_old); printf("tmp" "_new = %d\n", tmp_new);

Die Ausgabe she also z.B. folgendermaen aus:


res_old
res_new
tmp_old
tmp_new

=
=
=
=

12
78
3
-3

Testen Sie, welche Ersetzung hingegen bei folgendem Makro vorgenommen wird, bei dem ## nicht
benutzt wurde:
#define AUSGABEN(VAR) \
printf(#VAR "_old = %d\n", VAR_old); \
printf(#VAR "_new = %d\n", VAR_new)

13

C5

27 26 25 24 23 22 21 20

1 0 1 1 0 0 1 1

Abbildung 5: Dualzahl mit 8 binren Stellen; die Stellenwertigkeiten sind ber den Stellen angegeben.

6 Lexikalische Struktur von C-Programmen


Fr C existieren aus lexikalischer Sicht folgende Klassen von Wrtern:
Trennzeichen sind Leerzeichen, Tabulatoren, Zeilenumbrche und Kommentare. Letztere haben die
Form /* Ein Kommentar */, wobei sich der Kommentar ber mehrere Zeilen erstrecken
kann. In C99 knnen Kommentare auch mit // eingeleitet werden alle nachfolgenden Zeichen
bis zum Zeilenende sind dann Kommentar.
Namen Namen bezeichnen Typen, Variablen, Funktionen usw. Sie bestehen aus beliebigen Buchstaben,
Ziffern und dem Unterstrich, mssen aber mit einem Buchstaben beginnen. Schlsselwrter der
Sprache C wie switch oder enum drfen nicht verwendet werden. Eine sinnvolle Konvention ist
z.B. die Verwendung von kleinen Anfangsbuchstaben fr Variablen und Funktionen und groen
Anfangsbuchstaben fr Typen. Zwischen Gro- und Kleinschreibung wird unterschieden.
Literale bezeichnen feste Werte eines Datentyps. Sie knnen in C fr ganzzahlige und GleitkommaTypen (z.B. 100, 12.34) sowie fr Zeichen und Zeichenketten angegeben werden (z.B.
A, "Zeichenkette").
Operatoren (bspw. + << -- %) verknpfen Ausdrcke.

7 Typen und Variablen


7.1 Grunddatentypen
C verfgt ber Grunddatentypen fr
vorzeichenlose ganze Zahlen (Dualzahlen),
vorzeichenbehaftete ganze Zahlen (Zweier-Komplement-Zahlen, 2K-Zahlen) und
Gleitkommazahlen
Eine vorzeichenlose ganze Zahl vom Datentyp unsigned char belegt 1 Byte (8 Bit) im Speicher, sie
hat also 8 Binrstellen. Sie ist als Dualzahl in einem Stellencode hnlich dem Dezimalsystem kodiert.
Die Zahl in Abbildung 5 hat den dezimalen Wert
1 27 + 0 26 + 1 25 + 1 24 + 0 23 + 0 22 + 1 21 + 1 20
= 128 + 32 + 16 + 2 + 1
= 179
Vorzeichenbehaftete ganze Zahlen sind als Zweier-Komplement-Zahlen (2K-Zahlen) kodiert. Steht bei
diesen Zahlen in der hchstwertigen Stelle eine 0, so entspricht ihr Wert dem ihres dualen Stellencodes
14

Tabelle 1: Grunddatentypen von C


Typ
Bytes Minimum
Maximum
char
1
128
127
unsigned char 1
0
255
short
2
32768
32767
unsigned short 2
0
65536
31
int
4
2
231 1
unsigned int
4
0
232 1
31
long
4
2
231 1
unsigned long 4
0
232 1
38
float
4
3.4 10
3.4 1038
double
8
1.8 10308
1.8 10308
4932
long double
12
1.2 10
1.2 104932

(geben Sie die grte mit m Bit darstellbare positive 2K-Zahl an, und berprfen Sie Ihr Ergebnis anhand
von Tabelle 1). Steht an der hchstwertigen Stelle jedoch eine 1, so ist die Zahl negativ; ihr Betrag ergibt
sich durch Bildung des Zweier-Komplements (Negation aller Bitstellen und Addition von 1). Wre die
Zahl in Abbildung 5 also vom Typ signed char, so wre sie negativ (vorderste Stelle ist 1) und htte
den Betrag 77: die Negation aller Stellen ergibt die Binrzahl 01001100 mit dem dezimalen Wert 76
(berprfen Sie dies), durch Addition von 1 erhlt man 77 (H EROLD und A RNDT 2001, S. 16).
Gleitkommazahlen werden aus einer dual kodierten Mantisse und einem dual kodierten Exponenten zusammengesetzt; je nach Format variiert die Anzahl der Bits in Mantisse und Exponent.
Tabelle 1 fhrt wichtige Grunddatentypen von C an, im oberen Teil die ganzzahligen Typen (vorzeichenlose und Zweier-Komplement-Zahlen), im unteren die Gleitkommatypen. Es gibt jeweils mehrere
Mglichkeiten zur Angabe des Typs (z.B. long int statt long oder signed char statt char);
die Tabelle enthlt jeweils die krzeste Form (auer fr unsigned int, fr welches die Angabe
unsigned gengt). Die angegeben Lngen (Bytes) und Grenzwerte der ganzzahligen Typen gelten
fr gcc auf IA-32 Prozessoren (Intel Architecture, 32 Bit); generell schreibt der Standard aber nur vor,
dass
sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long)

Bei den Gleitkomma-Werten sind die Gren und Grenzwerte fr den gebruchlichen IEEE-754-Standard
angegeben4 .
Die Grenzwerte von ganzen Zahlen sind in der Header-Datei limits.h als Makros definiert, die von
Gleitkommazahlen in der Header-Datei float.h (H EROLD und A RNDT 2001, S. 147).
Ein spezieller Datentyp ist void (kein Typ); dieser spielt insbesondere bei Funktionen und in Zusammenhang mit Zeigern eine Rolle (Abschnitt 10).

7.2 Definition von Variablen


Jede Variable muss vor ihrer Benutzung definiert werden. Die Definition von Variablen erfolgt wie in
diesem Beispiel angegeben:
4

Der Datentyp long double entspricht einer 10-Byte langen Zahl, wird aber zur besseren Ausrichtung im Speicher mit
zwei Byte auf 12 Byte Lnge aufgefllt.

15

C6

C7

int i, j = 4, k;
float x, y, z[3];
char ch, *p;

Variablen knnen bei Ihrer Definition auch initialisiert werden (erste Zeile)5 . Die zweite Zeile enthlt
ein Beispiel fr eine Feld-Definition (z[3]) und die dritte Zeile ein Beispiel fr einen Zeiger auf char
(mit dem Namen p); siehe dazu auch Abschnitt 7.4.3 bzw. 7.4.2.

7.3 Literale in Grunddatentypen


Um ganzzahlige und Gleitkomma-Variablen nutzen zu knnen, muss es mglich sein, Literale anzugeben. Ein Literal bezeichnet einen festen Wert eines Datentyps. Bei ganzzahligen Datentypen knnen
Dezimal-, Oktal- (vor der ersten Ziffer steht eine Null) und Hexadezimal-Zahlen (die Zahl beginnt mit
0x) angegeben werden. Oktalzahlen beziehen sich auf die Basis 8, haben also als Ziffern 0 . . . 7; eine Oktalzahl fasst jeweils 3 Binrstellen zusammen. Hexadezimalzahlen beziehen sich auf die Basis
16; sie verwenden als die ersten 10 Ziffern die Ziffern 0 . . . 9, die weiteren 6 Ziffern werden durch die
Buchstaben A . . . F dargestellt. Eine Hexadezimalziffer fasst jeweils 4 Binrstellen zusammen. Die vorzeichenlose Zahl in Abbildung 5 lsst sich in C oktal als 0263 und hexadezimal als 0xb3 darstellen
(berprfen Sie dies) (H EROLD und A RNDT 2001, S. 25).
Ein L hinter der Zahl kennzeichnet die Konstante als zum Datentyp long gehrig, ein ul als unsigned
l und ein u als unsigned, ansonsten wird standardmig int angenommen, falls nicht der Wertebereich von int berschritten wird:
100
123456
5L
123u
777ul
-020
0x1fff
0x1ffful

/*
/*
/*
/*
/*
/*
/*
/*

int, dezimal */
int, dezimal */
long, dezimal */
unsigned int, dezimal */
unsigned long, dezimal */
int, oktal */
int, hexadezimal */
unsigned long, hexadezimal */

Gleitkomma-Literale knnen mit oder ohne Exponenten (e oder E) angegeben werden. Ein Exponent
oder ein Dezimalpunkt kennzeichnet das Literal als Gleitkomma-Literal. Diese Literale sind standardmig vom Typ double (ein f hinter dem Literal erzeugt ein Literal vom Typ float, ein L hinter dem
Literal erzeugt ein Literal vom Typ long double):
-9.876
123.456E-7
1e12
.0001
123.
1.234f
1.234L

/*
/*
/*
/*
/*
/*
/*

double */
double */
double */
double */
double */
float */
long double */

Zeichen-Literale (die den Typ int haben) werden oft in Zusammenhang mit Variablen vom Typ char
benutzt. Sie werden in Hochkommas eingeschlossen. Steuerzeichen (ASCII-Codes unter 20H) werden
mit Backslash-Sequenzen dargestellt:
5

Initialisierte Variablen innerhalb von Funktionen, die nicht als static erklrt sind, werden bei jedem Aufruf initialisiert. Initialisierte Variablen innerhalb von Funktionen, die als static definiert sind, werden nur einmal beim Programmstart
initialisiert.

16

C8

A
*
\0
\n
\t
\
\\
\nnn
\xhh

/*
/*
/*
/*
/*
/*
/*
/*
/*

Buchstabe A */
Zeichen * */
Steuerzeichen mit ASCII-Code 0 (Zeichenketten-Ende) */
Steuerzeichen Newline (Zeilenvorschub) */
Steuerzeichen Tabulator */
Apostroph-Zeichen */
Backslash-Zeichen */
Angabe des ASCII-Codes als 3 Oktalziffern nnn */
Angabe des ASCII-Codes als 2 Hexadezimalziffern hh */

7.4 Nutzerdefinierte Datentypen


7.4.1 Aufzhlungstypen
C erlaubt die Definition von Aufzhlungstypen. Diese sind Listen von Konstanten mit ganzzahligen
Werten. Aufzhlungstypen drfen berall verwendet werden, wo der Typ int erlaubt ist (H EROLD und
A RNDT 2001, S. 804). Die den Konstanten zugewiesenen Werte steigen in Einerschritten vom ersten
Wert in der Reihenfolge im Aufzhlungstyp auf. Standardmig erhlt die erste Konstante den Wert 0,
die zweite 1 usw. Man kann aber sowohl den Anfangswert als auch alle einzelnen Werte mit beliebigen ganzzahligen Werten belegen. Im Beispiel werden drei Variablen color, day und ctrl von drei
verschiedenen Aufzhlungstypen definiert:
enum {red, green, blue} color;
enum {mon = 1, tue, wed, thu, fri, sat, sun} day;
enum {bell = \007, newline = \n, tab = \t} ctrl;

Man kann nun sowohl die definierten Variablen eines Aufzhlungstyps als auch dessen Konstanten im
Programm verwenden:
color = blue;
if (day == fri)
printf("Thank god, its friday!%c%c", bell, newline);

1
2
3

7.4.2 Zeiger
Zeiger (pointer) spielen in C eine zentrale Rolle. Im Gegensatz zu anderen Programmiersprachen knnen Zeiger in C relativ freizgig manipuliert werden (worin aber auch eine Gefahr liegt). Zeiger sind
unverzichtbar fr die Realisierung verketteter Listen und Bume und dienen auch der bergabe von
Referenz-Argumenten an C-Funktionen (also Argumenten, bei denen eine Manipulation innerhalb der
Funktion auch den Wert der Variable auerhalb der Funktion ndert).
Zeiger-Variablen enthalten Adressen von Objekten eines bestimmten Datentyps (sowohl von Grunddatentypen als auch von nutzerdefinierten Datentypen). Auf der in der Zeiger-Variable gespeicherten
Adresse steht der sogenannte Inhalt des Zeigers, also die eigentlichen Daten. Abbildung 6 zeigt ein Beispiel: die Variable ptr ist eine Zeiger-Variable. In ptr steht eine Adresse (0x1bd284a2). Auf dem
Speicherplatz, der durch diese Adresse gekennzeichnet ist, findet sich der Inhalt des Zeigers, also die
eigentlichen Daten, auf die mit *ptr zugegriffen werden kann.
Zeiger-Variablen werden unter Angabe des Datentyps mit einem * definiert:
char *my_string;
float *num_ptr1, *num_ptr2, num;
struct { int year; float salary; } info, *info_ptr;
int **array2D;

17

Speicher

ptr:

0x1bd384a2:

0x1bd384a2

Inhalt

Abbildung 6: Zeiger: Eine Zeiger-Variable enthlt die Adresse eines Speicherplatzes, auf dem der Inhalt
des Zeigers zu finden ist.

Im Beispiel wird my_string als Zeiger auf char, num_ptr1 als ein Zeiger auf Objekte vom Typ
float, info_ptr als Zeiger auf Objekte des angegeben Struktur-Typs (Abschnitt 7.4.5) und array2D
als Zeiger auf einen Zeiger auf ein int-Objekt definiert. Allerdings zeigen diese Zeiger noch irgendwohin (undefinierte Adresse). Die Zeigervariable muss vor ihrer Verwendung sinnvoll belegt werden (nicht
oder falsch belegte Zeigervariablen sind eine hufige Ursache fr Programmfehler). Diese Zuweisung an
den Inhalt des uninitialisierten Zeigers ip wre also fatal:
int *ip;
*ip = 100;

Es gibt mehrere Mglichkeiten, eine Zeigervariable zu manipulieren. Man kann dem Zeiger die Adresse
eines Objektes zuweisen (Adress-Operator &):
ptr = &info;
num_ptr1 = &num;

Man kann den Inhalt der Adresse, welche in der Zeigervariablen steht, ermitteln (DereferenzierungsOperator *, Klammerung zur besseren Lesbarkeit):
num = num + (*num_ptr1) - (*num_ptr2);

Zeigervariablen drfen einander zugewiesen werden (damit zeigt num_ptr1 auf dasselbe Objekt wie
num_ptr2):
num_ptr1 = num_ptr2;

Auerdem sind bestimmte arithmetische und Vergleichs-Operationen zugelassen, die wir in Zusammenhang mit Feldern erklren werden (Abschnitt 7.4.3).
Das folgende Beispiel6 veranschaulicht nochmals den Umgang mit Zeigern; siehe Abbildung 7:
1

#include <stdio.h>

2
3
4
5

int main()
{
int i = 0, j;
6

/* Variablen vom Typ int */

Andreas Westfeld, Folien zur Vorlesung Betriebssysteme, TU Dresden

18

i (100):

j (230):

undef.

undef.

undef.

p (1000):

undef.

100

100

100

Variable (Adresse)

p = &i;

*p = 3;

j = i;

Abbildung 7: Beispiel zu Zeigern.

int *p;
p = &i;
*p = 3;
j = i;
printf("%d %d %d\n", i, j, *p);
return 0;

6
7
8
9
10
11

/*
/*
/*
/*
/*

Zeiger auf int */


Adresse von i an p zuweisen */
3 an Inhalt von p zuweisen */
j den Wert von i zuweisen */
Ausgabe: 3 3 3 */

12

In Zusammenhang mit Zeigern spielt die Zuweisung und Freigabe von Speicher auf dem Heap (Halde) eine besondere Rolle. Der Heap ist ein spezieller Datenbereich des Programms, in welchem das
Programm dynamisch Speicherplatz anfordern kann. Ohne diese Mechanismen knnten Zeiger nur auf
definierte (statische) Variablen verweisen. Mit der Allozierung (Anforderung, Bereitstellung) von Speicherplatz auf dem Heap kann an jeder Stelle des Programms neuer Speicherplatz erzeugt werden. Auf
diesen wird ber Zeiger zugegriffen (die selbst auch wieder im Heap liegen knnen). Auf diese Weise
lassen sich z.B. verkettete Listen implementieren.
Speicher kann mit den Funktionen calloc() und malloc() der Standard-C-Bibliothek angefordert
werden (H EROLD und A RNDT 2001, S. 662). Die Funktion malloc(), auf die wir uns hier beschrnken
wollen, alloziert eine geforderte Anzahl von Bytes auf dem Heap und liefert die Adresse des bereitgestellten Speicherbereiches (also einen Zeiger auf den Speicherbereich) zurck (calloc() initialisiert
diesen Speicherbereich zustzlich mit Nullen, was bisweilen ntzlich ist, aber lnger dauert). Soll Platz
fr ein Feld von 20 float-Elementen bereitgestellt werden, so lautet der Aufruf
float *arr;
arr = malloc(20 * sizeof(float));

wobei die Funktion sizeof() die Lnge eines einzelnen float in Bytes ermittelt. Dieser Speicherplatz kann nach Benutzung wieder freigegeben werden. Dazu dient die Funktion free(). Dieser wird
die Anfangsadresse des dynamisch allozierten Speicherplatzes bergeben (die zuvor allozierte Gre ist
der Heap-Verwaltung bekannt und muss nicht angegeben werden):
free(arr);

Dabei sollte arr keinesfalls auf einen bereits freigebenen Speicherplatz oder auf eine statische Variable
verweisen. Es ist zu beachten, dass C nicht wie andere Programmiersprachen eine automatische garbage collection durchfhrt, die ungenutzten Speicherplatz auf dem Heap freigibt die Freigabe muss
explizit mit free() erfolgen. Dies ist eine hufige Fehlerursache in C-Programmen: wird die Freigabe
vergessen, so kann es zu einem Speicherberlauf kommen, wenn wiederholt Speicher angefordert aber
nicht wieder freigegeben wird (Speicherleck).
19

Wie oben erwhnt wurde, spielt die Anforderung von Speicher auf der Halde vor allem fr Datenstrukturen wie Listen und Bume eine Rolle. Bei diesen befindet sich im angeforderten Speicherbereich wiederum eines oder mehrere Zeiger-Objekte. Folgendes Programm erstellt eine einfach verkettete Liste mit
2 Elementen:
struct list_element {
int i;
double d;
struct list_element *next;
};

1
2
3
4
5
6

int
main()
{
struct list_element *anker = NULL, *tmp;

7
8
9
10
11

tmp = malloc(sizeof(struct list_element));


tmp->i = 1111;
tmp->d = 1.111;
tmp->next = anker;
anker = tmp;

12
13
14
15
16
17

tmp = malloc(sizeof(struct list_element));


tmp->i = 2222;
tmp->d = 2.222;
tmp->next = anker;
anker = tmp;

18
19
20
21
22
23

tmp = anker;
while (tmp) {
printf("%d %g\n", tmp->i, tmp->d);
tmp = tmp->next;
}
exit(0);

24
25
26
27
28
29

30

Dabei ist anker ein Zeiger auf das erste Listen-Element und tmp eine Hilfs-Variable. Der Anker der
Liste wird mit dem Zeiger NULL initialisiert; dieser Wert kennzeichnet das Ende einer Liste (die Liste ist
anfangs also leer). In Zeile 12-16 und 18-22 wird jeweils Speicherplatz fr ein neues Listenelement mit
malloc() erzeugt, das Listenelement wird initialisiert, der Zeiger next wird so verndert, dass er auf
den Anfang der bisherigen Liste zeigt (der jeweils in anker steht), und der Anker verweist jeweils auf
das neue Listenelement. Das letzte Listenelement weist jeweils den Wert NULL in der Komponente next
auf an dieser Stelle endet die Liste. Abbildung 8 veranschaulicht den Aufbau der einfach verketteten
Liste (entsprechend Zeile 12 bis 22) in einzelnen Schritten.
Zeile 24 bis 28 zeigt, wie die angelegte Liste durchmustert werden kann (hier, um die Daten der Listenelemente auszugeben). Die Hilfsvariable tmp wird mit dem Ankerzeiger initialisiert. Solange tmp noch
nicht NULL ist, wird das Element, auf welches tmp verweist, mit printf() ausgegeben. Dann wird
tmp weitergestellt, indem der Zeiger auf das nchste Listenelement, der in der next-Komponente des
aktuellen Listenelements steht, an tmp zugewiesen wird.
7.4.3 Felder
Felder (Arrays) sind eine Zusammenfassung von Elementen des gleichen Typs, auf die mithilfe eines
(ganzzahligen) Index (beginnend bei 0) zugegriffen werden kann. Felder werden wie folgt vereinbart:
20

tmp
anker
2222
2.222
1111
1.111

1111
1.111

1111
1.111

1111
1.111
1111
1.111

2222
2.222

malloc
anker=tmp
tmp>next=anker

malloc
tmp>next=anker

2222
2.222

anker=tmp

Abbildung 8: Schritte beim Aufbau einer einfach verketteten Liste im Speicherabbild. Ganz rechts die
verkettete Liste in symbolischer Darstellung ohne Bezug zum Speicherabbild.

w:

w[0]

w:

w[0]

w:

w[0]

w+1:

w[1]

w+1:

w[1]

w+1:

w[1]

w+2:

w[2]

w+2:

w[2]

w+2:

w[2]

w+3:

w[3]

w+3:

w[3]

w+3:

w[3]

w+4:

w[4]

w+4:

w[4]

w+4:

w[4]

wptr:

wptr:

wptr = w;

wptr:

wptr = w + 3;

wptr;

Abbildung 9: Eindimensionale Felder und Zeiger.

char text[64];
int w[5];
float matrix[2][3];
double *array[10];

/*
/*
/*
/*

Feld mit 64 Zeichen: Zeichenkette */


Feld mit 5 int Elementen */
2D-Feld mit 2 x 3 Elementen */
Feld von 10 Zeigern auf double Elemente */

Eindimensionale Felder sind Zeiger auf einen reservierten Speicherbereich mit der entsprechenden Anzahl von Objekten; der Wert des Zeigers ist also die Adresse des Speicherbereichs. Man kann die Feldvariable also einer Zeigervariablen desselben Typs zuweisen (jedoch nicht umgekehrt); siehe Abbildung
9):
int w[5], *wptr;
wptr = w;
w = wptr;
/* Fehler! */

Leider werden in C mehrdimensionale Felder, die wie matrix in der oben gezeigten Weise statisch
definiert werden, nicht wie Zeiger auf Zeiger behandelt, mit anderen Worten, statische mehrdimensionale
Felder sind keine Felder von Zeigern, die wiederum auf Felder des entsprechenden Datentyps verweisen.
Statt dessen werden alle Elemente eines mehrdimensionalen Feldes im Speicher hintereinander abgelegt,
wobei der hinterste Index am schnellsten variiert (siehe Abbildung 10). Dies fhrt bei der bergabe von
statischen Feldern an Funktionen zu Problemen. Auer bei Feldern konstanter Gre werden in der Praxis deshalb mehrdimensionale Felder von Hand als Zeiger auf Zeiger usw. auf Elemente definiert, wie
21

float **a;
a:

a[0]
a[1]

a[0]:

a[0][0]
a[0][1]
a[0][2]

a[1]:

a[1][0]
a[1][1]
a[1][2]

float b[2][3];
b:

b[0][0]
b[0][1]
b[0][2]
b[1][0]
b[1][1]
b[1][2]

Abbildung 10: Unterschied zwischen zweidimensionalen Feldern, die als Zeiger-Zeiger implementiert
sind (links), und zweidimensionalen Feldern, die explizit als zweidimensionale Feldvariablen definiert
werden (rechts).

es bei a in Abbildung 10 der Fall ist. Tatschlich mssen dabei die einzelnen Teilfelder (wie a[0] und
a[1]) nicht die gleiche Lnge aufweisen (im Gegensatz zu statischen Feldern wie matrix) (H EROLD
und A RNDT 2001, S. 630).
Felder knnen bei ihrer Definition initialisiert werden:
float matrix[2][3] = {{9.0, 8.0, 7.0}, {6.0, 5.0, 4.0}};

Es ist auch die teilweise Initialisierung erlaubt; C99 gestattet sogar die gezielte Initialisierung einzelner
Elemente (H EROLD und A RNDT 2001, S. 611).
Der Zugriff auf Felder erfolgt mit ganzzahligen Indizes, wobei das erste Feld-Element jeder Dimension
immer den Index 0 hat:
int i;
matrix[1][2] = 1.234;
i = w[3];

Auf mehrdimensionale Felder wie matrix kann mit matrix[i][j] zugegriffen werden, wobei
matrix[i] einen Zeiger auf ein Feld von Elementen liefert (Typ: float *) und die nochmalige
Indizierung mit [j] ein Element dieses Feldes (Typ float). Dasselbe gilt natrlich fr Felder, welche als Zeiger auf ein Feld von Zeigern definiert wurden. Es erfolgt keine Bereichsberprfung fr den
Index!
In Zusammenhang mit Feldern erhalten auch Operationen mit Zeigern eine Bedeutung (H EROLD und
A RNDT 2001, S. 539). Es ist erlaubt, Zeiger um ganzzahlige Werte zu erhhen oder zu verringern. Dabei rckt der Zeiger um Vielfache der Speicherlnge des dem Zeiger zugrundeliegenden Typs vor oder
zurck. So zeigt nach
int w[5], *wptr;
wptr = w + 3;
wptr--;

der Zeiger wptr auf das Element im Feld w mit dem Index 2 (und nicht auf das dritte Byte des Feldes);
siehe Abbildung 9. Auerdem drfen Zeiger (gleichen Typs) verglichen werden (Operatoren == != <
> <= >=). Der Vergleich zwischen den Zeigern hat dasselbe Ergebnis wie ein Vergleich zwischen den
22

Indizes der Feld-Elemente, auf welche die Zeiger verweisen. Erlaubt ist auerdem der Vergleich mit
dem Wert NULL oder 0; dies ist der sogenannte NIL-Pointer (not in list), der z.B. zur Kennzeichnung
von Listenenden benutzt werden kann. Die Subtraktion zweier Zeigervariablen ergibt einen Wert, der
die Anzahl der Elemente (nicht der Bytes) zwischen den Feld-Elementen angibt, auf welche die Zeiger
zeigen.
Neben der Mglichkeit, auf Feld-Elemente ber einen Index in eckigen Klammern zuzugreifen (z.B.
w[i]) kann man dies auch ber den Inhaltsoperator und arithmetische Zeiger-Operationen tun (z.B.
*(w+i)). Da, wie oben gesagt wurde, Zeiger und Felder weitgehend gleich behandelt werden, funktioniert dies auch mit Zeigern, z.B. wptr[i] oder *(wptr + i).
Folgende Ausdrcke sollen die Beziehung zwischen Zeigern und eindimensionalen Feldern veranschaulichen:
int w[5], *wptr;
wptr = w;
wptr + 1
w+1
&wptr[1]
&w[1]
*(wptr+1)
*(w+1)

/*
/*
/*
/*
/*
/*

Zeiger auf w[1] */


Zeiger auf w[1] */
&(wptr[1]) Zeiger auf w[1] */
&(w[1]) Zeiger auf w[1] */
w[1] */
w[1] */

Einige Beispiele fr Ausdrcke mit nicht-statischen zweidimensionalen Arrays:


float **a;
a
*a
**a
a[1]
a[1][2]
a+1
*(a+1)
*(a+1)+2
&a[1][2]
*(*(a+1)+2)

/*
/*
/*
/*
/*
/*
/*
/*
/*
/*

Basisadresse des Feldes, float** */


Zeiger auf erstes Teilfeld a[0], float* */
Zeiger auf erstes Element des ersten Teilfelds a[0][0], float */
Zeiger auf zweites Teilfeld, float* */
drittes Element des zweiten Teilfelds, float */
Zeiger auf Zeiger auf zweites Teilfeld, float** */
a[1], Zeiger auf a[1][0], float* */
Zeiger auf a[1][2], float* */
Zeiger auf a[1][2], float* */
a[1][2] */

Machen Sie einen Vorschlag, wie der Inhalt zweier Felder ausgetauscht werden kann, ohne die Daten
kopieren zu mssen.
Schreiben Sie eine Funktion, welche das Element mit dem Index ia in einem Feld a gegen das
Element mit dem Index ib in einem Feld b austauscht. Der Prototyp der Funktion sei void
cross_over(float *a, int ia, float *b, int ib). Schreiben Sie eine Variante der
Funktion, welche Felder und Indizes verwendet, und eine zweite Variante, welche nur mit ZeigerArithmetik arbeitet. Zum Test knnen Sie die Funktion print_array() benutzen, welche ein Feld
auf der Standard-Ausgabe ausgibt:
1
2
3
4

void
print_array(char *str, float *a, int sz)
{
int i;

printf("%s: ", str);


for (i = 0; i < sz; i++)
printf("%g ", a[i]);
printf("\n");

6
7
8
9
10

23

!
!

C9

C10

Der Aufruf erfolgt z.B. mit print_array("a", a, DIM). (Musterlsung: cross_over.c).


7.4.4 Zeichenketten
Fr Zeichenketten existiert in C kein eigener Typ. Statt dessen werden Zeichenketten als Felder des Typs
char definiert:
char my_string[200];

Auch fr Zeichenketten knnen Literale angegeben werden. Dabei stehen die Zeichen-Literale in Anfhrungsstrichen. Der Compiler hngt automatisch die Ende-Kennzeichnung \0 an:
"A"
"Das ist eine Zeichenkette\n"
"\"abc\""

/* Folge von Zeichen A und \0 */


/* abgeschlossen mit Newline u. \0 */
/* Zeichenkette "abc" und \0 */

Zeichenketten-Literale liegen im initialisierten Datenbereich des Programms. Sie sind durch ihre Adresse
charakterisiert, welche einem Zeiger auf char zugewiesen werden kann:
char *my_string = "the string";

Zeichenketten knnen bei ihrer Definition initialisiert werden:


char another_string[20] = "initial content";

Sptere Zuweisungen an Zeichenketten-Felder mssen jedoch auf andere Weise erfolgen (z.B. durch die
Funktion strcpy der Standardbibliothek, siehe Abschnitt 11.2):
strcpy(another_string, "new content");

7.4.5 Strukturen und Unions


In einer Struktur knnen mehrere Objekte unterschiedlichen Typs zusammengefasst werden. So wird mit
struct {
int day;
char month[10];
int year;
} birthday, *date_ptr;

eine Strukturvariable birthday mit den angegebenen Komponenten sowie ein Zeiger date_ptr auf
solche Strukturen vereinbart. Der Zugriff erfolgt mit
date_ptr = &birthday;
birthday.day = 28;
date_ptr->year = 1999;

/* Adress-Operator & */
/* Element-Zugriff mit . */
/* Element-Zugriff ueber Zeiger mit -> */

Strukturen knnen initialisiert werden, z.B.:


struct {
int day;
char month[10];
int year;
} extra_fun_day = {11, "November", 2011};

24

bdf_prm:

b_un.nfract
b_un.ndirty
b_un.interval

data[0]
data[1]

b_un.age_buffer

data[2]
data[3]

Abbildung 11: Beispiel fr eine Union.

Hier bietet C99 komfortable Mglichkeiten zur Initialisierung einzelner Komponenten (H EROLD und
A RNDT 2001, S. 705).
Eine spezielle Form des Strukturtyps sind Unions. Sie bieten die Mglichkeit, dass sich Variablen unterschiedlicher Strukturtypen ein und denselben Speicherplatz teilen, d.h. im Programm kann der Inhalt der
Union je nach Zugriff auf verschiedene Weise interpretiert werden. So kann man mit
union {
unsigned int i;
float f;
} fu;

1
2
3
4
5

fu.f = -12.75;
printf("i = 0x%08x\n", fu.i);

6
7

die IEEE-754-Kodierung einer gegebenen Gleitkommazahl ausgeben lassen. Bisweilen ist auch gewnscht, auf Elemente sowohl mit einem Namen als auch ber einen Index zuzugreifen, z.B. um mehrere
Elemente in einer Schleife initialisieren zu knnen7 :
union {
struct {
int nfract;
int ndirty;
int interval;
int age_buffer;
} b_un;
int data[4];
} bdf_prm;

Hier wre bdf_prm.b_un.ndirty dasselbe Objekt wie bdf_prm.data[1]; siehe Abbildung 11


(H EROLD und A RNDT 2001, S. 784).
Die Verwendung von Unions kann zu Problemen fhren, wenn Feld-Elemente vom Compiler nicht unmittelbar hintereinander, sondern (zur Effizienz-Steigerung) auf glatte Adressen der Datenbusbreite
(Vielfache der Datenbusbreite) gelegt werden.
7.4.6 Bitfelder
Bitfelder sind eine spezielle Form von Strukturen, bei denen den Komponenten einzelne Bits einer Integergre zugewiesen werden. Bitfelder werden eher selten verwendet, statt dessen wird mit Bitmasken
gearbeitet (siehe Abschnitt 8.2).
7

Beispiel hnlich zu /usr/src/linux-2.4/fs/buffer.c

25

7.5 Typdefinitionen
Durch eine Typdefinition kann ein Synonym fr zuvor einen definierten Typ (Grunddatentyp, nutzerdefinierter Datentyp oder durch Typdefinition erklrter Typ) festgelegt werden:
typedef int Index, Flags;
typedef unsigned char Byte;
typedef struct {
short year;
char month[4];
short day;
} Date;
typedef float *DataPtr;

Dieses Synonym kann dann zur Definition von Variablen benutzt werden:
Index idx;
Byte b;
Date os_exam_passed;
DataPtr p;

/*
/*
/*
/*

int idx; */
unsigned char b; */
struct {...} os_exam_passed; */
float *p; */

Bei Aufzhlungstypen, Strukturen und Unions gibt es allerdings eine Alternative, bei der der Typname
direkt hinter enum, struct oder union angegeben wird:
enum Position {ceo, researcher, worker};
struct DatabaseEntry {
char name[50];
int age;
float salary;
enum Position pos;
};

Hier werden die Typen struct DatabaseEntry und enum Position deklariert. Bei der Definition von Variablen dieser Typen muss das Schlsselwort enum, struct oder union mit angegeben
werden:
struct DatabaseEntry person1;
enum Position myLastJob;

Die Variable person1 ist also vom Typ struct DatabaseEntry, die Variable myLastJob vom
Typ enum Position.
Fr die Definition verketteter Listen mit Strukturen muss diese Form verwendet werden, da der nextZeiger ein Zeiger auf den gerade definierten Typ ist. Allerdings kann man auch hier durch Verwendung
von typedef einen anderen Typbezeichner (elem_t) erklren:
typedef struct _elem_t {
int i;
/*
float f;
/*
struct _elem_t *next; /*
} elem_t;
/*
elem_t *anker;

data */
data */
pointer to next element*/
alias type name */

/* list */

Typbezeichner sollten anhand ihres Namens kenntlich gemacht werden. Hier beginnt bspw. der Typbezeichner mit einem Grobuchstaben oder endet, wie im Linux-Kernel blich, auf _t.
26

7.6 Export, Lebensdauer und Sichtbarkeit


Bei der Definition einer Variablen stellen sich drei Fragen:
Export Kann diese Variable in anderen Objektdateien benutzt werden?
Lebensdauer Wann wird Speicherplatz fr die Variable angelegt und wann wieder freigegeben?
Sichtbarkeit An welchen Stellen im Quelltext kann die Variable unter ihrem Namen angesprochen werden?
Von zentraler Bedeutung fr die beiden ersten Eigenschaften sind die Vorstze extern und static,
die vor die Variablen-Definition eingefgt werden knnen. Im folgenden werden nur die Standardflle
dargestellt.
7.6.1 Export
Eine Variable wird in der Regel nur in dem Modul benutzt, in dem sie definiert wurde. Manchmal ist es
jedoch wnschenswert, die Variable auch in anderen Modulen nutzen zu knnen. Die Vorstze extern
und static entscheiden, ob die Variable an andere Objektdateien exportiert wird:
Wird eine Variable auerhalb von Funktionen ohne Vorsatz oder als extern definiert, so kann
auf sie auch in anderen Objektdateien zugegriffen werden. Wird in einem anderen Modul derselbe
Variablen-Bezeichner ebenfalls ohne Vorsatz oder als extern definiert, so handelt es sich um
dieselbe Variable (um denselben Speicherplatz).
Die Konvention ist, die Variable ohne extern zu schreiben in dem Modul, zu dem sie inhaltlich
gehrt (sie ist dann trotzdem extern), und in allen anderen Modulen, die die Variable nur mitnutzen, das Schlsselwort extern zu verwenden. blicherweise wird eine c-Datei existieren,
in der die Variable ohne Vorsatz angefhrt ist, und eine h-Datei, in der sie mit extern angefhrt
ist. Diese h-Datei kann von den anderen Quell-Dateien eingebunden werden (siehe untenstehendes
Beispiel).
Wird eine Variable auerhalb von Funktionen als static definiert, so kann auf sie nicht in
anderen Objektdateien zugegriffen werden, selbst wenn dort eine Variable gleichen Namens als
extern definiert ist. (Es gbe in diesem Fall im ausfhrbaren Programm mehrere Variablen desselben Namens. berprfen Sie das an einem einfachen Code-Beispiel mit dem Programm nm.)
Quelldatei my_data.c:
int i = 123, j = 456;
float x = 9.87;

1
2

Header-Datei my_data.h:
1
2

#ifndef _MYDATA_H_
#define _MYDATA_H_

3
4
5

extern int i, j;
extern float x;

6
7

#endif

27

C11

Quelldatei my_main.c:
#include "my_data.h"
#include <stdio.h>
#include <stdlib.h>

1
2
3
4

int
main()
{
printf("i = %d, j = %d, x = %g\n", i, j, x);
}

5
6
7
8
9

7.6.2 Lebensdauer
Eine Variable wird zu einem bestimmten Zeitpunkt whrend der Abarbeitung des Programms angelegt
(es wird Speicherplatz bereitgestellt) und wieder zerstrt (ihr Speicherplatz wird freigegeben):
Variablen, die auerhalb von Funktionen definiert werden (globale Variablen) existieren ber die
gesamte Laufzeit des Programms (unabhngig vom verwendeten Vorsatz).
Variablen, die innerhalb von Funktionen (oder generell Blcken; siehe Abschnitt 9.3) ohne Vorstze definiert werden, existieren nur vom Eintritt in den Block bis zum Verlassen des Blocks.
Fr diese Variablen wird beim Eintritt in den Block Speicherplatz auf dem Stapel reserviert
der Stapel ist ein Zwischenspeicher der Organisationsform first-in last-out. Dies geschieht durch
Verstellen des Stapelzeigers und Zuweisen einer Adresse in dem dadurch erzeugten Stapelbereich.
Beim Verlassen des Blocks wird der Stapelzeiger zurckgestellt, die Variable muss dann als nicht
mehr existent betrachtet werden ihr Speicherplatz kann berschrieben werden (siehe auch Abschnitt 10.5).
Variablen, die innerhalb von Funktionen mit dem Vorsatz static definiert werden, existieren
ber die gesamte Laufzeit des Programms. Beim Wiedereintritt in eine Funktion findet sich dort
also der letzte Wert der Variable wieder. Der statischen Variable kann bei der Definition ein Initialwert zugewiesen werden, der einmalig bei Programmbeginn (und nicht bei jedem Eintritt in
die Funktion) gesetzt wird. Derartige Variablen sollten mglichst vermieden werden z.B. werden Funktionen durch ihre Verwendung nicht wiedereintrittsfest, d.h. sie knnen nicht rekursiv
aufgerufen und nicht von mehreren Prozessen gleichzeitig benutzt werden.
1

int i = 123;

/* i existiert bis zum Programmende */

2
3
4
5
6
7
8

int
fct(int j)
{
int k;
return i + j + k;
}

/* j existiert nur innerhalb von fct */


/* k existiert nur innerhalb von fct */

9
10
11
12
13

int
main()
{
int l = fct(12);

14

exit(0);

15
16

/* l existiert nur innerhalb von main */


/* d.h. bis zum Programmende */

28

7.6.3 Sichtbarkeit
Whrend des Compilierens muss der Compiler entscheiden, ob er eine Variable eines gegebenen Namens
sieht oder nicht:
Variablen, die auerhalb von Funktionen definiert werden, sind vom Punkt ihrer Definition bis zum
Ende der Quelltextdatei (c-Datei) sichtbar, auch in Funktionen (globale Variable). Sie liegen im
Datenbereich des Programms.
Variablen, die innerhalb von Funktionen oder Blcken definiert werden, sind nur vom Punkt ihrer
Definition bis zum Ende des Blocks sichtbar.
Wird innerhalb eines Blocks eine Variable definiert, die den gleichen Namen wie eine globale
Variable oder eine Variable eines umgebenden Blocks aufweist, so ist nur die Variable im innersten
Block sichtbar.
So liefert das folgende Programm
#include <stdio.h>
#include <stdlib.h>

1
2
3

int x = 777;

4
5

int
main()
{
printf("x = %d\n", x);
int x;
x = 123;
{
printf("x = %d\n", x);
int x;
x = 456;
{
printf("x = %d\n", x);
int x;
x = 789;
printf("x = %d\n", x);
}
printf("x = %d\n", x);
}
printf("x = %d\n", x);
exit(0);
}

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

die Ausgabe
x
x
x
x
x
x

=
=
=
=
=
=

777
123
456
789
456
123

29

7.7 Konstanten
ANSI-C erlaubt die Definition von Konstanten mit dem Schlsselwort const, z.B.
const int a = 12;

Konstanten knnen gelesen, aber nicht beschrieben werden; nur unmittelbar bei der Definition der Konstanten ist die Wertzuweisung erlaubt. Das Schlsselwort const wird weiterhin auch in den formalen
Argumenten von Funktionen verwendet um anzuzeigen, welche Argumente (insbesondere solche, die
via Zeiger bergeben werden) im Inneren der Funktion nicht modifiziert werden drfen. So bedeutet
void fct(const char *str) { ... }

dass str auf const chars zeigt, also der Inhalt von str nicht verndert werden darf. Abschnitt
11.5 liefert einige Beispiele fr Funktionsdefinitionen mit const. In Zusammenhang mit Zeigern wird
const folgendermaen verwendet (H EROLD und A RNDT 2001, S. 458):
const int *zeiger_auf_konstante = &variable;
/* Inhalt von zeiger_auf_konstante darf nicht verndert werden,
Zeiger selbst darf verndert werden */
int *const konstanter_zeiger = &variable;
/* Inhalt von konstanter_zeiger darf verndert werden,
Zeiger selbst darf nicht verndert werden */
const int *const konstanter_zeiger_auf_konstante = &variable;
/* weder Zeiger noch Inhalt drfen verndert werden */

8 Ausdrcke und Operatoren


8.1 Ausdrcke
Ausdrcke in C sind einfache Ausdrcke oder durch Operatoren verknpfte Ausdrcke. Jeder Ausdruck
besitzt einen Wert. Einfache Ausdrcke knnen sein:
Namen
Literale
(Ausdruck)
Name(Ausdruck,Ausdruck,...)
Name[Ausdruck]
Ausdruck.Name
Ausdruck->Name

Variablen, Konstanten
z.B. 100, A, "abc"
Klammerung zum ndern der Rangordnung
von Operatoren
Funktionsaufruf mit Ausdrcken
in der Argumentliste
Zugriff auf Feldelement ber einen Index
Verweis auf Komponenten eines Struktur-Typs
(Ausdruck muss ein L-Value sein)
entspricht (*Ausdruck).Name
(Ausdruck muss ein Zeiger sein)

Ein L-Value (left value) ist ein spezieller Ausdruck. Dieser Ausdruck verweist auf ein Daten-Objekt.
Bestimmte Operatoren lassen an bestimmten Stellen nur L-Values zu, z.B. verlangt der ZuweisungsOperator = einen L-Value auf der linken Seite. Das einfachste Beispiel fr einen L-Value ist ein Variablenname. Elemente von Feldern, Strukturen/Unions und Bitfeldern sind ebenfalls L-Values. Zeigervariablen und Strukturvariablen sind L-Values, Feldvariablen hingegen nicht.
30

Tabelle 2: Operatoren von C


Unre Operatoren
arithmetische
-x
--x
x-++x
x++
logische
!x
bitweise
~x
Adress-Op.
*x
&x
Typ-Wandlung (typ)
Binre Operatoren
arithmethische + - * /
%
Vergleichsop.
< >
<= >=
== !=
logische
&& ||
bitweise
<< >>
& |
^
Zuweisungsop. =
op=

Bedingungsop.

?:

Komma-Op.

Vorzeichenumkehr
Dekrement vor Verwendung des Wertes
Dekrement nach Verwendung des Wertes
Inkrement vor Verwendung des Wertes
Inkrement nach Verwendung des Wertes
logische Negation
bitweises Komplement
Inhalt des Zeigers
Adresse des Objektes
Typ-Wandlung zum Datentyp typ
arithmetische Operatoren
Rest bei ganzzahliger Division
kleiner als, grer als
kleiner gleich, grer gleich
gleich, ungleich
logisches AND und OR
bitweise Verschiebung nach links/rechts
bitweise AND- und OR-Verknpfung
bitweise XOR-Verknpfung
einfache Wertzuweisung
a op= b entspricht a = a op (b)
fr op knnen folgende Operatoren stehen:
+ - * / % << >> & | ^
a?b:c hat den Wert des Ausdrucks b
falls Ausdruck a logisch wahr ist,
ansonsten den Wert des Ausdrucks c
a,b hat den Wert des Ausdrucks b
der nach Auswertung des Ausdrucks a
ermittelt wird

8.2 Operatoren
Operatoren werden zur Bildung von Ausdrcken verwendet. Tabelle 2 stellt alle C-Operatoren dar.
Die arithmetischen binren Operatoren sowie das unre Minus (Vorzeichenumkehr) funktionieren wie in
anderen Sprachen. Fr die ganzzahlige Division gibt es zustzlich den binren Operator %, welcher den
Rest der Division bestimmt (Modulo-Operator).
Im Gegensatz zu anderen Programmiersprachen besitzt eine Wertzuweisung = wie jeder andere Ausdruck
einen Wert (nmlich den zugewiesenen). Erlaubt sind also Mehrfachzuweisungen wie
a = b = c = 1

welche vom Compiler entsprechend der Assoziativitt (siehe Abschnitt 8.3) als
a = (b = (c = 1))

31

interpretiert werden. Allgemein kann der Zuweisungsoperator in beliebigen Ausdrcken wie jeder andere
Operator benutzt werden, oft erfolgt die Auswertung von Bedingungen mit Konstrukten wie
while ((c = getchar()) != \n)
putchar(c);

Stolperstein: der Compiler bemngelt folgende Verzweigungsbedingung nicht als Fehler (deshalb: mit
-Wall alle Warnungen einschalten!), in der statt dem Vergleichsoperator == versehentlich der Zuweisungsoperator = verwendet wurde:
if (x = y) {
...
}

Fr einige binre Operationen existieren zusammengesetzte Zuweisungsoperatoren. So entspricht a +=


b der Operation a = a + b. Auch diese Operatoren knnen wie der Operator = kaskadiert werden
(s.o.).
Die Inkrement- und Dekrement-Operatoren ++ und -- existieren in zwei Formen, in Prfix-Notation
(++x, --x) und in Postfix-Notation (x++, x--). Bei der Prfix-Notation wird zuerst der Wert inkrementiert bzw. dekrementiert und danach im Ausdruck verwendet. Bei der Postfix-Notation wird der alte
Wert im Ausdruck verwendet und danach inkrementiert. Das Beispiel zeigt die Wirkung:
1
2
3
4
5
6
7

int x, y;
x = 3;
y = ++x;
/* jetzt ist x=4 und y=4 */
x = 3;
y = x++;
/* jetzt ist x=4 und y=3 */

ANSI-C89 kannte keinen speziellen Typ fr Wahrheitswerte; bei C99 ist der Typ _Bool hinzugekommen. Generell wird ein Ausdruck eines Grundtyps als wahr interpretiert, wenn er verschieden von 0 ist,
ansonsten als falsch (das gilt brigens auch fr Gleitkommazahlen und Zeiger). Die Vergleichsoperatoren
liefern den ganzzahligen Wert 0 (falsch), wenn der Vergleich nicht zutrifft, ansonsten 1 (verschieden
von 0, also wahr). Die logischen Operatoren ! (logische Negation), && (logisches AND) sowie ||
(logisches OR) verknpfen Wahrheitswerte (0, ungleich 0) und liefern Wahrheitswerte (0 und 1). Der
schaltalgebraische Ausdruck a (b c) lautet in C !(a || (b && c)).
Bei den logischen Operatoren && und || vermeidet C unntige Auswertungen von Teilausdrcken (H E ROLD und A RNDT 2001, S. 50). Ist z.B. der erste Ausdruck von
(a < b) && (b >= 0) && (a < 0)

falsch, so werden die beiden anderen Ausdrcke nicht ausgewertet, denn der Gesamtausdruck bleibt bei
einer AND-Verknpfung auf jeden Fall falsch. Bei einem Ausdruck wie
(a < b) || (b >= 0) || (a < 0)

wird die Auswertung der restlichen Ausdrcke eingespart, wenn sich bereits der erste Ausdruck als wahr
erweist eine OR-Verknpfung ist wahr, wenn mindestens einer der Ausdrcke wahr ist.
C untersttzt auch die bitweise Verknpfung von ganzzahligen Typen (siehe Tabelle 3). Dabei wird die
entsprechende Operation einzeln auf jedes Bit bzw. die Bits an derselben Position in zwei Binrworten
32

Tabelle 3: Zweistellige bitweise Verknpfungen. Gezeigt ist jeweils nur eine Bitstelle.
x y x & y x | y x ^ y
0 0
0
0
0
0 1
0
1
1
1 0
0
1
1
1 1
1
1
0

angewandt. Der Operator & verknpft die Bits mit dem Operator AND, der Operator | mit OR, ^ mit
XOR (exklusives OR) und der unre Operator ~ negiert alle Bits (Komplement). Diese Operationen
werden hufig eingesetzt, um in einem ganzzahligen Wert mehrere Flags zu kodieren; z.B. werden beim
Systemruf open() (ffnen einer Datei) folgende Konstanten als Flag verwendet:
1
2
3
4
5
6
7
8
9

#define
#define
#define
#define
#define
#define
#define
#define
#define

O_RDONLY
O_WRONLY
O_RDWR
O_CREAT
O_EXCL
O_NOCTTY
O_TRUNC
O_APPEND
O_NONBLOCK

00
01
02
0100
0200
0400
01000
02000
04000

Man beachte die oktalen Literale, bei denen jeweils genau eine Binrstelle 1 ist (berprfen Sie diese
Behauptung). Beim Aufruf werden dann ber eine OR-Verknpfung alle gewnschten Bits gesetzt (hier
wird eine Datei zum Lesen und Schreiben geffnet, die aktuelle Dateiposition wird das Dateiende, und
die Dateioperationen sind nicht-blockierend):

C12

char buf[100];
fd = open("mydata.dat", O_RDWR | O_APPEND | O_NONBLOCK);
write(fd, "some text output\n", 17);
read(fd, buf, 100);
close(fd);

1
2
3
4
5

(Welcher Wert wird im letzten Argument an open() bergeben?) Bei der Abfrage der im letzten Argument bergegebenen Flags im Inneren der Bibliothek knnte z.B. folgender Code auftauchen:

C13

int
open(const char *pathname, int flags)
{
...
if (flags & O_APPEND) {
/* verschiebe aktuelle Dateiposition zum Ende */
...
}
...
}

1
2
3
4
5
6
7
8
9
10

Hier wird mit dem AND-Operator das Wort flags maskiert, d.h. dass nur das Bit an der entsprechenden Position (welche ist das im Beispiel?) in flags seinen Wert beibehlt und alle anderen auf 0 gesetzt werden. In Abhngigkeit davon, ob das Bit 0 ist oder 1, ist auch der Wahrheitswert des Ausdruckes
33

C14

falsch oder wahr. Wie wrde eine (einzige) Abfrage aussehen, um zu berprfen, ob O_WRONLY und
O_RDWR gleichzeitig gesetzt sind eine Kombination, die nicht erlaubt ist? Testen Sie Ihre Lsung in
einem Programm, indem sie eine Funktion void my_open(int flags) schreiben und dieser verschiedene Flag-Kombinationen bergeben. Es soll eine Fehlermeldung ausgegeben werden, wenn obige
Bedingung erfllt ist (Musterlsung: open_error.c).
Mithilfe von bitweisen AND- und OR-Verknpfungen lassen sich gezielt Bits in einem Datenwort setzen,
lschen und ndern. Wenn in der Konstante SINGLE_BIT_FLAG nur genau eine Stelle eine 1 fhrt,
dann wird mit der Anweisung
flags |= SINGLE_BIT_FLAG;

dieses Bit in der Variable flags gesetzt, mit


flags &= ~SINGLE_BIT_FLAG;

das Bit gelscht und mit


flags ^= SINGLE_BIT_FLAG;

gendert (0 zu 1 bzw. 1 zu 0), whrend alle anderen Bits unverndert bleiben.


Fr spezielle Aufgaben existieren die Verschiebe-Operatoren << und >>, welche ein Bitwort um die
angegebene Zahl von Stellen nach links bzw. rechts verschieben. Eine elegante Definition der obigen
Flags wre also
#define
...
#define
#define
#define
#define
...

1
2
3
4
5
6
7

BIT(POSITION)

(1 << POSITION)

O_WRONLY
O_RDWR
O_CREAT
O_EXCL

BIT(0)
BIT(1)
BIT(6)
BIT(7)

Kompakter (aber leider auch schlecht lesbarer Code) lsst sich mit dem Bedingungsoperator (?:) und
dem Komma-Operator (,) produzieren. Der Bedingungsoperator (siehe Tabelle 2) kann z.B. zur Ermittlung des Maximums zweier Werte eingesetzt werden:
maxval = (val1 > val2) ? val1 : val2;

Der Komma-Operator wird meist bei der for-Anweisung verwendet, wenn mehrere Zhler verndert
werden mssen. Die for-Anweisung (Abschnitt 9.5.2) erwartet an dieser Stelle einen Ausdruck, es
mssen aber mehrere unabhngige Operationen ausgefhrt werden. Um z.B. einem Feld jedes zweite
Element eines anderen Feldes zuzuweisen, knnte man folgenden Code benutzen:
1
2
3
4
5

#define SIZE 10
int i, j;
float feld1[SIZE], feld2[2 * SIZE];
for (i = 0, j = 0; i < SIZE; i++, j+=2)
feld1[i] = feld2[j];

34

C15

Tabelle 4: Prioritten und Assoziativitt von C-Operatoren. Operatoren in der gleichen Zeile haben die
gleiche Prioritt. Von oben nach unten nimmt die Prioritt ab.
Operator
Assoziativitt
() [] -> .
links nach rechts
unre Operatoren, Typ-Wandlung 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
= op=
rechts nach links
, (Komma-Operator)
links nach rechts

Hier werden einerseits i=0 und j=0 und andererseits i++ und j+=2 mit dem Komma-Operator verknpft (und damit zu einem Ausdruck); ansonsten sei von der Verwendung dieses Operators eher abgeraten, insbesondere wenn die Ausdrcke voneinander abhngen.
Der Typ-Wandlungsoperator wird in Abschnitt 8.4 behandelt, die Adress-Operatoren wurden bereits in
Abschnitt 7.4.2 vorgestellt.
Einige Operatoren sind in ihren Argumenten beschrnkt. Der Zuweisungsoperator = verlangt einen LValue auf der linken Seite, ebenso die Zuweisungsoperatoren op=. Folgende Anweisungen wren z.B.
offenbar sinnlos:
(a + b / c) = (d * e + f);
(x - y) += z;

(Gibt es eine Mglichkeit, durch anderen Klammerungen erlaubte Anweisungen zu erzeugen? Musterlsung: lvalue.c) Die Inkrement- und Dekrement-Operatoren sowie der Adress-Operator & mssen
sich ebenfalls auf einen L-Value beziehen.

C16

8.3 Prioritten und Assoziativitt


Tabelle 4 zeigt die Prioritt und die Assoziativitt von Operatoren. Die Prioritt legt die Reihenfolge
der Ausfhrung von Operatoren (und damit die Strukturierung eines Ausdrucks) fest, whrend die Assoziativitt die Reihenfolge der Ausfhrung bei Operatoren gleicher Prioritt (also auch beim mehrfachen
Auftreten des gleichen Operators) bestimmt (H EROLD und A RNDT 2001, S. 72).
Der Ausdruck a == b || c < d, wie er in hnlicher Form hufig als Verzweigungs- oder Schleifenbedingung auftritt, wird entsprechend der Prioritt als (a == b) || (c < d) ausgewertet. Wegen
Assoziativitt der Operatoren von links nach rechts wird bspw. der Ausdruck a - b - c als (a b) - c ausgewertet, der Ausdruck !!!x aber als !(!(!x)).
Welchen Wert hat der Ausdruck i ^ k | 4 - j << 2 * k? Setzen Sie Klammern entsprechend
der Prioritten. Testen Sie Ihre Lsung in einem Programm (Musterlsung: op_test.c).
35

C17

Tabelle 5: Mathematische Funktionen


mathematische Funktionen C-Funktionen
Rundung
floor, round, ceil
Absolutbetrag
abs, fabs
trigonometrische
sin, cos, tan
asin, acos, atan, atan2
sinh, tanh, cosh
sonstige
exp, log, log10, pow
sqrt, hypot
Zufallszahlen (Std.-C-Lib) srand, rand, srand48, drand48

8.4 Typ-Umwandlungen
In arithmetischen Ausdrcken knnen Argumente mit verschiedenen Typen gemischt auftreten. Argumente vom Typ char und short werden immer als int behandelt (H EROLD und A RNDT 2001,
S. 133). Wo Typen unterschiedlicher Lnge benutzt werden, erfolgt implizit eine Umwandlung zur greren Lnge, also wird eine Operation mit float und double Operanden einen double produzieren.
Treten ganzzahlige und Gleitkomma-Typen gemeinsam auf, so ist das Ergebnis ein Gleitkomma-Typ.
Zuweisungen zwischen verschiedenen Typen sind erlaubt. Der Wert wird erhalten, auer wenn
die Lnge zu klein ist, um den Wert aufzunehmen (die Folge ist die Zerstrung des Wertes) oder
einem vorzeichenlosen Typ der Wert eines vorzeichenbehafteten Typs mit negativem Wert zugewiesen wird (die Folge ist die Zerstrung des Wertes) oder
ein Gleitkommatyp an einen ganzzahligen Typ zugewiesen wird (die Folge ist ein Abschneiden
der Nachkommastellen).
Fr explizite Typumwandlungen kann der Typ-Wandlungs-Operator (typ) eingesetzt werden:
int i = 3, j = 2, k;
double d = 1.4;

1
2
3
4
5
6

k = i * d;
k = (int) (i * d);
k = i * (int) d;

/* Resultat: k = 4 */
/* Resultat: k = 4 */
/* Resultat: k = 3 */

k = 10 * i / j;
k = 10 * (i / j);
k = 10 * ((double) i / j);

/* Resultat: k = 15 */
/* Resultat: k = 10 */
/* Resultat: k = 15 */

7
8
9
10

8.5 Mathematische Funktionen


Da Gleitkomma-Berechnungen in Betriebssystemen kaum eine Rolle spielen, seien hier (Tabelle 5) nur
die wichtigsten Funktionen aus libm.a (Header-Datei: math.h) angefhrt (die Funktionen zur Erzeugung von Zufallszahlen finden sich in der Standard-C-Bibliothek). Details finden sich in den Man-Pages.

36

9 Anweisungen
Die Anweisungen einer Sprache steuern den Programmablauf. C kennt einfache Anweisungen, leere
Anweisungen, Verbundanweisungen, verschiedene Verzweigungs- und Schleifen-Anweisungen sowie
Sprung-Anweisungen.

9.1 Einfache Anweisungen


In C wird aus einem Ausdruck eine einfache Anweisung, indem man ein Semikolon anhngt (das Semikolon kennzeichnet also das Ende einer Anweisung und ist kein Trennzeichen wie in anderen Sprachen).
Einfache Anweisungen sind z.B.
factor *= 0.75;
--counter;
calc(a, b);
ii && jj;

/* erlaubt, aber warning: statement with no effect */

Ausdrcke wie ii && jj haben keine Wirkung auf die Daten des Programms; der Compiler vermutet
deshalb einen Fehler und gibt deshalb eine Warnung aus. In diesem Fall sollte das Ergebnis mit dem
Operator = an einen L-Value zugewiesen werden.
Bsp.: Die while-Schleife erwartet eine Anweisung (siehe Abschnitt 9.5.1). Mit einer einfachen Anweisung erhalten wir z.B.:
while (i < 10)
a[i++] = i;

9.2 Leere Anweisung


Die leere Anweisung besteht nur aus einem Semikolon. Eine Endlosschleife mit while she z.B. so
aus:
while (1);

9.3 Verbundanweisung (Block)


Mehrere Anweisungen knnen durch Einschlieen in geschweifte Klammern zu einem Block (Verbundanweisung) zusammengefat werden. Variablen, die innerhalb eines Blocks vereinbart werden, sind nur
in diesem Block (und in dort eingeschachtelten Blcken) sichtbar; ihre Lebensdauer ist auf den Block
beschrnkt (siehe Abschnitt 7.6).
Eine Blockanweisung am Beispiel der while-Schleife:
while (i < 10) {
a[i] = i;
i += 2;
}

37

falsch
Ausdruck

Ausdruck

wahr

Anweisung_1

wahr
Anweisung_2

Anweisung_1

falsch
Anweisung_2

Abbildung 12: Verzweigung: Programmablaufplan (links) und Struktogramm (rechts).

9.4 Verzweigungsanweisungen
9.4.1 Verzweigungen mit if-then-else
Eine Verzweigung erfolgt mit der if-Anweisung. Hier wird der Ausdruck auf seinen Wahrheitswert
(wahr entspricht ungleich 0, falsch entspricht 0) geprft und entsprechend der if- oder der elseZweig ausgefhrt (letzterer ist optional):
if (Ausdruck)
Anweisung_1
else
Anweisung_2

Abbildung 12 veranschaulicht die Verzweigung als Programmablaufplan und als Struktogramm.


Bei geschachtelten if-Anweisungen gehrt ein else-Zweig immer zum letzten else-losen if-Zweig.
Im Zweifel sollte man geschweifte Klammern einsetzen, um durch Verbundanweisungen Klarheit zu
schaffen.
Bsp.: Eine if-Anweisung kann an der Stelle benutzt werden, wo die while-Anweisung eine Anweisung erwartet:
while (i < 10)
if (i > 2)
a[i] = 0.0;

Schreiben Sie ein Programm, welches bestimmt, ob ein (ber Standardeingabe oder als Kommandozeilen-Parameter) angegebenes Jahr ein Schaltjahr ist. Schaltjahre sind alle Jahre, die durch 4 teilbar sind.
Die durch Hundert teilbaren Jahre sind nur dann Schaltjahr, wenn sie auch durch 400 teilbar sind. Durch
diese Zusatzregel fallen in 400 Jahren drei Schaltjahre aus (Beispiel: 1700, 1800 und 1900 sind keine
Schaltjahre, das Jahr 2000 ist wieder ein Schaltjahr). Das Struktogramm in Abbildung 13 verdeutlich
den Algorithmus (3 Punkte, Musterlsung: schaltjahr.c).
9.4.2 Mehrfachverzweigungen mit switch
Die switch-Anweisung erlaubt eine Mehrfachverzweigung (Fallunterscheidung) in Abhngigkeit vom
Wert eines ganzzahligen Ausdrucks:

38

C18

Jahr einlesen (Variable jahr)

jahr % 4 == 0
J

jahr % 100 == 0
J

jahr % 400 == 0
J

Schaltjahr

kein Schaltjahr

Schaltjahr

kein Schaltjahr

Abbildung 13: Struktogramm zur Bestimmung von Schaltjahren (H EROLD und A RNDT 2001,
Abb. 1.17).

Fallkonst_1

Ausdruck
Fallkonst_n

default

...
Anweisung(en)_1

Anweisung(en)_n

Anweisung(en)

Abbildung 14: Mehrfachverzweigung: Struktogramm (H EROLD und A RNDT 2001, Abb. 13.1).

switch (Ausdruck) {
case Fallkonstante_1: Anweisung(en)_1
...
case Fallkonstante_n: Anweisung(en)_n
default: Anweisung(en)
}

Sollen zu einem case-Zweig mehrere Anweisungen gehren, so mssen diese nicht als Verbundanweisung geschrieben werden.
Der Wert des Ausdrucks wird nacheinander mit den Fallkonstanten vergleichen (es sind nur Konstanten
erlaubt, keine Ausdrcke). Bei einer bereinstimmung werden alle nachfolgenden Anweisungen ausgefhrt. Wird keine bereinstimmung gefunden, so werden die nach default angegeben Anweisungen
ausgefhrt.
Als Struktogramm knnen Mehrfachverzweigungen wie in Abbildung 14 dargestellt werden.
Durch das Ausfhren aller folgenden Anweisungen kann eine Aktion mehreren Fallkonstanten zugeordnet werden:
1
2
3
4
5
6
7
8
9

switch (ch) {
case \t:
case \n:
case :
puts("Trennzeichen!");
break;
default:
puts("anderes Zeichen!");
}

39

while

for

dowhile

Initial_Ausdruck;

falsch

Anweisung
Ausdruck
falsch
Test_Ausdruck

wahr

Anweisung

Ausdruck

wahr

wahr

Anweisung
falsch
Stell_Ausdruck;

Ausdruck

Initial_Ausdruck;
Ausdruck

Anweisung

Anweisung

Anweisung
Ausdruck
Stell_Ausdruck;

Abbildung 15: Schleifen: Programmablaufplne (oben) und Struktogramme (unten).

Bis auf diesen Spezialfall wird man in der Regel jede Anweisungsfolge eines Zweiges mit dem Schlsselwort break abschlieen (siehe obiges Beispiel). An dieser Stelle wird dann die switch-Anweisung
verlassen und die restlichen Zweige werden nicht mehr durchlaufen. Das Vergessen eines break ist eine
hufige Fehlerquelle.
Verwenden Sie obiges Code-Stck in Zusammenhang mit Schleifen (nachfolgend beschrieben), um aus
einer \0-terminierten Zeichenkette alle Trennzeichen (\t, \n, ) zu entfernen. Die Zeichenkette soll dabei krzer werden. Verzichten Sie dabei auf die Verwendung vordefinierter Zeichenketten-Funktionen (Abschnitt 11.2). (3 Punkte, Musterlsung: delim.c).

9.5 Schleifen-Anweisungen
C bietet drei verschiedene Formen von Schleifen:
die abweisende Schleife (while) mit Test der Bedingung am Anfang,
die Zhlschleife (for), eine spezielle abweisende Schleife, und
die nicht-abweisende Schleife (do-while) mit Test der Bedingung am Ende.
Abbildung 15 zeigt die Programmablaufplne und Struktogramme der drei Schleifenformen.
9.5.1 Schleifen mit Test am Anfang: while
Die while-Anweisung ist eine Programmschleife mit Test der Schleifenbedingung am Schleifenanfang:

40

C19

while (Ausdruck)
Anweisung

Die Anweisung wird solange wiederholt ausgefhrt, bis der Ausdruck den Wahrheitswert falsch hat.
Ist der Ausdruck bereits beim Erreichen der while-Anweisung falsch, so wir die Anweisung nie
ausgefhrt (abweisende Schleife). Einfaches Beispiel mit leerer Anweisung (wartet auf die Eingabe
eines Q):
while (getchar() != Q);

9.5.2 Zhlschleifen mit Test am Anfang: for


Die for-Anweisung
for (Initial_Ausdruck; Test_Ausdruck; Stell_Ausdruck)
Anweisung

ist eine Abkrzung fr die Anweisungsfolge


Initial_Ausdruck;
while (Test_Ausdruck) {
Anweisung;
Stell_Ausdruck;
}

Sie wird in der Regel als Zhlschleife eingesetzt (ist aber nicht auf diesen Einsatzfall beschrnkt), z.B.
zum Initialisieren eines Feldes:
for (i = 0; i < SIZE; i++)
a[i] = 0.0;

Jeder der drei Ausdrcke kann leer sein. Bei Initial_Ausdruck und Stell_Ausdruck kann der
Komma-Operator fr Mehrfach-Initialisierungen und Weiterstellen mehrerer Zhlvariablen zum Einsatz
kommen (siehe auch Abschnitt 8.2):
for (i = 0, j = 0; i < SIZE; i++, j+=2)
a[i] = b[j];

Nutzen Sie verschachtelte for-Schleifen, um ein Pascalsches Dreieck beliebiger Tiefe auszugeben. Die
Ausgabe Ihres Programms pascal soll so aussehen (3 Punkte, Musterlsung pascal.c):
>
1
1
1
1
1
1
>

pascal 6
1
2
3
4
5

1
3 1
6 4 1
10 10 5 1

41

C20

Schreiben Sie ein Makro ALLOC_ARRAY2D(ARRAY,TYPE,DIM1,DIM2,INIT), welches ein zweidimensionales Feld ARRAY der Gre DIM1 DIM2 mit Elementen vom Typ TYPE anlegt. Alle Elemente des Feldes sollen mit INIT initialisiert werden. Das zweidimensionale Feld soll aus einem eindimensionalen Feld mit Elementen vom Typ TYPE* bestehen, die auf eindimensionale Felder mit Elementen vom Typ TYPE zeigen (siehe Abschnitt 7.4.3); jedes Teilfeld soll einzeln mit malloc() vom
Heap angefordert und freigegeben werden. Die Variable TYPE **ARRAY soll nicht im Makro definiert werden. Schreiben Sie weiteres ein Makro DEL_ARRAY2D(ARRAY,DIM1), welches das mit dem
obigen Makro angelegte Feld (d.h. den assoziierten Heap-Speicher) freigibt. (5 Punkte, Musterlsung:
array2d_macros.h, array2d_macros.c).
Eine elegante Lsung fr dieses Problem liegt darin, das gesamte Feld als einen einzigen (eindimensionalen) Datenblock anzulegen und freizugeben (dies spart Zeit, da die Befehle malloc()
und free() teilweise aufwendige Aktionen zur Heap-Vewaltung ausfhren mssen). Der Block enthlt am Anfang die DIM1 Zeiger auf Zeiger auf TYPE und unmittelbar anschlieend die DIM1
DIM2 Datenelemente vom Typ TYPE. Beim Anlegen des Feldes wird nur ein einziges Mal
malloc() aufgerufen; die Zeiger-Zeiger am Anfang des Blocks mssen dann so initialisiert werden, so dass sie auf die richtigen Daten im zweiten Teil des Blocks zeigen. Schreiben Sie Makros
ALLOC_BLOCK2D(ARRAY,TYPE,DIM1,DIM2,INIT) und DEL_BLOCK2D(ARRAY), welche
diese Lsung realisieren (8 Punkte, Musterlsung array2d_macros.h, array2d_macros.c).
9.5.3 Schleifen mit Test am Ende: do-while
Bei while- und for-Schleifen erfolgt der Test der Schleifenbedingung am Anfang der Schleife, bei der
do-while-Schleife hingegen am Ende:
do
Anweisung
while (Ausdruck);

Die Anweisung im Innern wird also mindestens einmal ausgefhrt, auch wenn der Ausdruck bereits beim
Erreichen der do-while-Anweisung falsch ist (nicht-abweisende Schleife).
Es wird empfohlen (H EROLD und A RNDT 2001, S. 270), das abschlieende while immer zusammen
mit dem Semikolon der Block-Anweisung auf eine Zeile zu schreiben, da dies (insbesondere bei lngeren
Blcken) die Lesbarkeit verbessert, indem Verwechslungen mit while-Schleifen mit leerer Anweisung
ausgeschlossen werden:
/* so: */
do {
...
} while (zeichen != Q);
/* nicht so: */
do {
...
}
while (zeichen != Q);

9.5.4 Schleifen-Abbruch und bergang zur nchsten Iteration


Wird innerhalb einer der o.g. Schleifen (while, for, do-while) eine break-Anweisung ausgefhrt,
so wird die Schleife verlassen. Somit gibt es also einen Ausstieg auch aus Endlosschleifen:
42

C21

C22

k = 0;
while (1)
if (k++ > 10)
break;

Zu beachten ist, dass bei verschachtelten Schleifen nur die innerste Schleife verlassen wird.
Die Ausfhrung der Anweisung continue bewirkt den bergang zur nchsten Iteration. Bei whileund do-while-Schleifen wird die Abarbeitung mit dem Test der Bedingung fortgesetzt. Bei forSchleifen wird die Steuerung an den Stell_Ausdruck bergeben, im Beispiel i++:
for (i = 0; i < SIZE; i++) {
...
if (a[i] < 0)
continue;
/* ueberspringe negative Elemente */
...
}

Wie kann man obiges Problem ohne continue lsen?

C23

9.6 Sprung-Anweisung
C erlaubt auch die Definition von Sprungmarken und besitzt die Sprunganweisung goto. Die Verwendung von Sprngen ist nicht erforderlich und gilt allgemein als schlechter Programmierstil. Allerdings
kann ein goto bei Fehlern, die zum Abbruch vieler verschachtelter Schleifen fhren sollen, das Programm vereinfachen und beschleunigen (H EROLD und A RNDT 2001, S. 298):
for (...) {
while (...) {
for (...) {
...
if (fehler)
goto ende;
...
}
}
}
ende: ...

1
2
3
4
5
6
7
8
9
10
11

Wie lsst sich dasselbe Verhalten ohne goto realisieren?

10 Funktionen
Funktionen sind Unterprogramme, in denen bestimmte Ablufe, die im Programm an mehreren Stellen
benutzt werden, zusammengefasst werden. An Funktionen knnen Argumente bergeben werden, und
die Funktion kann einen Rckgabewert liefern. Es ist jedoch dem aufrufenden Code freigestellt, ob er
diesen Rckgabewert nutzt.
Eine Funktion hat in C folgende allgemeine Form:
Ergebnistyp Funktionsname ( Formale_Argumentliste )
{
Anweisungen
}

43

C24

Ein einfaches Beispiel ist eine Funktion zum Berechnen des Mittelwerts zweier Gleitkommazahlen:
float average(float a, float b)
{
float avg;
avg = (a + b) / 2.0;
return avg;
}

1
2
3
4
5
6

Hier besteht die Liste der formalen Argumente aus zwei float-Variablen.
Der Aufruf der Funktion knnte erfolgen mit
1
2
3
4

int
main()
{
float x = 5.0, result;

result = average(x, 15.0);


printf("average = %g\n", result);
if (average(2.0, x) < 1.0))
printf("average smaller than 1.0\n");
exit(0);

6
7
8
9
10
11

Die formalen Argumente a und b werden beim Aufruf mit den Werten der aktuellen Argumente x und
15.0 initialisiert, d.h. a erhlt den aktuellen Wert von x (in diesem Fall 5.0) und b den Wert 15.0.
Funktionsdefinitionen knnen in C nicht verschachtelt werden, d.h. es ist nicht erlaubt, eine Funktion
innerhalb einer anderen zu definieren.

10.1 Wert- und Referenz-Argumente


In C existieren im Gegensatz zu anderen Sprachen nur Wert-Argumente (call-by-value), aber keine
Referenz-Argumente (call-by-reference). Bei einem Wert-Argument wird der Wert des aktuellen Arguments dem formalen Argument zugewiesen. Es knnen also beliebige Ausdrcke (inklusive Literale,
15.0 im obigen Beispiel) bergeben werden. Modifikationen des formalen Arguments innerhalb der
Funktion haben keinen Einfluss auf Variablen, die als aktuelles Argument bergeben wurden (x im obigen Beispiel). Anders bei Referenz-Argumenten (in Sprachen wie Pascal): hier wird nicht der Wert zugewiesen, sondern bei formalem Argument und aktuellem Argument handelt es sich um denselben Speicherplatz. Deswegen drfen in diesen Sprachen Referenz-Argumenten auch nur L-Values als aktuelles
Argument bergeben werden.
Wie kann man nun Referenz-Argumente im C realisieren? So geht es offenbar nicht:
1
2
3
4
5

void
average_wrong(float a, float b, float avg)
{
avg = (a + b) / 2.0;
}

6
7
8
9

int
main()
{

44

float x = 5.0, result = 0.0;


average_wrong(x, 5.0, result);
...

10
11
12

13

Zwar wird in der Funktion avg modifiziert, aber nach aussen wird dies nicht sichtbar result hat
weiter den Wert 0.0.
C umgeht die Unterscheidung zwischen Wert- und Referenz-Argumenten und setzt statt dessen das
Zeiger-Konzept zur Realisierung der Referenz-Argumente ein. Mchte man, dass ein Objekt von der
Funktion modifiziert wird, so bergibt man die Adresse dieses Objekts als aktuelles Argument. Das formale Argument ist dann ein Zeiger auf den Datentyp des Objekts. Innerhalb der Funktion wird nun der
Inhalt des Zeigers manipuliert. Dies ist mglich, weil das formale Argument (Zeiger) auf den zu manipulierenden Speicherplatz verweist. Die auerhalb befindliche Zeiger-Variable wiederum kann im Inneren
der Funktion nicht verndert werden.
Die folgende Funktion fhrt eine Transformation von Polar- in kartesische Koordinaten aus. Da im Ergebnis die zwei Koordinaten eines Vektors geliefert werden, ist eine bergabe als Rckkehrwert nicht ohne weiteres mglich (berlegen Sie, wie man trotzdem beide Werte ber return zurckgegen kann und
implementieren Sie Ihre Lsung; siehe auch Abschnitt 10.2) (3 Punkte, Musterlsung: pol2cart.c);
die beiden Ergebnisse werden als via Zeiger geliefert:

C25

void
pol2cart(double r, double phi, double *x, double *y)
{
*x = r * cos(phi);
*y = r * sin(phi);
}

1
2
3
4
5
6

Beim Aufruf der Funktion werden die Adressen zweier Variablen bergeben:
1

double vecx, vecy;

2
3
4

pol2cart(1.0, M_PI/4, &vecx, &vecy);


printf("vec: x = %g, y = %g\n", vecx, vecy);

Ist das zu modifizierende Objekt ein Zeiger, so muss die Adresse des Zeigers bergeben werden. Das
formale Argument wre bspw. float **pp, und ein Zeiger float *ptr muss als aktuelles Argument mit &ptr bergeben werden. Schreiben Sie eine Funktion swapPtr, welche zwei Zeigervariablen
(float *) so modifiziert, dass der erste Zeiger auf dasselbe Objekt zeigt wie zuvor der zweite und umgekehrt.
Beim Aufruf einer Funktion erfolgt eine implizite Typ-Umwandlung von den aktuellen auf die formalen
Argumente, die nach denselben Regeln abluft, wie sie in Abschnitt 8.4 beschrieben wurden (H EROLD
und A RNDT 2001, S. 366). Man beachte, dass Zeiger-Variablen verschiedener Datentypen nicht kompatibel sind, also nicht automatisch ineinander umgewandelt werden knnen; hier wre eine explizite
Typumwandlung notwendig, bspw. mit dem Operator (double *). (Eine Ausnahme sind Zeiger auf
void, mit denen beliebige Zuweisungen an alle und von allen Zeiger-Typen erlaubt sind.)
Man beachte, dass weder die Reihenfolge der Auswertung der aktuellen Argumente der Funktion noch
die Reihenfolge des Aufrufs von Funktionen in Ausdrcken festgelegt ist (H EROLD und A RNDT 2001,
S. 386).

45

C26

10.2 Rckgabewerte
Eine Funktion, die keinen Rckgabewert liefert, muss mit dem Ergebnistyp void definiert werden (ansonsten verlangt der Compiler, dass mit return ein Wert zurckgegeben wird). Neben void sind
folgende Typen als Ergebnistyp erlaubt:
alle Grunddatentypen
Zeiger auf Objekte beliebiger Typen
Strukturen
Die Rckgabe des Funktionswertes erfolgt mit return Ausdruck;. An dieser Stelle wird die Funktion auch beendet und die Abarbeitung kehrt zum aufrufenden Code zurck. In einer Funktion mit dem
Ergebnistyp void kann return ohne Argumente aufgerufen werden, um die Funktion zu beenden.
Bei der Rckgabe von Zeigern aus Funktionen ist Vorsicht geboten: Es darf nie ein Zeiger auf eine lokale
Variable zurckgegeben werden, wie in diesem Beispiel:
float*
pol2cart_falsch(float r, float phi)
{
float cart[2];
cart[0] = r * cos(phi);
cart[1] = r * sin(phi);
/* fatal: cart existiert nur in der Funktion */
return cart;
}

1
2
3
4
5
6
7
8
9

Die Variable cart wird bei Eintritt in pol2cart_falsch() auf dem Stack angelegt und bei Verlassen der Funktion zerstrt. Der zurckgegebene Zeiger (Adresse von cart) zeigt daher auf einen
Speicherplatz, der anschlieend von anderen Funktionen benutzt und daher berschrieben wird. Der
Compiler gcc liefert deswegen die Warnung: warning: function returns address of local variable.

10.3 Prototypen und Export


Um eine Funktion aufrufen zu knnen, muss der Compiler neben dem Namen der Funktion zumindest
deren Ergebnistyp und die Typen der Argumente aus der Argumentliste kennen nur so kann er berprfen, ob beim Aufruf der Funktion die korrekte Art und Anzahl von Argumenten bergeben wird.
Wird die aufgerufene Funktion im Quelltext vor der aufrufenden Funktion definiert, so sind diese Informationen bekannt. Wird jedoch
die aufgerufene Funktion im Quelltext nach der aufrufenden definiert, oder
wird die aufgerufene Funktion in einem anderen Modul definiert,
so muss ein Prototyp deklariert werden. Dieser gleicht in allem der Funktions-Definition, auer dass der
Funktionsrumpf (geschweifte Klammern und Anweisungen) nicht vorhanden ist (statt dessen steht ein
Semikolon). Zudem wird ein Prototyp meist mit dem Vorsatz extern versehen. Ein Beispiel fr obige
Funktion wre
46

extern float average(float a, float b);

Funktionen sind auch ohne diesen Vorsatz implizit extern, d.h. auch in anderen Objektdateien bekannt.
Wie bei den Variablendefinitionen (siehe Abschnitt 7.6) besteht die Konvention, Funktionsprototypen in
Header-Dateien mit extern zu versehen. Soll eine Funktion nur in der gleichen Objektdatei, in der
sie auch definiert wurde, bekannt sein, von anderen Objektdateien aber nicht benutzt werden knnen, so
muss der Vorsatz static vor der Funktionsdefinition verwendet werden.

10.4 Zeiger auf Funktionen


Einige Betriebssystem-Funktionen werden ber Call-Back-Funktionen realisiert. Dabei wird einer Systemfunktion zunchst der Zeiger auf eine Funktion bergeben. An geeigneter Stelle kann nun das Betriebssystem entscheiden, diese Funktion aufzurufen es ruft zurck. Schauen wir uns dazu die Systemfunktion signal() an. Dieser Systemfunktion werden zwei Argumente bergeben, eine Signalnummer signum und ein Funktionszeiger handler (Details zu Funktionszeigern finden sich in The
Function Pointer Tutorials). Der Funktionszeiger wurde zuvor als Typ definiert (in signal.h):
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

Man kann nun eine Funktion wie my_handler() definieren und diese dem Funktionszeiger-Argument
bergeben.
#include <signal.h>

1
2

void
my_handler(int sig)
{
printf("caught signal %d\n", sig);
}

3
4
5
6
7
8

int
main()
{
signal(SIGHUP, my_handler);
}

9
10
11
12
13

Im Inneren der Funktion signal() kann mit dem bergebenen Funktionszeiger einfach durch
handler(sig) die Call-Back-Funktion mit einem Argument sig aufgerufen werden. Erweitern Sie
dieses Programm so, dass es nach dem Aufruf von signal() in eine Endlosschleife eintritt. Senden
Sie mit dem Unix-Kommando kill -HUP pid ein Signal HUP (Hangup) an den Prozess.
Schreiben Sie eine Funktion, welche einen Zeiger auf eine Funktion entgegennimmt, die ein einziges
formales Argument vom Typ double und den Ergebnistyp double hat. In der Funktion soll eine
Wertetabelle der Funktion ausgegeben werden. Testen Sie die Funktion durch bergabe von Zeigern auf
Funktionen aus libm.a, z.B. sin(), sqrt() (3 Punkte, Musterlsung: wertetab.c).
Ntzlich knnen auch Felder von Funktionszeigern sein (H EROLD und A RNDT 2001, S. 640), die man
am einfachsten ber einen zuvor definierten Funktionszeiger-Typ definiert:

47

C27

C28

#include <stdio.h>

1
2

double
double
double
double

3
4
5
6

fadd(double
fsub(double
fmul(double
fdiv(double

a,
a,
a,
a,

double
double
double
double

b)
b)
b)
b)

{
{
{
{

return
return
return
return

a+b;
a-b;
a*b;
a/b;

}
}
}
}

typedef double (*binfct_t)(double, double);

8
9

binfct_t func[4] = {fadd, fsub, fmul, fdiv};

10
11

int
main()
{
double a = 10.0, b = 20.0;
int i;

12
13
14
15
16
17

for (i = 0; i < 4; i++)


printf("function %d: result = %g\n", i, func[i](a, b));
return 0;

18
19
20

21

Dieses Programm wendet alle 4 Funktionen auf a und b an und erzeugt folgende Ausgabe:
function
function
function
function

0:
1:
2:
3:

result
result
result
result

=
=
=
=

30
-10
200
0.5

10.5 C-Funktionen und der Argument-Stapel


In Abschnitt 7.6.2 wurde erklrt, dass Variablen, die innerhalb von Funktionen und nicht als static
definiert sind, auf dem Stapel-Speicher angelegt werden (lokale Variablen). Zum besseren Verstndnis
wird im folgenden die Stapel-Verwaltung von gcc unter Linux auf IA-32 Prozessoren erlutert.
Der Stapel (Stack) ist ein Bereich im Hauptspeicher in der Organisationsform first-in last-out. Man
kann sich den Stapel bildlich als Stapel von Kisten vorstellen: man kann zum Stapel eine Kiste oben
hinzufgen und den Stapel auch nur von oben wieder abtragen. Auf das oberste Element des Stapels
verweist der Stapelzeiger (stack pointer), ein spezielles Register der CPU (bei IA-32 Prozessoren ESP)
der Stapelzeiger enthlt also die Adresse des obersten Elements. Bei Hochsprachen wird der Stapel
benutzt, um Argumente an Funktionen zu bergeben, um die Rckkehradresse in die aufrufende Funktion
zu speichern und um lokale Variablen innerhalb von Funktionen aufzunehmen.
Gegeben sei folgende Funktion my_func:
1
2
3
4

void
my_func(int i, int j)
{
int k, l, m;

/* Ihr Code an dieser Stelle! */


if (i > 0)
my_func(--i, j);

6
7
8
9

10
11

int

48

EBP+12:

EBP+8:

Argumente

Argumente

ReturnAdr.

EIP (Ret.Adr.)
EBP (neu):

EBP (alt)

EBP

EBP4
EBP8

EBP12

lokale Variable

lokale Variablen

anderes

...

Argumente
ReturnAdr.

ESP

EBP
lokale Variable
ESP

anderes

Abbildung 16: C-Stack (gcc, IA-32, Linux).

main()
{
my_func(3, 1234);
return 0;
}

12
13
14
15
16

Die Funktion hat zwei formale Argumente vom Typ int sowie drei lokale Variablen vom Typ int. Abbildung 16 zeigt, was beim Aufruf der Funktion auf dem Stapel passiert (bei IA-32 Prozessoren wchst
der Stapel zu niedrigen Adressen hin und ist deswegen hier hngend dargestellt).
1. Zunchst legt die Funktion, welche my_func aufruft (zuerst main, danach my_func selbst in
einem rekursiven Aufruf) die aktuellen Argumente in umgekehrter Reihenfolge auf den Stack.
Dann speichert die aufrufende Funktion beim CALL-Befehl ihre Rckkehradresse (Register EIP)
auf dem Stack. Die Maschinenbefehle lauten z.B.:
push dword 1234
push dword 3
call my_func

; push current argument j


; push current argument i
; call function

2. Nun bernimmt die aufgerufene Funktion. Sie richtet sich zunchst einen neuen Stack-Frame
(Stapel-Rahmen) ein. Der Stapel-Rahmen ist ein Bereich auf dem Stack, welcher zu der aufgerufen Funktion gehrt. Er wird ber das Register EBP (extended base pointer) definiert. Dieses
Register zeigt auf eine Adresse auf dem Stack, relativ zu der auf die Argumente und auf die lokalen Variablen zugegriffen werden kann. Da auch die aufrufende Funktion einen Stack-Frame
eingerichtet hatte, wird zuerst deren EBP-Register auf den Stack geschoben und so aufbewahrt.
Danach wird der EBP-Zeiger auf die Adresse gesetzt, die dem aktuellen Stack-Pointer (StapelZeiger) ESP entspricht. Dies geschieht mit:
push ebp
mov ebp, esp

; push old ebp onto stack


; new ebp is current esp value

3. Nun wird der Stapelzeiger ESP weiter verschoben, unter anderem, um Platz fr die lokalen Variablen k, l, m zu machen, auf welche ber einen Versatz (Offset) auf EBP zugegriffen werden
kann (3 int-Variablen bentigen 12 Byte):
49

sub esp, 12

Wird nun aus der Funktion selbst wiederum eine Funktion aufgerufen (Abbildung 16, rechts), so wchst
der Stack weiter nach unten, die aufgerufene Funktion richtet sich ihr eigenes Stack-Frame ein usw. Der
Stapel enthlt also die Geschichte aller noch nicht abgeschlossenen Funktionsaufrufe.
Bei der Rckkehr aus der Funktion wird der Stapel-Rahmen eingerissen. Die lokalen Variablen werden
nicht mehr bentigt, also kann der Stack-Pointer ESP diesen Bereich berspringen (er zeigt dann auf
denselben Speicherplatz wie EBP). Der alte Stapel-Rahmen (der aufrufenden Funktion) wird restauriert,
und mit einen RET Befehl erfolgt die Rckkehr zur aufrufenden Funktion:
mov esp, ebp
pop ebp
ret

; take down stack frame


; restore callers stack frame
; return to caller

Die aufrufende Funktion muss nun noch den Platz freigeben, der von den Argumenten i und j eingenommen wurde; dies geschieht mit
add esp, 8

Die hier untersuchte Funktion hat den Rckkehrtyp void. Fr Funktionen, die Rckkehrtypen mit 4
Byte oder weniger haben, wird der Rckgabewert im Register EAX zurckgeliefert. Ansonsten erzeugt
der Compiler intern ein zustzliches Zeiger-Argument fr den Rckgabewert (x=fct(a,b) wird zu
fct(&x,a,b)).
Details zum Aufbau von Stack-Frames finden sich in C Function Call Conventions and the Stack.

10.6 Rekursive Funktionen


Da bei jedem Aufruf einer Funktion die Argumente und lokalen Variablen in einem eigenen StapelRahmen untergebracht sind, ist es auch erlaubt, Funktionen rekursiv aufzurufen, wie es im obigen Beispiel geschieht. Es gibt keine Beeinflussung zwischen den Argumenten und lokalen Variablen der verschiedenen Aufrufe derselben Funktion. Dies ist hingegen nicht gewhrleistet, wenn die Funktionen auf
Variablen auerhalb der Funktion oder auf statische Variablen innerhalb der Funktion zugreifen, da diese
nur in je einer Instanz existieren, auch wenn die Funktion mehrfach aufgerufen wird.
Geben Sie die Adressen der Argumente und der lokalen Variablen von my_func auf der Standardausgabe aus (Code an der gekennzeichneten Stelle einfgen) und analysieren Sie das Ergebnis. Ermitteln
Sie durch Manipulation von Zeigern auch die auf dem Stapel gespeicherten Werte des alten EBP und der
RckkehrAdresse. (3 Punkte, Musterlsung: c_stack.c)
Rekursive Funktionen liefern elegante Lsungen fr den Umgang mit Datenstrukturen wie Listen und Bumen. Folgende Funktionen bestimmen die Lnge einer einfach verketteten Liste
(list_length()), geben die Liste in der Reihenfolge der Elemente aus (print_list()) und suchen nach bereinstimmung mit einem Element von vorn nach hinten (search_forward()) (einige
Hilfsfunktionen und ein Hauptprogramm sind angegeben):
1
2
3
4
5
6

/* entry of elem list */


typedef struct _elem_t {
int i;
float f;
struct _elem_t *next;
} elem_t;

/*
/*
/*
/*

data */
data */
pointer to next elem */
alias type name */

50

C29

7
8
9
10
11
12
13
14
15
16
17

/* create (and allocate) elem_t, assign initial values */


elem_t*
create_elem(int i, float f)
{
elem_t *e = malloc(sizeof(elem_t));
e->i = i;
e->f = f;
e->next = NULL;
/* just to be sure */
return e;
}

18
19
20
21
22
23
24
25

/* append elem to list */


elem_t*
append_elem(elem_t *list, elem_t *e)
{
e->next = list;
return e;
}

26
27
28
29
30
31
32
33
34

/* number of list elements */


int
list_length(elem_t *list)
{
if (!list)
return 0;
return 1 + list_length(list->next);
}

35
36
37
38
39
40
41
42
43
44

/* print list element */


void
print_elem(elem_t *e)
{
if (e)
printf("this=%p, i=%d, f=%g, next=%p\n", e, e->i, e->f, e->next);
else
printf("this=NULL\n");
}

45
46
47
48
49
50
51
52
53
54

/* print list */
void
print_list(elem_t *list)
{
if (!list)
return;
print_elem(list);
print_list(list->next);
}

55
56
57
58
59
60
61

/* return value indicates whether e1 and e2 are identical */


int
identical(elem_t *e1, elem_t *e2)
{
return (e1->i == e2->i) && (e1->f == e2->f);
}

62
63
64
65
66

/* search for first item in list which is equal to e */


elem_t *
search_forward(elem_t *list, elem_t *e)
{

51

if (!list)
return NULL;
if (identical(list, e))
return list;
return search_forward(list->next, e);

67
68
69
70
71

72
73

int
main()
{
elem_t *anker = NULL;
elem_t *to_find = create_elem(2, 2.22);

74
75
76
77
78
79

anker = append_elem(anker, create_elem(1, 1.11));


anker = append_elem(anker, create_elem(2, 2.22));
anker = append_elem(anker, create_elem(3, 3.33));
print_list(anker);
printf("length=%d\n", list_length(anker));
puts("forward");
print_elem(search_forward(anker, to_find));
return 0;

80
81
82
83
84
85
86
87

88

Allerdings ist zu beachten, dass rekursive Lsungen unter Umstnden deutlich langsamer sind, da im
Vergleich zu nicht-rekursiven Implementationen (Schleifen) der Aufbau und das Einreien des Stapelrahmens wesentlich hufiger erfolgt.
Schreiben Sie rekursive Funktionen, welche (1) die Liste lschen, (2) die Liste in umgekehrter Reihenfolge ausgeben und (3) nach bereinstimmung mit einem Element von hinten nach vorn suchen (9 Punkte,
Musterlsung reclist.c).
Auch viele mathematische Funktionen sind rekursiv definiert. Schreiben Sie eine rekursive Funktion,
die den grten gemeinsamen Teiler zweier ganzer Zahlen n und m nach folgender Vorschrift ermittelt:
ggt(n, m) = ggt(m, n)
ggt(n, m) = n
ggt(n, m) = ggt(m, n mod m)

fr m > n
fr m = 0
sonst

Verfolgen Sie durch Ausgaben die rekursiven Aufrufe (3 Punkte, Musterlsung: ggt.c).

10.7 Die main-Funktion und Kommandozeilen-Parameter


Jedes C-Programm braucht eine Funktion des Namens main(). Diese Funktion ist der Eintrittspunkt
in das Programm. Die Funktion hat den Ergebnistyp int. Im einfachsten Fall wird sie ohne Argumente
definiert. Mchte man Zugriff auf die Parameter haben, die dem Programm beim Aufruf von der Shell
bergeben werden, so kann man folgende Definition verwenden:
int
main(int argc, char *argv[])
{
...
}

52

C30

C31

argv

argc = 4

argv[0]
. / a r g t e s t \0
argv[1]
e i n s \0
argv[2]
z w e i \0

argv[0][7]

argv[3]
d r e i \0

Abbildung 17: Struktur des Kommandozeilen-Vektors argv.

In argc wird beim Aufruf die Anzahl der bergebenen Parameter bermittelt. Dabei zhlt der Name des
Programms (inklusive angegebener Pfade) als Parameter mit. Das zweite formale Argument, argv, ist
ein Feld von Zeigern auf char, also ein Feld von Zeichenketten. Diese drfen nicht manipuliert werden.
Das folgende Programm gibt die Kommandozeilen-Parameter auf der Standardausgabe aus:
1
2

#include <stdio.h>
#include <stdlib.h>

3
4
5
6
7
8
9
10
11

int
main(int argc, char *argv[])
{
int i;
for (i = 0; i < argc; i++)
printf("%d: [%s]\n", i, argv[i]);
exit(0);
}

Der Aufruf und die Ausgaben des Programms, dessen ausfhrbare Datei argtest heissen soll, sehen
so aus:
> ./argtest eins zwei drei
0: [./argtest]
1: [eins]
2: [zwei]
3: [drei]
>

Abbildung 17 zeigt die Struktur von argv fr dieses Beispiel.


Fr die Eingabe von ganzen und Gleitkommazahlen sind die Funktionen atof und atoi ntzlich.
Machen Sie sich mit diesen Funktionen anhand der Man-Pages vertraut. Schreiben Sie ein Programm,
welches als Parameter eine Gleitkommazahl, eine Operation (+, -, x, /) und eine zweite Gleitkommazahl erhlt und das Ergebnis der Operation auf der Standardausgabe ausgibt. (Wir verwenden x statt *,
weil letzteres ein Wildcard-Symbol der Shell ist.) Testen Sie, ob die korrekte Anzahl von Argumenten
vorhanden ist, ansonsten geben Sie eine Fehlermeldung auf der Standardfehler-Ausgabe aus (netterweise
mit Angabe der vom Programm gewnschten Parameter) und beenden das Programm mit exit(-1),
um einen fehlerhaften Ausstieg anzuzeigen (3 Punkte, Musterlsung: calc.c).

11 Ausgewhlte Funktionen der Standard-C-Bibliothek


Da die Sprache C keine eingebauten Operationen fr Ein-Ausgabe, Zeichenketten-Verarbeitung, Speicherverwaltung etc. zur Verfgung stellt, werden all diese Operationen als Funktionen der umfangreichen
53

C32

Tabelle 6: Ausgewhlte Zeichenketten-Funktionen


Zeichenkettenlnge bestimmen
strlen
Zeichen in Zeichenkette suchen
strchr, strrchr, index, rindex
Zeichenkette in Zeichenkette suchen strstr
Zeichenketten vergleichen
strcmp, strncmp
Zeichenketten kopieren
strcpy, strncpy
Zeichenketten verknpfen
strcat, strncat
Zeichenketten unterteilen
strtok, strtok_r
Speicherbereiche kopieren
memcpy, memccpy, memmove, bcopy
Speicherbereiche setzen
memset

Standard-C-Library zur Verfgung gestellt. Fr Details sei auf die Dokumentation The GNU C Library
verwiesen.

11.1 Systemrufe
Wir werden viele der in Zusammenhang mit Betriebssystemfunktionen auftretenden Aufrufe der Standard-C-Bibliothek (z.B. Systemrufe) an der entsprechenden Stelle in der Vorlesung Betriebssysteme
behandeln.

11.2 Verarbeitung von Zeichenketten


Die meisten dieser Funktionen aus string.h sind fr unsere Aufgaben nicht von Bedeutung (eine
Ausnahme sind die Funktionen zum Kopieren und Initialisieren von Speicherbereichen). Tabelle 6 zeigt
eine Auswahl der wichtigsten Funktionen.

11.3 Erzeugung von Zufallszahlen


Zur Erzeugung gleichverteilter ganzzahliger Zufallszahlen stehen die Funktionen
int rand(void);
void srand(unsigned int seed);

zur Verfgung. Die Funktion rand() erzeugt Zufallszahlen zwischen 0 und RAND_MAX; mit srand()
wird der Zufallszahlengenerator initialisiert (siehe Man-Pages).
Gleichverteilte reelle Zufallszahlen werden mit drand48() erzeugt; der Zufallszahlengenerator wird
mit srand48() initialisiert:
double drand48(void);
void srand48(long int seedval);

11.4 Heap-Verwaltung
Die Funktionen malloc() und free() wurden bereits in Zusammenhang mit den Zeigervariablen
eingefhrt (siehe Abschnitt 7.4.2).

54

11.5 Streams
Aus historischen Grnden wird in C die Datenstruktur zur Verwaltung eines Dateizugriffs (Datentyp
FILE) als Stream bezeichnet. Die Stream-Funktionen erlauben die komfortable Ein- und Ausgabe ber
Dateien, darunter auch Eingaben ber Tastatur und Ausgaben auf dem Bildschirm. Die meisten Bibliotheksfunktionen erwarten einen Zeiger FILE* als Argument. Zu beachten ist, dass diese Funktionen
verschieden sind von den low level Ein- und Ausgabe-Systemrufen des Betriebssystems (open(),
close(), read(), write()). Streams sind fr uns vor allem fr Testausgaben von Bedeutung.
11.5.1 Standard-Streams
Es existieren drei Streams, die beim Start des Programms bereits geffnet sind:
FILE *stdin ist die Standardeingabe (ohne Umlenkung kommen die Zeichen von der Tastatur).
FILE *stdout ist die Standardausgabe (ohne Umlenkung erfolgt die Ausgabe auf dem Terminal, welches mit dem Programm assoziiert ist)
FILE *stderr ist die Standard-Fehlerausgabe (ohne Umlenkung ebenfalls das Terminal).
11.5.2 ffnen und Schlieen eines Streams
Ein Stream wird mit der Funktion
FILE * fopen (const char *filename, const char *opentype)

geffnet. Diese erwartet einen Dateinamen und eine Zeichenkette opentype, welche die gewnschte
Art des Zugriffs auf die Datei angibt, z.B. "r" fr lesenden und "w" fr schreibenden Zugriff.
Ein Stream kann mit der Funktion
int fclose (FILE *stream)

geschlossen werden.
11.5.3 Einfache Ausgaben
Es existieren zahlreiche Funktionen, um einzelne Zeichen oder Zeichenketten auszugeben. Mit
int fputc (int c, FILE *stream)

wird ein einzelnes Zeichen auf einem Stream ausgegeben, mit


int fputs (const char *s, FILE *stream)

eine ganze Zeichenkette. Weitere ntzliche Funktionen sind puts(), putc() und putchar() (siehe
Man-Pages).

55

11.5.4 Einfache Eingaben


Die Funktion
int fgetc (FILE *stream)

liest ein einzelnes Zeichen von einem Stream ein. Ist der Stream am Ende angekommen (Dateiende), so
wird EOF zurckgegeben. Mit
char * fgets (char *s, int count, FILE *stream)

kann eine Anzahl count von (maximal) Zeichen in eine Zeichenkette s (die gengend Platz bieten
muss) eingelesen werden. Weitere ntzliche Funktionen sind getc() und getchar().
11.5.5 Ein- und Ausgabe von Blockdaten
Die Funktionen
size_t fread (void *data, size_t size, size_t count,
FILE *stream)
size_t fwrite (const void *data, size_t size, size_t count,
FILE *stream)

dienen zum Einlesen und Ausgeben von Datenfeldern (Zeiger data) einer bestimmten Gre (size)
von einem / auf einen Stream.
11.5.6 Formatierte Ausgabe
Die Funktion fprintf() (bzw. printf(), wenn die Standardausgabe angesprochen wird) dient der
formatierten Ausgabe mehrerer Argumente. Dazu wird eine Formatzeichenkette bergeben, welche festlegt, in welchem Format die folgenden Argumente ausgegeben werden sollen. Die Formatzeichenkette
kann beliebige Zeichen enthalten. An allen Stellen, an denen ein Prozentzeichen mit nachfolgenden
Formatangaben auftaucht, erfolgt die formatierte Ausgabe fr eines der nachfolgenden Argumente (in
derselben Reihenfolge). Die Anzahl der Argumente ist beliebig, sollte aber mit der im Formatstring festgelegten korrespondieren. Hier einige Beispiele; fr Details wird auf die Man-Page verwiesen:
1
2

#include <stdio.h>
#include <stdlib.h>

3
4
5
6
7
8
9
10
11

int
main()
{
char c = A, *s = "This is a string";
int i = 1234;
double x = -0.9876;
long l = 123456789L;
unsigned int u = 333;

12

fprintf(stderr, "x = %g, i =


printf("x = %7.2g, i = %07d,
fprintf(stdout, "u = %u, l =
printf("&i = %p, &x = %p\n",
exit(0);

13
14
15
16
17
18

%d, s = %s, c = %c\n", x, i, s, c);


i = %04xH\n", x, i, i);
%ld\n", u, l);
&i, &x);

56

Dieses Programm liefert die Ausgabe:


x = -0.9876, i = 1234, s = This is a string, c = A
x =
-0.99, i = 0001234, i = 04d2H
u = 333, l = 123456789
&i = 0xbfffe15c, &x = 0xbfffe150

Die Ausgabe von Zeichenketten und einzelnen Zeichen erfolgt mit "%s" bzw. "%c". Gleitkommazahlen
lassen sich mit "%g" ausgeben, ganze Zahlen mit Vorzeichen mit "%d", vorzeichenlose ganze Zahlen
mit "%u", ganze Zahlen des Typs long mit %ld. Zwischen dem Prozentzeichen und dem Formatbuchstaben knnen noch die Gesamtstellenzahl bzw. die Gesamtstellenzahl und die Zahl der Nachkommastellen angegeben werden (Bsp. %7.2.g: 7 Stellen, davon 2 Nachkommastellen); bei Angabe einer 0
wird vorn mit fhrenden Nullen aufgefllt (Bsp. %07d). Zeiger (Adressen) knnen mit %p als Hexadezimalzahl ausgegeben werden.
11.5.7 Formatierte Eingabe
Die formatierte Eingabe erfolgt mit der Funktion fscanf() (bzw. scanf(), wenn von der Standardeingabe gelesen wird). Wie fprintf() arbeitet auch fscanf() mit einer Formatzeichenkette die
angibt, in welches Format eine eingelesene Zeichenkette zu verwandeln ist. Entsprechend dieser Formatangabe erwartet die Funktion eine entsprechende Anzahl von Zeigern auf die Objekte, in welche die
konvertierten Daten geschrieben werden sollen.
11.5.8 Test auf Dateiende
Mit der Funktion feof() kann berprft werden, ob das Ende des Streams erreicht wurde.
11.5.9 Positionierung in der Datei
Normalerweise werden Daten einfach nacheinander in eine Datei geschrieben oder daraus gelesen.
Manchmal ist es jedoch erforderlich, die Daten ab einer bestimmten Stelle in die Datei zu schreiben
oder ab einer Stelle zu lesen. Die Funktion fseek() verschiebt die aktuelle Lese-/Schreibposition. Mit
der Funktion ftell() kann die aktuelle Position abgefragt werden.

12 Makefiles
Fr grere Softwareprojekte ist es sinnvoll, das Compilieren und Linken der Dateien ber Makefiles
mithilfe des Programms make zu organisieren. Dieses Programm bestimmt, welche Teile eines Projekts
neu compiliert bzw. gelinkt werden mssen (weil Dateien verndert wurden, von denen wiederum andere
Dateien abhngen), und fhrt die entsprechenden Kommandos aus. Im folgenden wird kurz der Syntax
von Makefiles vorgestellt; Details finden sich in der Dokumentation zu GNU-Make8 .
Fr jedes Softwareprojekt existiert eine Datei Makefile, welche von make gelesen wird; in dieser
Datei steht, welche Dateien wie zu behandeln (z.B. zu compilieren) sind, um die gewnschten Zieldateien
(meist ausfhrbare Dateien und Bibliotheken) zu erzeugen.
Kern eines Makefiles sind Regeln der folgenden Form:
8

http://www.gnu.org/software/make

57

Ziel: Voraussetzungen ...


Kommando
...
Das Ziel einer Regel ist die Datei, die mit dieser Regel erzeugt werden soll, meist ausfhrbare Dateien,
Objektdateien oder Bibliotheken. Es kann sich allerdings auch um den Namen einer Aktion handeln, wie
z.B. clean (ein Aufruf von make clean wrde die Kommandos dieser Aktion ausfhren, es wird
aber keine Datei erzeugt).
Die Voraussetzungen sind eine oder mehrere Dateien, die bentigt werden, um das Ziel zu erzeugen; so
hngen viele c-Dateien von h-Dateien ab. Immer wenn sich eine Datei, die als Voraussetzung angegeben
ist, ndert (d.h. wenn das Ziel lteren Datums ist als die Voraussetzung), so wird das Ziel neu erzeugt.
Das Kommando ist die Aktion, welche zur Erzeugung des Ziels ausgefhrt wird, also z.B. ein CompilerAufruf. Es sind mehrere Kommandos pro Regel mglich, jedes auf einer eigenen Zeile. Achtung: Diese
Zeilen mssen mit einem Tabulator beginnen!
Das Programm make kann nun mit einem Ziel als Parameter aufgerufen werden; wird kein Ziel angegeben, so wird die erste Regel in der Datei benutzt. Da diese aber ber die Voraussetzungen von anderen
Regeln abhngt, analysiert make zunchst alle Regeln und stellt die Abhngigkeiten fest.
Wir beginnen mit einem einfachen Makefile, welches nur aus Regeln besteht. Wir wollen ein
Programm testmain erzeugen, welches aus drei Objektdateien gebildet wird: testmain.o,
testcode.o und othercode.o. Diese wiederum werden aus c-Files des gleichen Namens erzeugt,
also testmain.c, testcode.c und othercode.c. Die Datei testmain.c bindet die HeaderDateien testcode.h und othercode.h ein, die Datei othercode.c den zugehrigen Header
othercode.h. Die Regel clean lscht alle erzeugten Dateien.
1
2

testmain: testmain.o testcode.o othercode.o


gcc -o testmain testmain.o testcode.o othercode.o -lm

3
4
5

testmain.o: testcode.h othercode.h


gcc -Wall -c testmain.c

6
7
8

testcode.o:
gcc -Wall -c testcode.c

9
10
11

othercode.o: othercode.h
gcc -Wall -c othercode.c

12
13
14

clean:
rm -f testmain testmain.o testcode.o othercode.o

Die folgende Sequenz zeigt einen Dialog in der Shell: Es werden zunchst alle zuvor erzeugten Dateien
gelscht, dann wird das komplette Projekt mit make compiliert und gelinkt, und nach der Modifikation
einer Datei (hier mit touch simuliert, welches nur das Datum aktualisiert) werden (nur) die Teile des
Projektes, die von der vernderten Datei abhngen, neu compiliert und gelinkt:
> make clean
rm -f testmain testmain.o testcode.o othercode.o
> make
gcc -Wall -c testmain.c
gcc -Wall -c testcode.c
gcc -Wall -c othercode.c
gcc -o testmain testmain.o testcode.o othercode.o -lm

58

> touch othercode.h


> make
gcc -Wall -c testmain.c
gcc -Wall -c othercode.c
gcc -o testmain testmain.o testcode.o othercode.o -lm
>

Die zweite Variante dieses Makefiles verwendet zur besseren Lesbarkeit eine Variable objects, in der
alle Objektdateien zusammengefasst sind:
objects = testmain.o testcode.o othercode.o

1
2

testmain: $(objects)
gcc -o testmain $(objects)

3
4
5

testmain.o: testcode.h othercode.h


gcc -Wall -c testmain.c

6
7
8

testcode.o:
gcc -Wall -c testcode.c

9
10
11

othercode.o: othercode.h
gcc -Wall -c othercode.c

12
13
14

clean:

15

rm -f testmain $(objects)

16

Die Betrachtung des Makefiles offenbart, dass alle Objektdateien mit dem gleichen Kommando erzeugt
werden bei einer groen Zahl von Objektdateien wre der Aufwand lstig, insbesondere wenn dieses Kommando berall gendert werden soll. Da das Erzeugen von Objektdateien aus C-Quellen eine
hufig bentigte Aktion ist, kennt make ein Standard-Kommando (eine implizite Regel) fr die Erzeugung. Es gengt deshalb, nur noch die Voraussetzungen anzugeben. Das Standard-Kommando fr die
Erzeugung von Dateien mit der Endung .o aus Dateien mit der Endung .c lautet
$(CC) -c $(CPPFLAGS) $(CFLAGS)

Das Kommando enthlt Variablen fr den C-Compiler, die Flags fr den C-Prprozessor und die Flags
fr den C-Compiler. Die Flag-Variablen sind standardmig leer, der Compiler ist auf cc gesetzt. Im
folgenden Makefile wird gcc als Compiler eingesetzt, und wir geben -Wall als CFLAGS an:
1
2

CC = gcc
CFLAGS = -Wall

3
4

objects = testmain.o testcode.o othercode.o

5
6
7

testmain: $(objects)
gcc -o testmain $(objects)

8
9
10
11

testmain.o: testcode.h othercode.h


testcode.o:
othercode.o: othercode.h

12
13
14

clean:
rm -f testmain $(objects)

59

Man muss sich allerdings nicht auf die impliziten Regeln verlassen, sondern kann diese umdefinieren
oder eigene Regeln angeben (als explizite Regeln bezeichnet). Das folgende Makefile gibt eine solche
Regel fr die Erzeugung von Objektdateien (Endung .o) aus C-Quelldateien (Endung .c) an. Dabei
setzt make fr $< den Namen der Quelldatei und fr $@ die den Namen der Zieldatei ein. Um die
Ausgaben etwas zu verschnern, wird mit echo das gerade ausgefhrte Kommando erklrt; jedes Kommando, dem ein @ vorangestellt wird, wird dabei nicht nochmals (wie oben gezeigt) ausgegeben:
%.o: %.c

@echo compiling $@ from $<


@gcc -Wall -c $< -o $@

2
3
4

objects = testmain.o testcode.o othercode.o

5
6

testmain: $(objects)
@echo linking testmain
@gcc -o testmain $(objects)

7
8
9
10

testmain.o: testcode.h othercode.h


testcode.o:
othercode.o: othercode.h

11
12
13
14

clean:

15

rm -f testmain $(objects)

16

Die Ausgabe dieses Makefiles sieht so aus:


> make
compiling testmain.o from testmain.c
compiling testcode.o from testcode.c
compiling othercode.o from othercode.c
linking testmain
>

Das folgende Makefile hat zwei Ziele, nmlich die Erzeugung einer Bibliothek libtest.a, in welche
die Objektdateien testcode.o und othercode.o einflieen, und die Erzeugung einer ausfhrbaren
Datei testmain aus der Objektdatei testmain.o und der zuvor erzeugten Bibliothek libtest.a:
1

all: libtest.a testmain

2
3
4
5

%.o: %.c
@echo compiling $@ from $<
@gcc -Wall -c $< -o $@

6
7
8
9

testmain.o: testcode.h othercode.h


testcode.o:
othercode.o: othercode.h

10
11

libobj = testcode.o othercode.o

12
13
14
15

libtest.a: $(libobj)
@echo creating libtest.a
@ar rcs libtest.a $(libobj)

16
17
18
19

testmain: testmain.o libtest.a


@echo linking testmain
@gcc -o testmain testmain.o -L. -ltest

60

20

clean:

21

rm -f libtest.a testmain testmain.o $(libobj)

22

Mit dem Aufruf make oder make all wird nun sowohl die Bibliothek als auch die ausfhrbare Datei
erzeugt:
> make all
compiling testcode.o from testcode.c
compiling othercode.o from othercode.c
creating libtest.a
compiling testmain.o from testmain.c
linking testmain
>

Das make-Tool erlaubt sehr viel komplexere Makefiles, z.B. die Interaktion mit der Shell, den komfortablen Umgang mit Variablen, die bedingte Ausfhrung usw.; es sei auf die ausfhrliche Dokumentation
von GNU-Make verwiesen.

13 Aufgaben
13.1 Game of Life
Das Game of Life (J. H. Conway) ist ein Zellular-Automat mit interessanten lebenshnlichen Eigenschaften. Der Automat besteht aus einem zweidimensionalen Feld von Zellen, von denen jede entweder
den Zustand 0 (tot) oder den Zustand 1 (lebendig) annehmen kann. In jeder Generation werden
alle Zellen gleichzeitig nach einer Spielregel aktualisiert. Diese Spielregel besagt, dass (1) eine tote
Zelle lebendig wird, wenn genau 3 ihrer unmittelbaren 8 Nachbarn im Gitter lebendig sind, (2) eine lebende Zelle stirbt, wenn weniger als 2 oder mehr als 3 ihrer 8 Nachbarn lebendig sind (Vereinsamung
oder berfllung), und (3) der Zustand der Zelle ansonsten unverndert bleibt.
Implementieren Sie diesen Zellular-Automaten in C. Dem Programm sollen die gewnschte Gre des
Feldes, die Anzahl der Generationen und eine Verzgerung (in ms) auf der Kommandozeile bergeben
werden. Das Programm soll ein Spielfeld der angegebenen Gre erzeugen (sie knnen dazu die FeldMakros aus Abschnitt 9.5.2 verwenden) und mit einer Anfangskonfiguration initialisieren. Interessante
Anfangs-Konfigurationen lebender Zellen (*) sind der Glider (links) und das Pi (rechts):
*
*
***

***
* *
* *

In jeder Generation erfolgt die Aktualisierung aller Zellen gleichzeitig. Gestalten Sie Ihr Programm so,
dass das Ergebnis unabhngig von der Reihenfolge der Aktualisierung der Zellen ist. Das Spielfeld soll
zum Torus geschlossen sein, d.h. alle 4 Ecken sind unmittelbar benachbart. Verwenden Sie ModuloOperationen, um dieses Geometrie zu realisieren. Beachten Sie, dass der Modulo-Operator % fr negative
Zahlen nicht das (vermutlich) von Ihnen gewnschte Verhalten zeigt. Geben Sie nach jeder Aktualisierung das Feld auf der Standard-Ausgabe aus und fgen Sie eine kurze Wartezeit entsprechend des
bergebenen Wertes ein. Wiederholen Sie dies fr die angegebene Anzahl von Iterationen. (15 Punkte,
Musterlsung: life.c).

61

C33

Anker
next
prev

Struktur 1

Struktur 2

Struktur 3

next
prev

next
prev

next
prev

Abbildung 18: Doppelt verkettete Liste mit list_head Datenstruktur (dick) (B OVET and C ESATI
2003, Abb. 3.4).

13.2 Doppelt verkettete Listen


Schreiben Sie Funktionen zur Verwaltung von doppelt verketteten Listen. Sie werden diese Funktionen
spter fr die Implementation von Prozess-Listen bentigen. Eine elegante Vorgehensweise ist es, eine
Struktur list_head zu definieren
struct list_head {
struct list_head *next, *prev;
};

welche einerseits als Listen-Anker dient und andererseits als Komponente in die Strukturen der Listenelemente aufgenommen werden kann, z.B.
struct proc_info {
struct list_head head;
int pid;
int counter;
int priority;
};

Wird head als erstes Element der Struktur definiert, so stimmen die Adresse von head und die Adresse der Struktur berein entsprechend kann durch explizite Typumwandlung aus der Adresse eines
struct list_head die Adresse einer struct proc_info gewonnen werden. Eine solcherart
realisierte Liste ist in Abbildung 18 dargestellt.
In dieser Implementation ist eine Liste genau dann leer, wenn im Anker (Typ struct list_head)
beide Zeiger auf den Anker selbst zeigen. Ihre Header-Datei sollte so aussehen:
1
2
3

/* initialize "shortcut links" for empty list */


extern void
list_init(struct list_head *head);

4
5
6
7

/* insert new entry after specified head */


extern void
list_add(struct list_head *new, struct list_head *head);

8
9
10
11

/* insert a new entry before the specified head */


extern void
list_add_tail(struct list_head *new, struct list_head *head);

12
13

/* deletes entry from list, reinitializes it (next = prev = 0),

62

C34

and returns pointer to entry */


extern struct list_head*
list_del(struct list_head *entry);

14
15
16
17

/* delete entry from one list and add as anothers head */


extern void
list_move(struct list_head *entry, struct list_head *head);

18
19
20
21

/* delete entry from one list and add as anothers tail */


extern void
list_move_tail(struct list_head *entry, struct list_head *head);

22
23
24
25

/* tests whether a list is empty */


extern int
list_empty(struct list_head *head);

26
27
28

Schreiben Sie ein Testprogramm, welches eine Prozessliste mit 5 Elementen mit pid von 0 bis 4 anlegt, und testen Sie alle Listenfunktionen. Lassen Sie sich nach jeder Manipulation die Liste auf der
Standardausgabe anzeigen. (15 Punkte, Musterlsung: dll.c).

Literaturverzeichnis
C function call conventions and the stack.
http://www.cs.umbc.edu/chang/cs313.s02/stack.shtml.

The function pointer tutorials.


http://www.function-pointer.org.

The GNU C library.


http://www.gnu.org/manual/glibc-2.2.5/html_node/.

B OVET, DANIEL P. and M. C ESATI (2003). Understanding the Linux Kernel. OReilly & Associates,
Sebastopol, CA USA, 2. ed.
C LASSEN , L UDWIG und U. O EFLER (1987). UNIX und C. Ein Anwenderhandbuch. VEB Verlag
Technik Berlin, 2. Aufl.
C LAUSS , M ATTHIAS und G. F ISCHER (1988). Programmieren mit C. VEB Verlag Technik Berlin, 1.
Aufl.
G OOGLE D IRECTORY. ComputerProgrammierenSpracheC.
http://directory.google.com/Top/World/Deutsch/Computer/Programmieren/Sprachen/C/.

H EROLD , H ELMUT und J. A RNDT (2001). C-Programmierung unter Linux. SuSE Press, Nrnberg.
H OLMES , S TEVE. C programming.
http://www.strath.ac.uk/IT/Docs/Ccourse.

L OVE , T IM. ANSI C for programmers on UNIX systems.


http://www.eng.cam.ac.uk/help/tpl/languages/C/teaching_C/teaching_C.html.

M ARSHALL , A. D. Programming in C Unix system calls and subroutines.


http://www.cs.cf.ac.uk/Dave/C.

63

S CHELLONG , H ELMUT. C-Tutorial und Referenz.


http://www.schellong.de/c.htm.

S UMMIT, S TEVE. C material (FAQ, classes).


http://www.eskimo.com/scs.

Liste der Aufgaben


Aufgabe 1
Aufgabe 2
Aufgabe 3
Aufgabe 4
Aufgabe 5
Aufgabe 6
Aufgabe 7
Aufgabe 8
Aufgabe 9
Aufgabe 10
Aufgabe 11
Aufgabe 12
Aufgabe 13
Aufgabe 14
Aufgabe 15
Aufgabe 16
Aufgabe 17
Aufgabe 18
Aufgabe 19
Aufgabe 20
Aufgabe 21
Aufgabe 22
Aufgabe 23
Aufgabe 24
Aufgabe 25
Aufgabe 26
Aufgabe 27
Aufgabe 28
Aufgabe 29
Aufgabe 30
Aufgabe 31
Aufgabe 32
Aufgabe 33
Aufgabe 34

(S. 7)
(S. 8)
(S. 11)
(S. 12)
(S. 13)
(S. 15)
(S. 15)
(S. 16)
(S. 23)
(S. 23)
(S. 27)
(S. 33)
(S. 33)
(S. 33)
(S. 34)
(S. 35)
(S. 35)
(S. 38)
(S. 40)
(S. 41)
(S. 42)
(S. 42)
(S. 43)
(S. 43)
(S. 45)
(S. 45)
(S. 47)
(S. 47)
(S. 50)
(S. 52)
(S. 52)
(S. 53)
(S. 61)
(S. 62)

0 Punkte
0 Punkte
0 Punkte
0 Punkte
0 Punkte
0 Punkte
0 Punkte
0 Punkte
0 Punkte
0 Punkte
0 Punkte
0 Punkte
0 Punkte
0 Punkte
0 Punkte
0 Punkte
0 Punkte
3 Punkte
3 Punkte
3 Punkte
5 Punkte
8 Punkte
0 Punkte
0 Punkte
3 Punkte
0 Punkte
0 Punkte
3 Punkte
3 Punkte
9 Punkte
3 Punkte
3 Punkte
15 Punkte
15 Punkte

Compileraufruf
Debugger
Makro vs. Funktion
Prprozessorlauf
Makro-Operator
2K-Zahlen
2K-Zahlen
Oktal- und Hexadimalzahlen
Felder austauschen
Element austauschen
Export
Flags als Oktalzahlen
Flags
Flags
Abfrage mit Flags
Klammerung
Prioritten
Schaltjahre
Trennzeichen entfernen
Pascalsches Dreieck
Makro fr zweidimensionale Felder
Makro fr kompakte zweidimensionale Felder
Lsung ohne continue
Lsung ohne goto
Rckgabe von zwei Werten mit return
Funktion swapPtr
Signal ausprobieren
Funktionszeiger fr mathematische Funktionen
Stack analysieren
Rekursive Listenfunktionen
Rekursive ggt-Funktion
Kommandozeilen-Parameter: Taschenrechner
Zellularautomat Life
Doppelt verkettete Listen

Gesamtpunktzahl: 76

64

Das könnte Ihnen auch gefallen