Beruflich Dokumente
Kultur Dokumente
Release 0.2.0d
Aktualisiert am 02.12.2018
Bernhard Grotz
http://www.grund-wissen.de
Dieses Buch wird unter der Creative Commons License (Version 3.0, by-nc-sa) veröffent-
licht. Alle Inhalte dürfen daher in jedem beliebigen Format vervielfältigt und/oder wei-
terverarbeitet werden, sofern die Weitergabe nicht kommerziell ist, unter einer gleichen
Lizenz erfolgt, und das Original als Quelle genannt wird. Siehe auch:
Unabhängig von dieser Lizenz ist die Nutzung dieses Buchs für Unterricht und Forschung
(§52a UrhG) sowie zum privaten Gebrauch (§53 UrhG) ausdrücklich erlaubt.
Der Autor erhebt mit dem Buch weder den Anspruch auf Vollständigkeit noch auf Feh-
lerfreiheit; insbesondere kann für inhaltliche Fehler keine Haftung übernommen werden.
Die Quelldateien dieses Buchs wurden unter Linux mittels Vim und Sphinx, die enthal-
tenen Graphiken mittels Inkscape erstellt. Der Quellcode sowie die Original-Graphiken
können über die Projektseite heruntergeladen werden:
http://www.grund-wissen.de
Bei Fragen, Anmerkungen und Verbesserungsvorschlägen bittet der Autor um eine kurze
Email an folgende Adresse:
info@grund-wissen.de
Bernhard Grotz
Inhaltsverzeichnis
Kontrollstrukturen 31
if, elif und else – Bedingte Anweisungen . . . . . . . . . . . . . . . . . . . . 31
switch – Fallunterscheidungen . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
for und while – Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Zusammengesetzte Datentypen 39
typedef – Synonyme für andere Datentypen . . . . . . . . . . . . . . . . . . . . 39
enum – Aufzählungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
struct – Strukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
union – Alternativen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
i
Dateien und Verzeichnisse 45
Dateien und File-Pointer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
Daten in eine Datei schreiben . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Daten aus einer Datei einlesen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Modularisierung 50
Präprozessor, Compiler und Linker 51
Präprozessor-Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
#include – Einbinden von Header-Dateien . . . . . . . . . . . . . . . . . 51
#define – Definition von Konstanten und Makros . . . . . . . . . . . . . 52
#if, #ifdef, #ifndef – Bedingte Compilierung . . . . . . . . . . . . . . . 53
Compiler-Optionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Verlinken von Bibliotheken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Dynamische Datenstrukturen 57
Verkettete Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
Hilfreiche Werkzeuge 64
astyle – Code-Beautifier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
cdecl – Deklarations-Übersetzer . . . . . . . . . . . . . . . . . . . . . . . . . . 64
cflow – Funktionsstruktur-Viewer . . . . . . . . . . . . . . . . . . . . . . . . . . 65
doxygen – Dokumentations-Generator . . . . . . . . . . . . . . . . . . . . . . . 65
gdb – Debugger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
gprof – Profiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
make – Compilier-Hilfe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
splint – Syntax Checker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
time – Timer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
valgrind - Speicher-Testprogramm . . . . . . . . . . . . . . . . . . . . . . . . . 73
Die C-Standardbibliothek 75
assert.h – Einfache Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
math.h – Mathematische Funktionen . . . . . . . . . . . . . . . . . . . . . . . . 75
cmath.h – Mathe-Funktionen für komplexe Zahlen . . . . . . . . . . . . . . . . 77
string.h – Zeichenkettenfunktionen . . . . . . . . . . . . . . . . . . . . . . . . 77
stdio.h – Ein- und Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
stdlib.h – Hilfsfunktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
time.h – Funktionen für Datum und Uhrzeit . . . . . . . . . . . . . . . . . . . 86
Curses 89
Curses starten und beenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Ausgeben und Einlesen von Text . . . . . . . . . . . . . . . . . . . . . . . . . . 90
Editor-Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
ii
Attribute und Farben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
Fenster und Unterfenster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Pads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
Debugging von Curses-Programmen . . . . . . . . . . . . . . . . . . . . . . . . . 100
Links 102
Literaturverzeichnis 104
Stichwortverzeichnis 105
iii
Einführung: Editieren und Übersetzen
Um ein lauffähiges C-Programm zu erzeugen, muss zunächst mit einem Texteditor eine
Quelltext-Datei angelegt und mit Code gefüllt werden. Anschließend wird ein Compiler
gestartet, der den Quellcode in Maschinen-Code übersetzt und ein lauffähiges Programm
erstellt.
Als klassisches Beispiel soll hierzu ein minimales Programm dienen, das lediglich "Hallo,
Welt!" auf dem Bildschirm ausgibt. Hierzu wird mit einem Texteditor folgender Code in
eine (neue) Datei hallo.c geschrieben:
// Datei: hallo.c /* 1. */
# include <stdio.h> /* 2. */
void main() /* 3. */
{
printf("Hallo, Welt!\n"); /* 4. */
}
1. Eine mit // eingeleitete Zeile am Dateianfang stellt einen Kommentar dar. Sie wird
beim Übersetzen durch den Compiler ignoriert und dient lediglich der besseren Les-
barkeit. Ebenso werden Textbereiche, die durch /* und */ begrenzt sind, als Kom-
1
mentare für Erklärungen oder Hinweise genutzt.
3. Die Funktion main() startet das Hauptprogramm, das sich innerhalb der folgenden
geschweiften Klammern befindet. Jedes C-Programm verfügt über eine derartige
main()-Funktion.3
1 In vielen Programmen werden ausschließlich Kommentare verwendet, die mit den Zeichenfolgen /*
und */ begrenzt sind. Hierdurch wird eine Kompatibilität mit alten C-Compiler-Versionen sicher gestellt.
Im obigen Tutorium wird hingegen – nach persönlichem Geschmack – die //-Variante für (einzeilige)
Kommentare verwendet.
Zusätzliche Kommentare der Form /* 1. */ dienen in diesem Tutorium als Marker, um im Text auf
die jeweiligen Stellen im Quellcode eingehen zu können.
2 Genauer gesagt gilt die Anweisumg dem Präprozessor, einem Teil des Compilers.
3 Die Bezeichung void besagt lediglich, dass die Funktion keinen Rückgabe-Wert liefert, der ander-
weitig im Programm zu verwenden wäre.
1
4. Durch den Aufruf der Funktion printf() wird auf dem Bildschirm der in doppel-
ten Hochkommata stehende Text ausgegeben. Die Zeichenfolge \n steht dabei als
Zeichen für eine neue Zeile. Der Aufruf der Funktion muss, wie jede C-Anweisung,
mit einem Strichpunkt ; beendet werden.
Durch die Option -o hallo wird dabei die Output-Datei, d.h. das fertige Programm,
mit hallo benannt. Ist der Compilier-Vorgang abgeschlossen, kann das neu geschriebene
Programm im gleichen Ordner aufgerufen werden:
./hallo
Damit ist das erste C-Programm fertig gestellt. In den folgenden Abschnitten werden
weitere Eigenschaften und Funktionen der Programmiersprache C erläutert sowie einige
nützliche Werkzeuge und Programmiertechniken vorgestellt.
2
Definition von Variablen
Da ein Computer-Prozessor nur mit Maschinencode arbeiten kann, müssen intern sowohl
Zahlen wie auch Text- und Sonderzeichen als Folgen von Nullen und Einsen dargestellt
werden. Dies ist aus der Sichtweise eines Programmierers zunächst nur soweit von Bedeu-
tung, als dass er wissen muss, dass ein und dieselbe Folge von Nullen und Einsen vom
Computer wahlweise als Zeichen oder als Zahl interpretiert werden kann. Der Program-
mierer muss dem Computer somit mitteilen, wie der Inhalt einer Variable zu interpretieren
ist.
Um Variablen benutzen zu können, muss der Datentyp der Variablen (z.B. int für ganze
Zahlen) dem Compiler mitgeteilt werden („Deklaration“). Muss dabei auch Speicherplatz
reserviert werden (was meist der Fall ist, wenn sich die Deklaration nicht auf Variablen
externer Code-Bibliotheken bezieht), so spricht man von einer Definition einer Variablen.
In C werden Variablen stets zu Beginn einer Datei oder zu Beginn eines neuen, durch
geschweifte Klammern begrenzten Code-Blocks definiert. Sie sind im Programm gültig,
1
bis die Datei beziehungsweise der jeweilige Code-Block abgearbeitet ist.
int n;
Es dürfen auch mehrere gleichartige Variablen auf einmal definiert werden; hierzu werden
die einzelnen Namen der Variablen durch Kommata getrennt und die Definition mit einem
abschließenden Strichpunkt beendet.
int x,y,z;
Wird einer Variablen bei der Definition auch gleich ein anfänglicher Inhalt („Initialwert“)
2
zugewiesen, so spricht man auch von einer Initiation einer Variablen.
3
int c = 256;
Variablennamen dürfen in C maximal 31 Stellen lang sein. Sie können aus den Buchstaben
A-Z und a-z, den Ziffern 0-9 und dem Unterstrich bestehen. Die einzige Einschränkung
besteht darin, dass am Anfang von Variablennamen keine Ziffern stehen dürfen; Unter-
striche am Anfang von Variablennamen sind zwar erlaubt, sollten aber vermieden werden,
da diese üblicherweise für Bibliotheksfunktionen reserviert sind.
Ist einmal festgelegt, um welchen Datentyp es sich bei einer Variablen handelt, wird die
Variable im Folgenden ohne Angabe des Datentyps verwendet.
Elementare Datentypen
Der Speicherbedarf der einzelnen Datentypen hängt von der konkreten Rechnerarchitektur
ab; in der obigen Tabelle sind die Werte für 32-Bit-Systeme angegeben, die für Monocore-
Prozessoren üblich sind. Auf anderen Systemen können sich andere Werte für die einzelnen
Datentypen ergeben. Die Größe der Datentypen auf dem gerade verwendeten Rechner
kann mittels des sizeof -Operators geprüft werden:
// Datei: sizeof.c
# include <stdio.h>
void main()
(continues on next page)
3 Der Wertevergleich, wie er in der Mathematik durch das Ist-Gleich-Zeichen ausgedrückt wird, erfolgt
in C durch den Operator ==.
4
(Fortsetzung der vorherigen Seite)
{
printf("Size of char: %lu\n", sizeof (char) );
printf("Size of int: %lu\n", sizeof (int) );
printf("Size of short: %lu\n", sizeof (short) );
printf("Size of long: %lu\n", sizeof (long) );
printf("Size of float: %lu\n", sizeof (float) );
printf("Size of double: %lu\n", sizeof (double));
}
Einen „Booleschen“ Datentyp, der die Wahrheitswerte True oder False repräsentiert, exis-
tiert in C nicht. Stattdessen wird der Wert Null für False und jeder von Null verschiedene
Wert als True interpretiert.
Komplexere Datentypen lassen sich aus diesen elementaren Datentypen durch Anein-
anderreihungen ( Felder ) oder Definitionen von Strukturen (struct) erzeugen. Zusätzlich
existiert in C ein Datentyp namens void, der null Bytes groß ist und beispielsweise dann
genutzt wird, wenn eine Funktion keinen Wert als Rückgabe liefert.
Modifier
Alle grundlegenden Datentypen (außer void) können zusätzlich mit einem der folgenden
„Modifier“ versehen werden:
extern:
Dieser Modifier ist bei der Deklaration einer Variablen nötig, wenn diese
bereits in einer anderen Quellcode-Datei definiert wurde. Für externe Va-
riablen wird kein neuer Speicherplatz reserviert. Gleichzeitig wird durch
den extern-Modifier dem Compiler mitgeteilt, in den zu Beginn einge-
bundenen Header-Dateien nach einer Variablen dieses Namens zu suchen
und den dort reservierten Speicherplatz gemeinsam zu nutzen.
static:
5
Eine Variable ist üblicherweise nur innerhalb des jeweiligen durch ge-
schweifte Klammern begrenzten Codeblocks gültig, innerhalb dessen sie
definiert wurde.
Auch Variablen, die gleich zu Beginn einer Datei definiert werden, können
mit dem Modifier static versehen werden. Auf eine solche Variable kön-
nen dann alle Funktionen dieser Datei zugreifen, für Funktionen anderer
Dateien ist sie hingegen nicht sichtbar.
Umgekehrt ist jede Funktion und jede außerhalb einer Funktion definier-
te Variable „global“, wenn sie nicht mit static versehen wurde. Globale
Variablen sollten, sofern möglich, vermieden werden, da sie von vielen
Stellen aus manipuliert werden können und im Zweifelsfall die Fehler ver-
ursachende Stelle im Code nur schwer gefunden wird.
const:
Mit const können Variablen bezeichnet werden, auf die nur lesend zu-
gegriffen werden sollte. Schreibzugriffe auf solche Konstanten sind zwar
möglich, sollten jedoch vermieden werden, da das Ergebnis undefiniert
ist. Das Schlüsselwort const wird somit zur besseren Lesbarkeit verwen-
det und erlaubt es dem Compiler, gewisse Optimierungen vorzunehmen.
volatile
Es gibt Variablen, die sich ändern können, ohne dass der Compiler dies
vermuten würde. Üblicherweise werden solche Variablen vom Compiler
aus Optimierungsgründen durch eine Konstante ersetzt und nicht stets
6
erneut eingelesen. Mit dem Schlüsselwort volatile hingegen zwingt man
den Compiler, den Wert dieser Variablen bei jeder Benutzung erneut aus
dem Speicher zu lesen und mehrfaches Lesen nicht weg zu optimieren.
Das ist beispielsweise wichtig bei Variablen, die Zustände von Hardware-
komponenten anzeigen, oder bei Variablen, die durch Interrupt-Routinen
verändert werden.
Beispiel:
volatile int Tastenzustand;
Tastenzustand = 0;
while (Tastenzustand == 0)
{
// Warten auf Tastendruck
}
7
Zeiger und Felder
In vielen Fällen ist es nützlich, Variablen nicht direkt anzusprechen, sondern anstatt
dessen so genannte Zeiger („Pointer“) zu nützen. Bei einem solchen Zeiger handelt es sich
um eine eigenständige Variable, deren Inhalt die Speicheradresse einer anderen Variablen
ist.
Zeiger
Bei der Definition eines Zeigers wird festgelegt, für welchen Datentyp der Zeiger vorgese-
hen ist. Die Definition eines Zeigers ähnelt dabei weitgehend der einer normalen Variablen,
mit dem Unterschied, dass zur eindeutigen Kennzeichnung vor den Namen der Zeigerva-
riablen ein * geschrieben wird:
int *n;
Es dürfen wiederum mehrere Zeiger auf einmal definiert werden; hierzu werden die ein-
zelnen Namen der Zeigervariablen durch Kommata getrennt und die Definition mit einem
abschließenden Strichpunkt beendet.
Um einer Zeigervariablen einen Inhalt, d.h. die eine gültige Speicheradresse zuzuweisen,
wird der so genannte Adress-Operator & verwendet. Wird dieser Operator vor eine belie-
bige Variable geschrieben, so gibt er die zugehörige Speicheradresse aus. Diese kann wie
gewöhnlich in der Variablen auf der linken Seite des =-Zeichens gespeichert werden:
p_num = #
8
In diesem Beispiel ist p_num ein Zeiger auf eine Integer-Variable, hat also selbst den Daten-
typint *. Entsprechend gibt es auch Zeiger auf die anderen Datentypen, beispielsweise
float *, char * usw.1
Ein Zeiger, dem noch keine Speicheradresse zugewiesen würde oder der auf eine ungültige
2
Speicheradresse zeigt, bekommt in C automatisch den Wert NULL zugewiesen.
Der Inhalts-Operator *
Möchte man den Zeiger wiederum dazu nutzen, um auf den Inhalt der Speicheradresse
zuzugreifen, kann der sogenannte Inhalts-Operator * verwendet werden. Angewendet auf
eine bereits deklarierte Variable gibt dieser den zur Speicheradresse gehörigen Inhalt aus.
Erzeugt man beispielsweise einen Zeiger b, der auf eine Variable a zeigt, so ist *b identisch
mit dem Wert von a:
int a;
int *b;
a = 15;
b = &a;
Der *-Operator kann auch für Wertzuweisungen, also auf der linken Seite des Istgleich-
Zeichens benutzt werden. Hierbei muss der Programmierer allerdings unbedingt darauf
achten, dass der jeweilige Zeiger bereits initiiert (nicht NULL) ist, sondern auf eine gültige
Speicherstelle zeigt:
int a;
int *b;
1 Es gibt auch void *-Zeiger, die auf keinen bestimmten Datentyp zeigen. Solche Zeiger werden bei-
spielsweise von der Funktion malloc() bei einer dynamischen Reservierung von Speicherplatz als Ergeb-
nis zurückgegeben. Der Programmierer muss in diesem Fall dem Zeiger selbst den gewünschten Datentyp
zuweisen.
2 Der Grund für die Verwendung eines NULL-Zeigers (einer in der Datei stddef.h definierten Kon-
stanten mit dem Wert 0) liegt darin, dass eine binär dargestellte Null in C niemals als Speicheradresse
verwendet wird.
Manchmal wird der NULL-Pointer von Funktionen , die gewöhnlich einen bestimmten Zeiger als Ergebnis
liefern, zur Anzeige einer erfolglosen Aktion verwendet. Liegt kein Fehler vor, so ist der Rückgabewert
die Adresse eines Speicherobjektes und somit von 0 verschieden.
9
(Fortsetzung der vorherigen Seite)
Wäre der Zeiger auf der linken Seite gleich NULL, so würde die Wertzuweisung an eine
undefinierte Stelle erfolgen; im schlimmsten Fall würde eine andere für das Programm
wichtige Speicheradresse überschrieben werden. Ein solcher Fehler kann vom Compiler
nicht erkannt werden, kann aber mit großer Wahrscheinlichkeit ein abnormales Verhalten
des Programms oder einen Absturz zur Folge haben.
Felder
Als Feld („Array“) bezeichnet man eine Zusammenfassung von mehreren Variablen glei-
chen Datentyps zu einem gemeinsamen Speicherbereich.
Bei der Definition eines Arrays muss einerseits der im Array zu speichernde Datentyp
angegeben werden, andererseits wird zusätzlich in eckigen Klammern die Größe des Arrays
angegeben. Damit ist festgelegt, wie viele Elemente in dem Array maximal gespeichert
3
werden können. Die Syntax lautet somit beispielsweise:
int numbers[10];
Wird ein Array bei der Definition gleich mit einem konkreten Inhalt initialisiert, so kann
die explizite Größenangabe entfallen und anstelle dessen ein leeres Klammerpaar [] ge-
setzt werden.
Der Hauptvorteil bei der Verwendung von Arrays liegt darin, eine Vielzahl gleichartiger
Datei über eine einzige Variable (den Namen des Arrays) ansprechen zu können. Auf
die einzelnen Elemente eines Feldes kann nach im eigentlichen Programm mittels des so
genannten Selektionsoperators [] zugegriffen werden. Zwischen die eckigen Klammern
wird dabei ein (ganzzahliger) Laufindex i geschrieben.
Hat ein Array insgesamt n Elemente, so kann der Laufindex i alle ganzzahligen Werte
zwischen 0 und n-1 annehmen. Das erste Element hat also den Index 0, das zweite den
Index 1, das letzte schließlich den Index n-1. Somit kann der Inhalt jeder im Array
gespeicherten Variablen ausgelesen oder durch einen anderen ersetzt werden:
3 Die Größe von Feldern kann nach der Deklaration nicht mehr verändert werden. Somit muss das
Feld ausreichend groß gewählt werden, um alle zu erwartenden Werte speichern zu können. Andererseits
sollte es nicht unnötig groß gewählt werden, da ansonsten auch unnötig viel Arbeitsspeicher reserviert
wird.
Soll die Größe eines Feldes erst zur Laufzeit festgelegt werden, so müssen die Funktionen malloc()
bzw. calloc() verwendet werden.
10
int numbers[5];
numbers[0] = 3;
numbers[1] = 5;
numbers[2] = 8;
numbers[3] = 13;
numbers[4] = 21;
Eine Besonderheit von Arrays in C ist es, dass der Compiler beim Übersetzen nicht prüft,
ob bei der Verwendung eines Laufindex die Feldgrenzen eingehalten werden. Im Fall ei-
nes Arrays numbers mit fünf Elementen könnte beispielsweise mit numbers[5] = 1 ein
Eintrag in einen Speicherbereich geschrieben werden, der außerhalb des Arrays liegt. Auf
korrekte Indizes muss somit der Programmierer achten, um Programmfehler zu vermeiden.
Mehrdimensionale Felder
Ein Array kann wiederum Arrays als Elemente beinhalten. Beispielsweise kann man sich
eine Tabelle aus einer Vielzahl von Zeilen zusammengesetzt denken, die ihrerseits wieder-
um eine Vielzahl von Spalten bestehen können. Beispielsweise könnte ein solches Tabellen-
4
Array, das als Einträge jeweils Zahlen erwartet, folgendermaßen deklariert werden:
Auch in diesem Fall laufen die Indexwerte bei 𝑛 Einträgen nicht von 1 bis 𝑛, sondern von
0 bis 𝑛 − 1. Der erste Auswahloperator greift ein Zeilenelement heraus, der zweite eine
bestimmte Spalte der ausgewählten Zeile. Auch eine weitere Verschachtelung von Arrays
nach dem gleichen Prinzip ist möglich, wobei der Zugriff auf die einzelnen Werte meist
über for -Schleifen erfolgt.
In C sind Felder und Zeiger eng miteinander verwandt: Gibt man den Namen einer Array-
Variablen ohne eckige Klammern an, so entspricht dies einem Zeiger auf die erste Spei-
cheradresse, die vom Array belegt wird; nach der Deklaration int numbers[10]; kann
also beispielsweise als abkürzende Schreibweise für das erste Element des Feldes anstelle
5
von &numbers[0] auch die Kurzform numbers benutzt werden.
4 Eine direkte Initialisierung eines mehrdimensionalen Arrays ist ebenfalls unmittelbar möglich; dabei
werden die einzelnen „Zeilen“ für eine bessere Lesbarkeit in geschweifte Klammern gesetzt. Beispielsweise
kann gleich bei der Definition int zahlentabelle[3][4] = { {3,4,1,5}, {8,5,6,9}, {4,7,0,3} };
geschrieben werden.
5 Legt man bei der Deklaration eines Feldes seine Groesse nicht fest, um diese erst zur Laufzeit mittels
malloc() zu reservieren, so kann bei der Deklaration anstelle von int numbers[]; ebenso int *numbers;
geschrieben werden.
11
Da alle Elemente eines Arrays den gleichen Datentyp haben und somit gleich viel Spei-
cherplatz belegen, unterscheiden sich die einzelnen Speicheradressen der Elemente um
die Länge des Datentyps, beispielsweise um sizeof (int) für ein Array mit int-Werten
oder sizeof (float) für ein Array mit float-Werten. Ausgehend vom ersten Element
eines Arrays erhält man somit die weiteren Elemente des Feldes, indem man den Wert
des Zeigers um das 1, 2, . . . , 𝑛 − 1-fache der Länge des Datentyps erhöht:
int numbers[10];
int *numpointer;
Beim Durchlaufen eines Arrays ist eine Erhöhung des Zeigers in obiger Form auch mit
dem Inkrement-Operator möglich: Es kann also auch numpointer++ statt numpointer =
numpointer + sizeof (int) geschrieben werden, um den Zeiger auf das jeweils nächste
Element des Feldes zu bewegen; dies wird beispielsweise in for -Schleifen genutzt. Ebenso
kann das Feld mittels numpointer-- schrittweise rückwärts durchlaufen werden; auf das
Einhalten der Feldgrenzen muss der Programmierer wiederum selbst achten.
Andere mathematische Operationen sollten auf Zeiger nicht angewendet werden; ebenso
sollten Array-Variablen, obwohl sie letztlich einen Zeiger auf das erste Element des Feldes
darstellen, niemals direkt inkrementiert oder dekrementiert werden, da das Array eine
feste Stelle im Speicher einnimmt. Stattdessen definiert man stets einen Zeiger auf das
erste Element des Feldes und inkrementiert diesen, um beispielsweise in einer Schleife auf
die einzelnen Elemente eines Feldes zuzugreifen.
Zeichenketten
Zeichenketten („Strings“), beispielsweise Worte und Sätze, stellen die wohl häufigste Form
von Arrays dar. Eine Zeichenkette besteht aus einer Aneinanderreihung einzelner Zeichen
(Datentyp char) und wird stets mit einer binären Null ('\0') abgeschlossen. Beispiels-
weise entspricht die Zeichenkette "Hallo!" einem Array, das aus 'H', 'a', 'l', 'l', 'o',
'!' und dem Zeichen '\0' besteht. Dieser Unterschied besteht allgemein zwischen Zei-
12
chenketten, die mit doppelten Hochkommatas geschrieben werden, und einzelnen Zeichen,
die in einfachen Hochkommatas dargestellt werden.
Die Deklaration einer Zeichenkette entspricht der Deklaration eines gewöhnlichen Feldes:
Bei der Festlegung der maximalen Länge der Zeichenkette muss beachtet werden, dass
neben den zu speichernden Zeichen auch Platz für das String-Ende-Zeichen '\0' blei-
ben muss. Als Programmierer muss man hierbei selbst darauf achten, dass die Feldgröße
ausreichend groß gewählt wird.
Wird einer String-Variablen nicht bereits bei der Deklaration eine Zeichenkette zugewie-
sen, so ist dies anschliessend zeichenweise (beispielsweise mittels einer Schleife ) möglich:
string_one[0] = 'H';
string_one[1] = 'a';
string_one[2] = 'l';
string_one[3] = 'l';
string_one[4] = 'o';
string_one[5] = '!';
string_one[6] = '\0';
Eine Zuweisung eines ganzen Strings an eine String-Variable in Form von string_one
= "Hallo!" ist nicht direkt möglich, sondern muss über die Funktion strcpy() aus der
Standard-Bibliothek string.h erfolgen:
// Am Dateianfang:
# include <string.h>
// ...
// String-Variable deklarieren:
char string_one[15];
// Zeichenkette ausgeben:
printf("%s\n", string_one);
Anstelle der Funktion strcpy() kann auch die Funktionstrncpy() verwendet werden,
die nach der zu kopierenden Zeichenkette noch einen int-Wert 𝑛 erwartet; diese Funktion
kopiert maximal 𝑛 Zeichen in die Zielvariable, womit ein Überschreiten der Feldgrenzen
ausgeschlossen werden kann.
13
ASCII-Codes und Sonderzeichen
Die einzelnen Zeichen (Datentyp char) werden vom Computer intern ebenfalls als ganz-
zahlige Werte ohne Vorzeichen behandelt. Am weitesten verbreitet ist die so genannte
ASCII-Codierung („American Standard Code for Information Interchange“), deren Zu-
weisungen in der folgenden ASCII-Tabelle abgebildet sind. Wird beispielsweise nach der
Deklarierung char c; der Variablen c mittels c = 120 ein numerischer Wert zugewiesen,
so liefert die Ausgabe von printf("%c\n", c); den zur Zahl 120 gehörenden ACII-Code,
also x.
Dez AS- Dez AS- Dez AS- Dez AS- Dez AS- Dez AS- Dez AS- Dez AS-
CII CII CII CII CII CII CII CII
0 NUL 16 DLE 32 SP 48 0 64 @ 80 P 96 ‘ 112 p
1 SOH 17 DC1 33 ! 49 1 65 A 81 Q 97 a 113 q
2 STX 18 DC2 34 " 50 2 66 B 82 R 98 b 114 r
3 ETX 19 DC3 35 # 51 3 67 C 83 S 99 c 115 s
4 EOT 20 DC4 36 $ 52 4 68 D 84 T 100 d 116 t
5 ENQ 21 NAK 37 % 53 5 69 E 85 U 101 e 117 u
6 ACK 22 SYN 38 & 54 6 70 F 86 V 102 f 118 v
7 BEL 23 ETB 39 ' 55 7 71 G 87 W 103 g 119 w
8 BS 24 CAN 40 ( 56 8 72 H 88 X 104 h 120 x
9 HT 25 EM 41 ) 57 9 73 I 89 Y 105 i 121 y
10 LF 26 SUB 42 * 58 : 74 J 90 Z 106 j 122 z
11 VT 27 ESC 43 + 59 ; 75 K 91 [ 107 k 123 {
12 FF 28 FS 44 , 60 < 76 L 92 \ 108 l 124 |
13 CR 29 GS 45 - 61 = 77 M 93 ] 109 m 125 }
14 SO 30 RS 46 . 62 > 78 N 94 ^ 110 n 126 ~
15 SI 31 US 47 / 63 ? 79 O 95 _ 111 o 127 DEL
Die zu den Zahlen 0 bis 127 gehörenden Zeichen sind bei fast allen Zeichensätzen identisch.
Da der ASCII-Zeichensatz allerdings auf die englische Sprache ausgerichtet ist und damit
keine Unterstützung für Zeichen anderer Sprachen beinhaltet, gibt es Erweiterungen des
ASCII-Zeichensatzes für die jeweiligen Länder.
Zeichen Bedeutung
\n Zeilenwechsel („new line“)
\t Tabulator (entspricht üblicherweise 4 Leerzeichen)
\b Backspace
\\ Backslash-Zeichen
\" Doppeltes Anführungszeichen
\' Einfaches Anführungszeichen
Eine weitere Escape-Sequenz ist das Zeichen '\0' als Endmarkierung einer Zeichenkette,
das verständlicherweise jedoch nicht innerhalb einer Zeichenketten stehen darf.
14
Ausgabe und Eingabe
Das Ausgeben und Einlesen von Daten über den Bildschirm erfolgt häufig mittels der
Funktionen printf() und scanf().1 Beide Funktionen sind Teil der Standard-Bibliothek
stdio.h , so dass diese zu Beginn der Quellcode-Datei mittels include <stdio.h> einge-
2
bunden werden muss.
Die Funktion printf() dient grundsätzlich zur direkten Ausgabe von Zeichenketten auf
dem Bildschirm; beispielsweise gibt printf("Hallo Welt!") die angegebene Zeichenkette
auf dem Bildschirm aus. Innerhalb der Zeichenketten können allerdings Sonderzeichen
sowie Platzhalter für beliebige Variablen und Werte eingefügt werden.
Zeichen Bedeutung
\n Neue Zeile
\t Tabulator (4 Leerzeichen)
\\ Backslash-Zeichen \
\' Einfaches Anführungszeichen
\" Doppeltes Anführungszeichen
Ein Platzhalter besteht aus einem %-Zeichen, gefolgt von einem oder mehreren Zeichen,
welche den Typ der auszugebenden Werte oder Variablen angeben und gleichzeitig festle-
gen, wie die Ausgabe formatiert werden soll. Damit kann beispielsweise bestimmt werden,
wie viele Stellen für einen Wert reserviert werden sollen, ob die Ausgabe links- oder rechts-
bündig erfolgen soll, und/oder ob bei der Ausgabe von Zahlen gegebenenfalls führende
Nullen angefügt werden sollen.
1 Um Daten von Dateien anstelle vom Bildschirm einzulesen, gibt es weitere Funktionen, die im Ab-
schnittDateien und Verzeichnisse näher beschrieben sind.
2 Genau genommen erfolgt bei der Funktion printf() die Ausgabe auf den Standard-Ausgang
(stdout). Bei diesem handelt es sich als Voreinstellung um den Bildschirm, in speziellen Fällen kann je-
doch mittels der Funktion freopen() auch eine beliebige Datei oder ein angeschlossenes Gerät als Standard-
Ausgang festgelegt werden.
Ebenso liest die Funktion scanf() vom Standard-Eingang (stdin) ein, der als Voreinstellung wiederum
dem Bildschirm entspricht.
15
// Den Wert Pi auf sechs Nachkommastellen genau ausgeben:
printf("%02i.:\n%02i.:\n%02i.:\n", 8, 9, 10);
// Ergebnis:
// 08.:
// 09.:
// 10.:
In den obigen Beispielen wurden der Funktion printf() zwei oder mehr Argumente über-
geben. Beim ersten Argument handelt es sich um einen so genannten Formatstring, bei
den folgenden Argumenten um die auf dem Bildschirm auszugebenden Werte. Falls diese,
wie im ersten Beispiel, mehr Nachkommastellen haben als in der Formatierung vorgesehen
(Die Angabe %.6f steht für einen Wert vom Datentyp float sechs Nachkommastellen),
so wird der Wert automatisch auf die angegebene Genauigkeit gerundet.
Zur Festlegung des Datentyps einer auszugebenden Variablen gibt es allgemein folgende
Umwandlungszeichen:
16
Zeichen Argument Bedeutung
d, i int Dezimal-Zahl mit Vorzeichen.
o int Oktal-Zahl ohne Vorzeichen (und ohne führende Null).
x, X int Hexadezimal-Zahl ohne Vorzeichen (und ohne führendes 0x oder 0X), also
abcdef bei 0x oder ABCDEF bei 0X.
u int Dezimal-Zahl ohne Vorzeichen.
c int Ein einzelnes Zeichen (unsigned char).
s char * Zeichen einer Zeichenkette bis zum Zeichen \0, oder bis zur angegebenen Ge-
nauigkeit.
f double Dezimal-Zahl als [-]mmm.ddd, wobei die angegebene Genauigkeit die Anzahl
der d festlegt. Die Voreinstellung ist 6, 0 entfällt der Dezimalpunkt.
bei
e, E double Dezimal-Zahl als [-]m.dddddde±xx oder [-]m.ddddddE±xx, wobei die ange-
gebene Genauigkeit die Anzahl der d festlegt. Die Voreinstellung ist 6, bei 0
entfällt der Dezimalpunkt.
g, G double Dezimal-Zahl wie wie %e oder %E. Wird verwendet, wenn der Exponent kleiner
als die angegebene Genauigkeit ist; unnötige Nullen am Schluss werden nicht
ausgegeben.
p void * Zeiger (Darstellung hängt von Implementierung ab).
n int * Anzahl der aktuell von printf() ausgegebenen Zeichen.
Die obigen Formatangaben lassen sich durch Steuerzeichen („flags“) zwischen dem %- und
dem Umwandlungszeichen weiter modifizieren:
.Zahl: Genauigkeit von Gleitkommazahlen festlegen: Gibt die maximale Anzahl von
Zeichen an, die nach dem Dezimalpunkt ausgegeben werden
Leerzeichen: Ausgabe eines Leerzeichens vor einer Zahl, falls das erste Zeichen kein
Vorzeichen ist
0: Zahlen bei der Umwandlungen bis zur Feldbreite mit führenden Nullen aufüllen
Anstelle einer Zahl kann auch das Zeichen * als Feldbreite angegeben werden. In die-
sem Fall wird die Feldbreite durch eine zusätzlich an dieser Stelle in der Argumentliste
angegebenen int-Variablen festgelegt:
printf("Der Wert von der Variable \"zahl\" ist: %*d", breite, zahl);
17
Soll einelong-Variante eines Integers ausgegeben werden, so muss vor das jeweilige Um-
wandlungszeichen ein l geschrieben werden, beispielsweise lu für long unsigned int
oder ld für long int; für long double wird L geschrieben.
Soll das %-Zeichen innerhalb einer Zeichenkette selbst ausgegeben werden, so muss an
dieser Stelle %% geschrieben werden.
Soll über mehrere Zeilen hinweg Text mittels printf() ausgegeben werden, so ist meist
es für eine bessere Lesbarkeit empfehlenswert, für jede neue Zeile eine eigene printf()-
Anweisung zu schreiben.
Sollen nur einfache Zeichenketten (ohne Formatierung und ohne Variablenwerte) ausgege-
ben werden, so kann anstelle von printf() auch die Funktion puts() aus der Standard-
Bibliothek stdio.h verwendet werden. Die in der Tabelle Escape-Sequenzen aufgelisteten
Sonderzeichen können auch bei puts() verwendet werden, es muss jedoch am Ende einer
Ausgabezeile kein \n angehängt werden; puts() gibt automatisch jeden String in einer
neuen Zeile aus.
Mittels putchar() können einzelne Zeichen auf dem Bildschirm ausgegeben werden. Diese
Funktion wird nicht nur von den anderen Ausgabefunktionen aufgerufen, sondern kann
auch verwendet werden, wenn beispielsweise eine Datei zeichenweise eingelesen und nach
3
Anwendung eines Filters wieder zeichenweise auf dem Bildschirm ausgegeben werden soll.
Die Funktion scanf() kann als flexible Funktion verwendet werden, um Daten direkt vom
Bildschirm beziehungsweise von der Tastatur einzulesen. Dabei wird bei scanf(), ebenso
wie bei printf(), ein Formatstring angegeben, der das Format der Eingabe festlegt. Die
Funktion weist dann die eingelesen Daten, die dem Format entsprechen, vom Bildschirm
ein und weist ihnen eine oder mehrere Programmvariablen zu. Im Formatstring können
die gleichen Umwandlungszeichen wie bei printf() verwendet werden.
Die Eingabe mittels scanf() erfolgt „gepuffert“, d.h. die mit der Tastatur eingegebenen
Zeichen werden zunächst in einem Zwischenspeicher („Puffer“) des Betriebsystems abge-
legt. Erst, wenn der Benutzer die Enter-Taste drückt, wird der eingegebene Text von
scanf() verarbeitet.
3 Streng genommen handelt es sich bei putchar() nicht um eine Funktion, sondern um ein Makro :
Letztlich wirdputchar(Zeichen) vom Präprozessor durch einen Funktionsaufruf von fputc(Zeichen,
stdin) ersetzt. Die Funktion fputc() wird im Abschnitt Dateien und Verzeichnisse näher beschrieben.
18
Bei der Zuweisung der eingelesenen Daten wird bei Benutzung der Funktion scanf() nicht
der jeweilige Variablenname, sondern stets die zugehörige Speicheradresse angegeben, an
welcher die Daten abgelegt werden sollen; diese kann leicht mittels des Adress-Operators
& bestimmt werden. Um also beispielsweise einen int-Wert vom Bildschirm einzulesen,
gibt man folgendes ein:
int n;
Sobald der Benutzer seine Eingabe mit Enter bestätigt, wird im obigen Beispiel die ein-
gegebene Zahl eingelesen und am Speicherplatz der Variablen n hinterlegt.
Zum Einlesen von Zeichenketten muss dem Variablennamen kein & vorangestellt werden,
da es sich bei einer Zeichenkette um ein Array handelt. Dieses wiederum entspricht einem
Zeiger auf den ersten Eintrag, und ab eben dieser Stelle soll die eingelesene Zeichenkette
abgelegt werden. Beim Einlesen von Daten in Felder muss allerdings beachtet werden, dass
der angegebene Zeiger bereits initialisiert wurde. Eine simple Methode, um dies sicherzu-
stellen, ist dass eine String-Variable nicht mit char *mystring;, sondern beispielsweise
mit char mystring[100]; definiert wird.
Mit einer einzelnen scanf()-Funktion können auch mehrere Werte gleichzeitig eingele-
sen werden, wenn mehrere Umwandlungszeichen im Formatstring und entsprechend viele
Speicheradressen als weitere Argumente angegeben werden. Beim Einlesen achtet scanf()
dabei so genannte Whitespace-Zeichen (Leerzeichen, Tabulator-Zeichen oder Neues-Zeile-
Zeichen), um die einzelnen Daten voneinander zu trennen. Soll der Benutzer beispielsweise
zwei beliebige Zahlen eingeben, so können diese mit einem einfachen Leerzeichen zwischen
ihnen, aber ebenso in zwei getrennten Zeilen eingegeben werden.
Da die Daten bei Verwendung von scanf() zunächst in einen Zwischenspeicher eingelesen
werden, können Probleme auftreten, wenn der Benutzer mehr durch Whitespace-Zeichen
getrennte Werte eingibt, als beim Aufruf der Funktion scanf() verarbeitet werden. Die
19
restlichen Werte verbleiben in diesem Fall im Zwischenspeicher und würden beim nächsten
Aufruf von scanf() noch vor der eigentlich erwarteten Eingabe verarbeitet werden. Eine
Abhilfe hierfür schafft die Funktion fflush(), die nach jedem Aufruf von scanf() auf-
gerufen werden sollte und ein Löschen aller noch im Zwischenspeicher abgelegten Werte
bewirkt.
Beim Einlesen von Zeichenketten mittels %s ist das wortweise Einlesen von scanf()
oftmals hinderlich, da in der mit %s verknüpften Variable nur Text bis zum ersten
Whitespace-Zeichen (Leerzeichen, Tabulator-Zeichen oder Neues-Zeile-Zeichen) gespei-
chert wird. Ganze Zeilen, die aus beliebig vielen Wörtern bestehen, sollten daher bevorzugt
mittels gets() oder fgets() eingelesen werden.
Um eine Textzeile auf einmal einzulesen, kann die Funktion gets() aus der Standard-
Bibliothek stdio.h verwendet werden. Diese Funktion liest eine Textzeile vom Bildschirm
ein und speichert sie in der angegebenen Variablen ein:
int mystring[81];
gets(mystring);
Ein Neues-Zeile-Zeichen \n am Ende des Eingabestrings wird von gets() automatisch ab-
geschnitten, das Zeichen \0 zum Beenden der Zeichenkette automatisch angefügt. Wichtig
ist allerdings bei der Verwendung von gets(), dass der angegebene String-Pointer auf ein
ausreichend großes Feld zeigt. Im obigen Beispiel darf die eingelesene Zeile somit nicht
mehr als 80 Zeichen haben, da auch noch Platz für das Zeichen \0 bleiben muss. Werden
die Feldgrenzen überschritten, kann dies ein unkontrolliertes Verhalten des Programms
4
oder gar einen Programmabsturz zur Folge haben.
Als bessere Alternative zu gets() kann die Funktion fgets() verwendet werden, welche
die Anzahl der maximal eingelesenen Zeichen beschränkt:
int mystring[81];
int n = 80;
fgets(mystring, n, stdin);
4 Im neuen C11-Standard wird gets() aufgrund seiner Fehleranfälligkeit nicht mehr als Standard
gelistet, den ein Compiler abdecken muss. Da die Funktion in sehr vielen Programmcodes vorkommt,
wird gcc wohl auch in absehbarer Zukunft diese Funktion unterstützen. In C11 wurde dafür die ähnli-
che Funktion gets_s() im optionalen Teil von stdio.h aufgenommen, die jedoch ebenfalls nicht jeder
Compiler zwingend unterstützen muss. Dies ist ein weiterer Grund, bevorzugt fgets() zu verwenden.
20
stdio.h verwendet werden. Diese Funktion liest eine Textzeile vom Bildschirm ein und
speichert sie in der angegebenen Variablen ein:
Wird das Zeichen nach einer Umlenkung des Standard-Eingangs (beispielsweise mittels
freopen() ) nicht von der Tastatur, sondern von einer Datei eingelesen, so wird so lange
jeweils ein einzelnes Zeichen zurückgegeben, bis ein Fehler auftritt oder die Funktion auf
das Ende des Datenstroms bzw. der Datei trifft; in diesem Fall wird EOF als Ergebnis
zurückgegeben.
. . . to be continued . . .
5 Streng genommen handelt es sich bei getchar() nicht um eine Funktion, sondern um ein Makro .
Letztlich wird getchar() vom Präprozessor durch einen Funktionsaufruf von fgetc(stdin) ersetzt. Die
Funktion fputc() wird im Abschnitt Dateien und Verzeichnisse näher beschrieben.
21
Operatoren und Funktionen
Operatoren
Mit einem Operator werden üblicherweise zwei Aussagen oder Variablen miteinander ver-
knüpft. Ist die Anwendung des Operators für die angegebenen Variablen erlaubt, so kann
dieser – je nach Operator – einen einzelnen Rückgabewert als Ergebnis liefern. Beispiels-
weise wird durch den Zuweisungsoperator = das Ergebnis des Ausdrucks auf der rechten
Seite in der links vom Istgleich-Zeichen stehende Variablen gespeichert.
In C existieren auch Operatoren, die nur auf eine einzelne Variable angewendet werden,
beispielsweise der Adressoperator &, der die Speicheradresse einer Variablen oder einer
Funktion als Ergebnis liefert, oder der Inhaltsoperator *, der den an einer Speicherstelle
abgelegten Wert ausgibt.
Mathematische Operatoren
Operator Beschreibung
+ Addition zweier Zahlen
- Subtraktion zweier Zahlen
* Multiplikation zweier Zahlen
/ Division zweier Zahlen (Division durch Null nicht erlaubt!)
% Ganzzahliger Rest bei der Division zweier Zahlen
Darüber hinaus existieren in C die beiden weiteren Operatoren ++ und --, die jeweils
auf eine einzige ganzzahlige Variable angewendet werden. Der Inkrement-Operator ++
erhöht den Wert der Variablen um 1, der Dekrement-Operator -- erniedrigt den Wert der
Variablen um 1. Beide Operatoren werden üblicherweise verwendet, um beispielsweise in
22
Schleifen den Wert einer Zählvariablen schrittweise um Eins zu erhöhen beziehungsweise
erniedrigen und dabei den Variablenwert mittels des Zuweisungsoperators = einer anderen
Variablen zuzuweisen:
Wie das obige Beispiel zeigt, ist es bei der Anwendung der Operatoren ++ und -- von
Bedeutung, ob der Operator vor oder nach der jeweiligen Variablen steht; im ersten Fall
wird die Variable erst inkrementiert beziehungsweise dekrementiert und anschließend zu-
gewiesen, im zweiten Fall ist es umgekehrt.
Die Operatoren ++ und -- haben für Zeiger auf Felder eine eigene Bedeutung: Sie erhöhen
den Wert des Zeigers nicht um 1, sondern um die Länge des Datentyps, der in dem
Array gespeichert ist, also beispielsweise um size(int) für ein Array mit int-Variablen.
Somit können in Schleifen auch Felder mit dem Inkrement- bzw. Dekrement-Operator
durchlaufen werden.
Zuweisungsoperatoren
Der wichtigste Zuweisungsoperator ist das Istgleich-Zeichen =: Es weist den Wert des
Ausdrucks, der rechts des Istgleich-Zeichens steht, der links stehenden Variablen zu.
Operator Beschreibung
= Wertzuweisung (von rechts nach links)
+= Erhöhung einer Variablen (um Term auf der rechten Seite)
-= Reduzierung einer Variablen
*= Vervielfachung einer Variablen
/= Teilung einer Variablen (durch Term auf der rechten Seite)
%= Ganzzahliger Rest bei Division (durch Term auf der rechten
Seite)
Vergleichsoperatoren
23
Operator Beschreibung
== Test auf Wertgleichheit
!= Test auf Ungleichheit
< Test, ob kleiner
<= Test, ob kleiner oder gleich
=> Test, ob größer oder gleich
> Test, ob größer
Logische Operatoren
Wie in der Aussagenlogik der Mathematik lassen sich auch in C mehrere Ausdrücke mit-
tels logischer Operatoren zu einem Gesamt-Ausdruck kombinieren. Die jeweiligen Symbole
für die logischen Verknüpfungen Und, Oder und Nicht sind in der folgenden Tabelle auf-
gelistet.
Operator Beschreibung
! Negation
&& Logisches Und
|| Logisches Oder
Das !-Zeichen als logisches Nicht bezieht sich auf den unmittelbar rechts stehenden Aus-
druck und kehrt dabei den Wahrheitswert des Ausdrucks um. Die anderen beiden Opera-
toren && und || verknüpfen den unmittelbar links und den unmittelbar rechts stehenden
Ausdruck zu einer Gesamt-Aussage. Eine Und-Verknüpfung ist genau dann wahr, wenn
beide Teil-Ausdrücke wahr sind, eine Oder-Verknüpfung ist wahr, wenn mindestens einer
der beiden Ausdrücke wahr ist.
Zur besseren Lesbarkeit sowie zur Vermeidung von Fehlern ist es empfehlenswert, die
durch logische Ausdrücke verknüpften Aussagen stets in runde Klammern zu setzen, also
beispielsweise (ausdruck_1 && ausdruck_2) zu schreiben.
Der Bedingungs-Operator
Der Bedingungs-Operator ist der einzige Operator in C, der drei Ausdrücke miteinander
verbindet. Er hat folgenden Aufbau:
Wenn der Bedingungs-Ausdruck wahr ist, also einen Wert ungleich Null als Ergebnis
liefert, so wird anweisung1 ausgeführt, ist der Bedingungs-Ausdruck falsch, so wird
anweisung2 ausgeführt. Beim Bedingungs-Operator handelt es sich somit um eine sehr
kurze Schreibform einer if-else-Anweisung . Er kann unter anderem bei der Zuweisung von
Werten eingesetzt werden, um beispielsweise einer neuen Variablen den größeren Wert
zweier anderer Variablen zuzuweisen:
24
// Die größere der beiden Variabeln var_1 und var_2 in my_var abspeichern:
my_var = ( var_1 > var_2 ) ? var_1 : var_2;
Der Cast-Operator
Mittels des so genannten Cast-Operators kann eine Variable mit einem bestimmten Da-
tentyp manuell in einen anderen Datentyp umgewandelt werden.
Während eine automatische Konvertierung in den jeweils nächst „größeren“ Datentyp ohne
Probleme möglich ist (beispielsweise float -> double oder double -> long double),
so ist eine Konvertierung in einen kleineren Datentyp oftmals mit Verlusten behaftet;
beispielsweise kann der float-Wert 3.14 nur gerundet als int-Wert dargestellt werden.
Eine solche derartige Umwandlung erfolgt in C dadurch, dass man bei der Zuweisung
vor den Ausdruck auf der rechten Seite den gewünschten Datentyp in runden Klammern
angibt:
int n;
float pi=3.14;
n = (int) pi;
Die runde Klammer mit dem darin enthaltenen Ziel-Datentyp wird hierbei als Cast-
Operator bezeichnet. Am häufigsten werden Casts wohl beim dynamischen Reservieren
von Speicherplatz verwendet: Hierbei wird zunächst ein unbestimmter Zeiger auf den
reservierten Speicherplatz erzeugt, der dann in einen Zeiger des gewünschten Typs umge-
wandelt wird.
Der sizeof-Operator
Der sizeof-Operator gibt die Größe des anschließend angegebenen Datentyps oder der
anschließend angegebenen Variablen an. Die Angabe eines Datentyp muss dabei (wie
beim cast -Operator) mit runden Klammern erfolgen; dies liegt daran, dass ansonsten
nicht zwischen der Bezeichnung eines Datentyps und einem Variablennamen unterschieden
werden kann. Beispielsweise würde alsosizeof (float);, je nach Rechner-Architektur,
den Wert 4 liefern. Wendet man den sizeof-Operator hingegen auf einen Variablennamen
an, so können runde Klammern um den Variablennamen wahlweise gesetzt oder auch
weggelassen werden.
Mit dem sizeof-Operator kann auch die Größe von Feldern oder Zusammengesetzten
Datentypen ermittelt werden; sie entspricht der Summe der Größen aller darin vorkom-
menden Elemente.
25
Das Ergebnis von sizeof hat als Datentyp size_t, was gleichbedeutend mit unsigned
int ist.
Der Komma-Operator
In C wird das Komma meist als Trennungszeichen für Funktionsargumente oder bei der
Deklaration von Variablen verwendet. Es kann allerdings auch als Operator genutzt wer-
den, wenn es zwischen zwei Ausdrücken steht. Hierbei wird zunächst der links vom Komma
stehende Ausdruck ausgewertet, anschließend der rechte. Als Ergebnis wird der Wert des
rechten Ausdrucks zurückgegeben.
In der folgenden Tabelle ist aufgelistet, welche Operatoren mit welcher Priorität ausge-
wertet werden (ebenso wie „Punkt vor Strich“ in der Mathematik). Operatoren mit einem
hohen Rang, die weiter oben in der Tabelle stehen, werden vor Operatoren mit einem
niedrigen Rang ausgewertet. Haben zwei Operatoren den gleichen Rang, so entscheidet
die so genannte Assoziativität, in welcher Reihenfolge ein Ausdruck auszuwerten ist:
Bei der Assoziativität „von links nach rechts“ wird der Ausdruck der Reihe nach
abgearbeitet, genau so, wie man den Code liest.
Bei der Assoziativität „von rechts nach links“ wird zunächst der Ausdruck auf der
rechten Seite des Operators ausgewertet, und erst anschließend der Operator auf
den sich ergebenden Ausdruck angewendet.
Enthält ein Ausdruck mehrere Operatoren mit gleicher Priorität, so werden die meisten
Operatoren von links nach rechts ausgewertet. Beispielsweise haben im Ausdruck 3 *
26
4 % 5 / 2 alle Operatoren die gleiche Priorität, sie werden gemäß ihrer Assoziativität
von links nach rechts ausgewertet, so dass der Ausdruck formal mit ((3 * 4) % 5) / 2
identisch ist; somit ist das Ergebnis gleich (12 % 5) / 2 = 2 / 2 = 1.
Zur besseren Lesbarkeit können Teil-Aussagen die durch einen Operator mit höherer Prio-
rität verbunden sind jederzeit, auch wenn es nicht notwendig ist, in runde Klammern
gesetzt werden, ohne den Wert der Aussage zu verändern.
Funktionen
Eine Funktion kann somit als „Unterprogramm“ angesehen werden, dem gegebenenfalls
ein oder auch mehrere Werte als so genannte „Argumente“ übergeben werden können und
das je nach Definition einen Wert als Ergebnis zurück gibt.
Der Rückgabe-Typ gibt den Datentyp an, den die Funktion zurück gibt, beispielsweise
int für ein ganzzahliges Ergebnis oder char * für eine Zeichenkette. Liefert die Funk-
tion keinen Wert zurück, wird void als Rückgabe-Typ geschrieben. Die Argumentenlis-
te der Funktion kann entweder leer sein oder eine beliebige Anzahl an zu übergeben-
den Argumenten beinhalten, wobei jedes Argument aus einem Argument-Typ und einem
Argument-Namen besteht. Beim Aufruf der Funktion müssen die Datentypen der überge-
benen Werte mit denen der bei der Deklaration angegebenen Argumentliste übereinstim-
1
men.
Soll eine Funktion einen Wert als Ergebnis zurückzugeben, so muss innerhalb der Funk-
tion das Schlüsselwort return gesetzt werden, gefolgt von einem C-Ausdruck. Wenn die
Funktion an einer return-Anweisung ankommt, wird der Ausdruck ausgewertet und das
Ergebnis an die aufrufende Stelle im Programm zurück gegeben. Zu beachten ist lediglich,
1 Streng genommen werden die Argumente bei der Definition als „formale Parameter“ bezeichnet, die
beim Aufruf übergebenen Werte hingegen werden „aktuelle Parameter“ oder schlicht Argumente genannt.
27
dass der von return zurück gelieferte Wert mit dem in der Funktionsdefinition angege-
benen Datentyp übereinstimmt, damit der Compiler keine Fehlermeldung ausgibt.
Nach der Definition der Funktion kann diese an beliebigen Stellen im Code genutzt wer-
den, sie kann also auch von anderen Funktionen aufgerufen werden. Um eine Funktion
allerdings bereits aufrufen zu können, wenn ihre Definition erst an einer späteren Stelle
der Datei erfolgt, muss am Dateianfang – wie bei Variablen – zunächst der Prototyp der
2
Funktion deklariert werden:
Bei C-Programmen, die nur aus einer einzigen Datei bestehen, werden die Funktions-
Prototypen üblicherweise gemeinsam mit der Deklaration von Variablen an den Anfang
der Datei geschrieben. Die konkrete Definition der Funktionen erfolgt dann üblicherweise
nach der Definition der Funktion main().
Um eine Funktion aufzurufen, wird der Name der Funktion in Kombination mit einer
Argumentliste in runden Klammern angegeben:
Beim Aufruf einer Funktion müssen die Anzahl der übergebenen Argumente und ihre
Datentypen mit der Funktions-Definition übereinstimmen.
C-Programme bestehen letztlich aus einer Vielzahl an Funktionen, die jeweils möglichst
eine einzige, klar definierte Teilaufgabe übernehmen; entsprechend sollte der Funktionsna-
me auf den Zweck der Funktion hinweisen. Eine Funktion Funktion sollte ebenfalls nicht
3
allzu umfangreich sein, nur wenige Funktionen bestehen aus mehr als 30 Zeilen Code.
Auf diese Weise lassen sich einerseits einzelne Code-Teile leichter wieder verwerten, an-
dererseits kann dadurch beim Suchen nach Fehlern der zu hinterfragende Code-Bereich
schneller eingegrenzt werden.
In C werden alle Argumente standardmäßig „by Value“ übergeben, das heißt, dass die
übergebenen Werte beim Funktionsaufruf kopiert werden, und innerhalb der Funktion mit
lokalen Kopien der Werte gearbeitet wird. Eine Funktion kann hierbei die Originalvariable
nicht verändern.
Wenn eine Funktion übergebene Variablen jedoch verändern soll, so müssen anstelle der
Variablenwerte die Adressen der jeweiligen Variablen übergeben werden. Eine derartige
Übergabe wird als „Call by Reference“ bezeichnet: Anstelle der Variablen wird ein Zeiger
auf die Variable als Argument übergeben. Ändert die Funktion den Wert der Speicher-
2 Deklarationen von Funktionen sind für das Compilieren des Programms unerlässlich, da für jeden
Funktionsaufruf geprüft wird, ob die Art und Anzahl der übergebenen Argumente korrekt ist.
3 Eine Funktion sollte maximal 100 Zeilen umfassen. Die Hauptfunktion main() sollte nur Unterfunk-
tionen aufrufen, um möglichst übersichtlich zu sein.
28
stelle, auf die der Pointer zeigt, so wird, wenn der Variablenwert erneut abgerufen wird,
die Veränderung auch im restlichen Programmteil festgestellt.
Komplexe Datentypen, beispielsweise Strukturen , werden fast nie direkt, sondern meistens
mittels eines Zeigers an eine Funktion übergeben; dadurch muss nicht die ganze Struktur,
sondern nur die Speicheradresse (ein unsigned int-Wert) kopiert werden. Wird ein Array
mittels eines Pointers an eine Funktion übergeben, so wird häufig dessen maximale Anzahl
an Elementen (ein int-Wert) als zusätzliches Argument an die Funktion übergeben.
Lokale Variablen
Innerhalb einer Funktion können, ebenso wie am Anfang einer Quellcode-Datei, neue Va-
riablen deklariert werden. Die in der Funktionsdefinition angegebenen Parameter-Namen
werden automatisch als neue Variablen deklariert. Beim Aufruf einer Funktion werden
den Parameter-Namen dann die entsprechenden Argumente als Werte zugewiesen.
Die so genannten „lokalen“ Variablen, die innerhalb einer Funktion definiert werden, sind
völlig unabhängig von den Variablen, die außerhalb der Funktion existieren. Variablen
des Programms können nur als Argumente an die Funktion übergeben werden, und Va-
riablenwerte der Funktion können nur über die return-Anweisung an das Programm
zurückgegeben werden.
Gibt es in einem Programm eine Variable var_1, so kann innerhalb einer Funktion also
dennoch eine gleichnamige Variable var_1 definiert werden. Die lokale Variable „über-
deckt“ in diesem Fall die Programmvariable, bis die Funktion abgearbeitet ist. Mit dem
Funktionsende erlischt eine lokale Variable wieder, es sei denn, sie wurde als static dekla-
riert. In diesem Fall hat die lokale Variable beim nächsten Funktionsaufruf den Wert, den
sie beim Beenden des vorhergehenden Funktionsaufrufs hatte.
Rekursion
Ruft eine Funktion in ihrem Anweisungsblock sich selbst auf, so spricht man von Rekursi-
on. Das wohl bekannteste Beispiel einer rekursiven Funktion ist die so genannte Fakultät
𝑥!:
𝑥! = 𝑥 · (𝑥 − 1) · (𝑥 − 2) · . . . · 2 · 1
Diese mathematische Funktion, die für positive ganzzahlige Werte definiert ist, kann mit-
tels einer C-Funktion für jeden beliebigen Wert 𝑥 rekursiv mittels 𝑥! = 𝑥·(𝑥−1)! berechnet
werden:
29
(Fortsetzung der vorherigen Seite)
{
x *= fakultaet(x-1);
return x;
}
}
Bei diesem Beispiel wird die Funktion fakultaet so lange von sich selbst aufgerufen, bis
das Argument x gleich 1 ist. Die zurückgegebenen Werte werden dabei jeweils mit Hilfe
des Zuweisungsoperators *= mit dem als Argument übergebenen Wert von x multipliziert,
das Ergebnis wird an die aufrufende Funktion zurückgegeben.
Rekursive Funktionen sollten, sofern möglich, vermieden werden. Der Grund liegt darin,
dass der Computer bei jedem neuen Funktionsaufruf unter anderem Variablenwerte kopie-
ren und neue Variablen initiieren muss, was zu einer Verlangsamung des Programms führt.
Die Fakultäts-Funktion kann beispielsweise auch geschickter mittels einer for -Schleife im-
plementiert werden, dank der insbesondere bereits berechnete Teilergebnisse nicht erneut
berechnet werden müssen:
return result;
}
30
Kontrollstrukturen
Mit Hilfe des Schlüsselworts if kann an einer beliebigen Stelle im Programm eine Bedin-
gung formuliert werden, so dass die Anweisung(en) im unmittelbar folgenden Code-Block
nur dann ausgeführt werden, sofern die Bedingung einen wahren Wert (ungleich Null)
ergibt.
if (Bedingung)
{
Anweisungen
}
In den runden Klammern können mittels der logischen Verknüpfungsoperatoren and be-
ziehungsweise or mehrere Teilbedingungen zu einer einzigen Bedingung zusammengefügt
werden. Bei einer einzeiligen Anweisung können die geschweiften Klammern weggelassen
werden. Liefert die Bedingung den Wert Null, so wird der Anweisungsblock übersprungen
und das Programm fortgesetzt.
Eine if-Anweisung kann um den Zusatz else erweitert werden. Diese Konstruktion wird
immer dann verwendet, wenn man zwischen genau zwei Alternativen auswählen möchte.
if (Bedingung)
{
Anweisungen
}
else
{
Anweisungen
}
31
Soll neben der if-Bedingung eine (oder mehrere) weitere Bedingung getestet werden,
so kann dies mittels des kombinierten Schlüsselworts else if geschehen. Die else if-
Anweisungen werden nur dann ausgeführt, wenn die if-Bedingung falsch und die elif-
Bedingung wahr ist.
if (Bedingung_1)
{
Anweisungen
}
else if (Bedingung_2)
{
Anweisungen
}
Allgemein können in einer if-Struktur mehrere else if-Bedingungen, aber nur ein else-
Block vorkommen.
switch – Fallunterscheidungen
switch (Ausdruck)
{
case const_1:
Anweisungen_1
case const_2:
Anweisungen_2
...
default:
Default-Anweisungen
Bei den Konstanten, mit denen der Wert von Ausdruck verglichen wird, muss es sich um
int- oder char-Werte handeln, die nicht mehrfach vergeben werden dürfen. Trifft kein
case zu, so werden die unter default angegebenen Anweisungen ausgeführt.
case zu, so werden die angegebenen Anweisungen ausgeführt, anschließend wird
Trifft ein
der Ausdruck mit den übrigen case-Konstanten verglichen. Möchte man dies vermeiden,
so kann man am Ende der case-Anweisungen die Anweisung break; einfügen, die einen
Abbruch der Fallunterscheidung an dieser Stelle zur Folge hat.
In C ist es auch möglich Anweisungen für mehrere case-Werte zu definieren. Die Syntax
dazu lautet:
32
switch (Ausdruck)
{
case const_1:
case const_2:
case const_3:
Anweisungen
...
}
In diesem Fall werden die bei case const_3 angegebenen Anweisungen auch aufgerufen,
wenn die Vergleiche case const_1 oder case const_2 zutreffen.
Wenn die Bedingung falsch ist, so wird die for-Schleife beendet, und das Programm
springt zur nächsten Anweisung außerhalb der Schleife.
Wenn die Bedingung wahr ist, so werden die im folgenden Block angegebenen An-
weisung(en) ausgeführt.
Soll eine Schleife vorzeitig beendet werden, so kann dies mittels des Schlüsselworts break
erreicht werden: Trifft das Programm auf diese Anweisung, so wird die Schleife unmit-
telbar beendet. [# ] Möchte man die Schleife nicht beenden, sondern nur den aktuellen
Schleifendurchgang überspringen, so kann man das Schlüsselwort continue verwenden.
33
Trifft das Programm auf diese Anweisung, so wird der aktuelle Schleifendurchgang been-
det, und das Programm fährt mit dem nächsten Schleifendurchgang fort.
Üblicherweise werden for-Schleifen verwendet, um mittels der Zählvariablen für eine be-
stimmte Anzahl von Durchläufen zu sorgen. Ist zu Beginn der Schleife nicht bekannt, wie
häufig der folgende Anweisungsblock durchlaufen werden soll, wird hingegen meist eine
while-Schleife eingesetzt.
while ( Bedingung )
{
Anweisungen
}
Eine while-Schleife führt einen Anweisungsblock aus, solange die angegebene Bedingung
wahr (nicht Null) ist. Das Programm wertet dabei zunächst den als Bedingung angege-
benen Ausdruck aus, und nur falls dieser einen von Null verschiedenen Wert liefert, wird
der Anweisungsblock ausgeführt. Ergibt der als Bedingung angegebene Ausdruck bereits
bei der ersten Auswertung den Wert Null, so wird die while-Schleife übersprungen, ohne
dass der Anweisungsblock ausgeführt wird.
Häufig werden while-Schleifen als Endlos-Schleifen verwendet, die einen (zunächst) wah-
ren Ausdruck als Bedingung verwenden. Unter einer bestimmten Voraussetzung wird dann
mittels einer if-Anweisung innerhalb des Schleifenblocks entweder der Bedingungsaus-
druck auf den Wert Null gesetzt oder die Schleife mittels break beendet.
Soll eine gewöhnliche while-Schleife, unabhängig von ihrer Bedingung, mindestens einmal
ausgeführt werden, so wird in selteneren Fällen eine do-while-Schleife eingesetzt. Eines
solche Schleife ist folgendermaßen aufgebaut:
do
{
Anweisungen
} while ( Bedingung )
Da es stets möglich ist, eine do-while-Schleife auch mittels einer while-Schleife zu schrei-
ben, werden letztere wegen ihrer besseren Lesbarkeit meist bevorzugt.
34
Funktionen für Felder und
Zeichenketten
Soll die Größe eines Feldes erst zur Laufzeit bestimmt werden, so ermöglichen es die Funk-
tionen malloc() und calloc() aus der Standard-Bibliothek stdlib.h , nach Möglichkeit
ein entsprechend großes Stück an freiem Speicherplatz („memory“) zu finden und für das
Feld zu reservieren („allocate“).
Der Speicher eines Programms setzt sich allgemein zusammen aus einem Teil namens
„Stack“, der für statische Variablen reserviert ist, und einem dynamischen Teil namens
„Heap“, auf den mittels malloc() oder calloc() zugegriffen werden kann.
Bei der Verwendung dieser Funktionen kann valgrind als „Debugger“ für dynamischen
Speicherplatz eingesetzt werden.
Als Ergebnis gibt die Funktion malloc() einen Zeiger auf die nutzbare Speicheradresse
zurück, oder NULL, falls keine Speicherreservierung möglich war. Bei jeder neuen Spei-
cherreservierung sollte der Rückgabewert geprüft und gegebenenfalls eine Fehlermeldung
ausgegeben werden. Im erfolgreichen Fall hat der zurück gegebene Zeiger den Typ void
* und wird üblicherweise vom Programmierer mittels des cast-Operators in einen Zeiger
vom gewünschten Typ umgewandelt.
An die Funktion malloc() wird allgemein die zu reservierende Speichergröße in Bytes als
Argument übergeben; für beispielsweise 50 Werte vom Datentyp int ist damit auch das
Fünfzigfache der Größe dieses Datentyps nötig. Der Rückgabewert von malloc(), nämlich
void *, wird mit Hilfe des Casts (int *) in einen Zeiger auf int umgewandelt.
Wird der Speicher nicht mehr benötigt, so muss er manuell mittels free() wieder frei-
gegeben werden. Als Argument wird dabei der Name des variablen Speichers angegeben,
35
also beispielsweise free(numbers). In C gibt es keinen „Garbage Collector“, der nicht
mehr benötigte Speicherbereiche automatisch wieder freigibt; es ist also Aufgabe des Pro-
grammierers dafür zu sorgen, dass Speicher nach dem Gebrauch wieder freigegeben wird
und somit kein Speicherleck entsteht.
Neben der Funktion malloc() gibt es in der Standardbibliothek stdlib.h eine weitere
Funktion zur dynamischen Speicherreservierung namens calloc(). Beim Aufruf dieser
Funktion wird als erstes Argument die Anzahl der benötigten Variablen, als zweites Ar-
gument die Größe einer einzelnen Variablen in Bytes angegeben. Bei einer erfolgreichen
Reservierung wird, wie beimalloc(), ein void *-Zeiger auf den reservierten Speicher zu-
rückgegeben, andernfalls NULL. Der Unterschied zwischen malloc() und calloc() liegt
darin, dass calloc() alle Bits im Speicherbereich auf 0 setzt und dadurch sicherstellt,
dass zuvor mit free() freigegebene Daten zufällig weiterverarbeitet werden.
Auch bei der Verwendung von calloc() muss Speicher, der nicht mehr benötigt wird,
manuell mittels free() wieder freigegeben werden.
Mit der Funktion realloc() kann ein mit malloc() oder calloc() reservierter Speicher-
bereich nachträglich in seiner Größe verändert werden.
Als erstes Argument gibt man bei realloc() einen Zeiger auf einen bereits existierenden
dynamischen Speicherbereich an, als zweites die gewünschte neue Größe des Speicherbe-
reichs. Kann der angeforderte Speicher nicht an der bisherigen Adresse angelegt werden,
weil dort kein ausreichend großer zusammenhängender Speicherbereich mehr frei ist, dann
verschiebt realloc() den vorhandenen Speicherbereich an eine andere Stelle im Speicher,
an der noch genügend Speicher frei ist.
Als Ergebnis gibt die Funktion realloc() ebenfalls einen void *-Zeiger auf den reser-
vierten Speicherbereich zurück, wenn die Speicherreservierung erfolgreich war, andernfalls
NULL. Übergibt man an realloc() einen NULL-Pointer als Adresse, so ist realloc() mit
malloc() identisch und gibt einen Zeiger auf einen neu erstellten dynamischen Speicher-
bereich zurück.
In C kann man den Inhalt zweier Felder nicht direkt vergleichen, es kann hierfür also nicht
array_1 == array_2 geschrieben werden. Bei diesem Test würden lediglich, da der Name
eines Feldes auf das erste im Feld gespeicherte Element verweist, die Speicheradressen
zweier Variablen verglichen werden, jedoch nicht deren Inhalt.
36
Für einen inhaltlichen Vergleich müssen alle Einzelelemente der Felder miteinander vergli-
chen werden. Dies kann automatisch mit der Funktion memcmp() aus der Standardbiblio-
thek string.h durchgeführt werden. Bei identischen Feldern wird der Wert 0 als Ergebnis
zurückgegeben. Stößt die Funktion im ersten Feld auf einen Wert, der größer ist als im zu
vergleichenden Feld, so wird ein positiver Wert >0 zurückgegeben, im umgekehrten Fall
ein negativer Wert < 0.
Handelt es sich bei den Feldern um Zeichenketten, so sollte anstelle von memcmp() bevor-
zugt die Funktion strcmp() verwendet werden. Diese prüft ebenfalls Zeichen für Zeichen,
ob die beiden angegebenen Zeichenketten übereinstimmen. Anders als bei memcmp() wird
jedoch das Überprüfen der Feldinhalte beendet, sobald das String-Ende-Zeichen \0 er-
reicht wird. Mögliche Inhalte der Felder hinter diesem Zeichen werden somit nicht vergli-
chen.
Der Funktion strcpy() wird als erstes Argument der Name des Zielstrings, als zweites
Argument eine dorthin zu kopierende Zeichenkette übergeben:
char target_string[50];
puts(target_string);
// Ergebnis: "Hallo Welt!"
Der Zielstring wird von strcpy() automatisch mit dem Zeichenkette-Ende-Zeichen '\0'
abgeschlossen. Wichtig ist zu beachten, dass strcpy() nicht prüft, ob der Zielstring aus-
reichend groß ist; reicht der Platz dort nicht aus, werden die Bytes einer anschließend im
Speicher abgelegten Variablen überschrieben, was unvorhersehbare Fehler mit sich brin-
gen kann. Als Programmierer muss man somit entweder selbst darauf achten, dass nicht
Zielstring ausreichend groß ist, oder die Funktion strncpy() verwenden, welcher als drittes
Argument die Anzahl 𝑛 der zu kopierenden Zeichen übergeben wird.
Der Funktion strcat() wird als erstes Argument der Name des Zielstrings, als zweites
Argument eine dort anzufügenden Zeichenkette übergeben:
char target_string[50];
puts(target_string);
// Ergebnis: "Hallo Welt! Auf Wiedersehen!"
37
strcat() überschreibt automatisch das Zeichenkette-Ende-Zeichen '\0' des Zielstring
mit dem ersten Zeichen des anzuhängenden Strings und schließt nach dem Anfügen der
restlichen Zeichen den Zielstring wiederum mit '\0' ab.
Ebenso wie bei strcpy() muss auch bei Verwendung von strcat() auf einen ausreichend
strncat() verwen-
grossen Zielstring geachtet werden. Als Alternativ kann die Funktion
det werden, der als drittes Argument eine Anzahl 𝑛 an anzuhängenden Zeichen übergeben
wird.
38
Zusammengesetzte Datentypen
Mit dem Schlüsselwort typedef kann ein neuer Name für einen beliebigen Datentyp ver-
geben werden. Die Syntax lautet dabei wie folgt:
Beispielsweise kann mittels typedef int integer ein „neuer“ Datentyp namens integer
erzeugt werden. Dieser kann anschließend wie gewohnt bei Deklarationen von Varia-
blen verwendet werden, beispielsweise wird durch integer num_1; eine neue Variable
als Integer-Wert deklariert.
Die Verwendung von typedef ist insbesondere bei der Definition von zusammengesetzten
Datentypen hilfreich.
enum – Aufzählungen
Bei der Deklaration eines enum-Typs werden die einzelnen Elemente der Aufzählung durch
Komma-Zeichen getrennt aufgelistet. Sie bekommen dabei, sofern nicht explizit andere
Werte angegeben werden, automatisch die Nummern 0, 1, 2, ... zugewiesen; bei ex-
pliziten Wertzuweisungen wird der Wert für jedes folgende Element um 1 erhöht.
typedef enum
{
const1, const2, const3, ...
} enum_name;
# Beispiel:
typedef enum
{
MONTAG = 1, DIENSTAG, MITTWOCH, DONNERSTAG, FREITAG, SAMSTAG, SONNTAG
} wochentag;
39
Allgemein müssen die Elemente eines enum-Typs unterschiedliche Werte besitzen. Oftmals
werden die aufgelisteten Elemente zudem in Großbuchstaben geschrieben, um hervorzu-
heben, dass es sich auch bei ihnen um (ganzzahlige) Konstante handelt.
Nach der obigen Deklaration ist beispielsweise wochentag als neuer Datentyp verfügbar,
der stets durch einen „benannten“ int-Wert repräsentiert wird:
// Funktionen definieren:
Es können somit nach der Deklaration des enum-Datentyps auch dessen Elemente als
numerische Konstante im C-Code verwendet werden.
struct – Strukturen
typedef struct
{
// ... Deklaration der Komponenten ...
} struct_name;
// Beispiel:
typedef struct
{
char name[50];
int laenge;
int breite;
(continues on next page)
40
(Fortsetzung der vorherigen Seite)
int hoehe;
} gegenstand;
Nach der Deklaration einer Struktur kann diese als neuer Datentyp verwendet werden.
Die einzelnen Komponenten werden nicht dabei durchnummeriert, sondern lassen sich
mittels des Strukturzugriff-Operators . über bei der Definition vergebene Schlüsselwörter
ansprechen:
// Struktur-Objekt definieren:
gegenstand tisch =
{
"Schreibtisch", 140, 60, 75
};
Handelt es sich bei einer Struktur-Komponente um einen Zeiger, beispielsweise eine Zei-
chenkette, so muss der Inhalts-Operator * vor den Strukturnamen geschrieben werden.
Im obigen Beispiel würde man also nicht tisch.*name schreiben (was beim Compilieren
einen Fehler verursachen würde), sondern *tisch.name, da der Strukturzugriff-Operator
. eine höhere Priorität besitzt. Zuerst wird also der Strukturzugriff ausgewertet, wobei
sich eine Variable vom Typ char * ergibt; anschließend kann diese mit dem Inhaltsope-
rator dereferenziert werden. Bei *strukturname.komponente kann somit der Punkt wie
ein Teil des Veriablennamens gelesen werden.
Eine Struktur wird selten direkt als Argument an eine Funktion übergeben, da hierbei der
gesamte Strukturinhalt kopiert werden müsste. Stattdessen wird üblicherweise ein Zeiger
auf die Struktur an die Funktion übergeben.
Hat man beispielsweise eine Struktur mystruct mit den Komponenten int a und int
b und ein bereits existierendes mystruct-Objekt x_1, so kann man mittels mystruct
* x_1_pointer = &x_1; einen Zeiger auf die Struktur definieren. Mittels eines solchen
Pointers kann man auf folgende Weise auf die Komponenten der Struktur zugreifen:
// Struktur deklarieren:
typedef struct
{
(continues on next page)
41
(Fortsetzung der vorherigen Seite)
int a;
int b;
} mystruct;
// Struktur-Objekt erzeugen:
mystruct x = {3, 5};
(*xpointer).a == xpointer->a
// Ergebnis: TRUE
Mit dem Pfeil-Operator -> kann also in gleicher Weise auf die Komponenten eines
Struktur-Pointers zugegriffen werden wie mit . auf die Komponennten der Struktur selbst.
union – Alternativen
Mittels des Schlüsselworts union lässt sich ein zusammengesetzter Datentyp definieren, bei
dem sich die bei der Deklaration angegebenen Elemente einen gemeinsamen Speicherplatz
teilen: Es kann dabei zu jedem Zeitpunkt nur eine der angegebenen Komponenten aktiv
sein. Der Speicherplatzbedarf einer Union entspricht somit dem Speicherplatzbedarf der
größten angegebenen Komponente. Die Deklaration einer union erfolgt nach folgendem
Schema:
typedef union
{
// ... Deklaration der Komponenten ...
} union_name;
// Beispiel:
typedef union
{
char text[20];
int ganzzahl;
float kommazahl;
} cell_value;
42
Nach der Deklaration einer Union kann diese als neuer Datentyp verwendet werden. Der
Zugriff auf die einzelnen möglichen Elemente, die eine Union-Variable beinhaltet, erfolgt
wie bei Strukturen, mit dem .-Operator:
// Union-Variablen deklarieren:
printf("%s\n", cell_1.text)
Im Falle eines Zeigers auf eine union-Variable kann, ebenso wie bei Zeigern auf Strukturen ,
mit dem Pfeil-Operator -> auf die einzelnen Komponenten zugegriffen werden.
Unabhängig davon, welche Komponente aktuell in einer union-Variable mit einem Wert
versehen ist, können stets alle möglichen Komponenten der Union abgefragt werden; dabei
wird der aktuell gespeicherte Wert mittels eines automatischen Casts in den jeweiligen
Datentyp umgewandelt. Da diese Umwandlung zu unerwarteten Ergebnissen führen kann,
kann es hilfreich sein, für die einzelnen Datentypen der Union-Komponenten symbolische
Konstanten zu vergeben. Fasst man dann sowohl den aktuellen Typ der Union-Variablen
sowie die Union-Variable zu einer Struktur zusammen, so lässt sich bei komplexeren Da-
tentypen nicht nur Speicherplatz sparen, es kann auch mittels einer case -Anweisung gezielt
Code in Abhängigkeit vom aktuellen Wert aufgerufen werden:
typedef enum
{
STRING=0, INTEGER=1, FLOAT=2
} u_type;
typedef struct
{
u_type type;
cell_value value;
} cell_content;
cell_content my_cell;
my_cell.type = FLOAT;
my_cell.value = 3.14;
switch (my_cell.type)
{
case STRING:
printf("In dieser Zelle ist die Zeichenkette %s gespeichert.", *my_cell.
˓→value);
case INT:
(continues on next page)
43
(Fortsetzung der vorherigen Seite)
case FLOAT:
printf("In dieser Zelle ist die float-Zahl %f gespeichert.", my_cell.
˓→value);
Auf diese Weise könnte in einem „echten“ Programm die Ausgaben eines Wertes aufgrund
nicht nur seines Datentyps, sondern beispielsweise auch aufgrund von Darstellungsoptio-
nen (Anzahl an Kommastellen, Prozentwert, usw.) angepasst werden.
44
Dateien und Verzeichnisse
Jede Ein- und Ausgabe von Daten erfolgt in C über Datenkanäle („Files“). Beim Pro-
grammstart werden automatisch die Standard-Files stdin, stdout und stderr geöff-
net und mit dem Bildschirm verknüpft. Somit muss in Programmen nur die Standard-
Bibliothek stdio.h eingebunden werden, damit Daten beispielsweise mittels printf() auf
dem Bildschirm ausgegeben oder mittels scanf() von der Tastatur eingelesen werden kön-
1
nen.
# include <stdio.h>
FILE *fp;
Um eine Datei zu öffnen, wird die Funktion fopen() verwendet. Als erstes Argument wird
hierbei der Pfadname der zu öffnenden Datei übergeben, als zweites ein Zeichen, das den
Zugriffsmodus auf die Datei angibt:
"w": Textdatei zum Schreiben neu erzeugen (gegebenenfalls alten Inhalt wegwerfen)
"a": Text anfügen; Datei zum Schreiben am Dateiende öffnen oder erzeugen
"a+": Datei neu erzeugen oder zum Ändern öffnen und Text anfügen (Schreiben am
Ende)
1 Programme, deren einzige Aufgabe darin besteht, Daten vom Bildschirm einzulesen, zu verarbeiten,
und wieder auf dem Bildschirm auszugeben, werden bisweilen auch als „Filter“ bezeichnet. Derartige
Programme können unter Linux mittels des Pipe-Zeichens verbunden werden, beispielsweise kann so in
einer Shell programm_1 | programm_2 | programm_3 eingegeben werden.
45
Als Ergebnis gibt fopen() einen File-Pointer auf die Datei zurück, oder NULL, falls beim
Öffnen ein Fehler aufgetreten ist.
fp = fopen("/path/to/myfile","r");
if (fp == NULL)
fprintf(stderr,"Datei konnte nicht geoeffnet werden.\n");
Wird der Zugriff auf eine Datei nicht mehr benötigt, so sollte sie mittels fclose() wie-
der geschlossen werden. Hierbei muss als Argument der zur geöffneten Datei gehörende
File-Pointer angegeben werden, also beispielsweise fclose(fp). Bei einem Schreibzugriff
ist das Schließen einer Datei mittels fclose() Pflicht, da hierdurch unter anderem die
Modifikationszeit der Datei aktualisiert wird.
In C gibt es keine eigenständige Funktion, um die Existenz einer Datei zu prüfen. Man
kann allerdings die Funktion fopen() auch zu diesem Zweck nutzen:
fp = fopen(filename, "r");
if (fp == NULL)
{
result = 0;
}
else
{
result = 1;
fclose(fp);
}
return result;
}
Hierbei wurde als Zugriffsmodus "r" gewählt, da die Datei nicht verändert werden soll und
die Methode auch mit schreibgeschützten Dateien funktionieren soll. Die Rückgabewerte
wurden im obigen Beispiel so gewählt, damit sie an einer anderen Stelle im Code innerhalb
einer if-Abfrage genutzt werden können.
46
Daten in eine Datei schreiben
Wie bereits im Abschnitt Ausgabe und Eingabe beschrieben wurde, gibt es in C mehrere
Möglichkeiten, um Daten von der Tastatur beziehungsweise vom Bildschirm („stdin“) ein-
zulesen. Ebenso gibt es in C mehrere Möglichkeiten, um Inhalte aus Dateien einzulesen
oder dorthin zu schreiben. Die einzelnen Funktionen sind dabei den bereits behandelten
Funktionen sehr ähnlich.
Mit fprintf() können normale Zeichenketten, Sonderzeichen und Werte von Variablen
mittels Platzhaltern in formatierter Weise in eine Datei geschrieben werden. Die Syntax
entspricht dabei derjenigen von printf() , wobei als erstes Argument der Name eines File-
Pointers angegeben werden muss:
FILE *fp;
// Datei öffnen:
fp = fopen(filename, "w");
// Daten schreiben:
fprintf(fp, "Teststring!\n");
// Datei schließen:
fclose(fp);
Sollen bei der Verwendung von fprintf() mehrere Zeilen auf einmal geschrieben wer-
den, so müssen diese mittels des Neue-Zeile-Zeichens \n getrennt werden. Am Ende des
Schreibvorgangs muss die Datei wieder mittels fclose() geschlossen werden, damit die
Modifikationszeit angepasst wird.
Mit fputs() können normale Zeichenketten in eine Datei geschrieben werden. Sonderzei-
chen in den Zeichenketten sind erlaubt, ein Ersetzen von Platzhaltern durch Werte von
Variablen hingegen nicht.
Auch die Funktionen zum Einlesen von Daten aus einer Datei ähneln denen im Abschnitt
Ausgabe und Eingabe beschriebenen Funktionen zum Einlesen von Daten vom Bildschirm.
47
fgetc() – Daten zeichenweise einlesen
Die Funktion fgetc() liest ein einzelnes Zeichen aus einer Datei ein und gibt es als int-
Wert zurück. Vor Verwendung von fgetc() muss wiederum zunächst ein File-Pointer
mittels fopen() bereitgestellt werden:
Die Funktion fgetc() wird häufig in Verbindung mit einer while-Schleife eingesetzt,
wobei als Abbruchfunktion die Funktion feof() genutzt wird: Diese prüft, ob der an-
gegebene File-Pointer auf das Ende der Datei zeigt und gibt in diesem Fall einen Wert
ungleich Null zurück.
48
Interaktionen mit dem Betriebsystem
Mittels der Funktion system() aus der Standard-Bibliothek stdlib.h können Program-
me des Betriebsystems, beispielsweise Shell-Programme, aus einem C-Programm heraus
aufgerufen werden. Als Argument wird der Funktion dabei eine Zeichenkette übergeben,
die den Namen des aufzurufenden Programms mitsamt aller Argumente und Optionen
enthält, beispielsweise "ls -lh":
# include <stdlib.h>
// ...
system("ls -lh");
// ...
Wenn das externe Programm beendet ist, wird das C-Programm weiter ausgeführt.
Mittels der Funktionexit() kann ein Programm in geordneter Weise beendet werden. Als
Argument wird beim Aufruf der Funktion ein int-Wert angegeben, der als Rückgabewert
an das System dient. Der Wert 0 gilt dabei für ein normales Programmende, der Wert 1
wird üblicherweise im Falle eines Fehlers zurück gegeben.
Trifft das Programm auf eine exit()-Funktion, so werden automatisch alle noch nicht
geschriebenen Ausgabe-Streams geschrieben, alle offenen Dateien geschlossen sowie alle
mittels tmpfile() angelegten temporären Dateien gelöscht.
Zusätzlich können im vorangehenden Teil des Codes, häufig in der Funktion main(),
mittels atexit() Pointer auf Funktionen angegeben werden, die bei einem Aufruf von
exit() ausgeführt werden, bevor das Programm beendet wird. Das Besondere dabei ist,
dass die Funktionen von hinten nach vorne durchlaufen werden, d.h. die zuletzt angege-
bene atexit()-Funktion wird als erstes aufgerufen, die als erstes angegebene atexit()-
Funktion zuletzt.
49
Modularisierung
Jedes Modul besitzt eine Schnittstelle mit „globalen“ Variablen und Funktionen und Va-
riablen des Moduls, auf die auch von einer anderen Datei aus zugegriffen werden kann.
Die anderen Funktionen und Variablen sind „lokal“, sie haben also keine direkten Auswir-
kungen auf andere Module.
Jedes Modul sollte möglichst wenig Funktionen oder Variablen in seiner Schnittstelle
definieren, damit Änderungen an lokalen Funktionen keine Änderungen in anderen Code-
Teilen zur Folge haben. Beispielsweise betrifft die Änderung einer globalen Funktionen
bezüglich ihres Namens oder ihrer Anzahl an Argumenten alle Code-Teile, in denen die
Funktion benutzt wird.
Die Schnittstelle eines Moduls (einer .c-Datei) wird üblicherweise in einer gleichnamigen
Headerdatei (einer .h-Datei) definiert. In einer solchen Datei werden Variablen und Funk-
tionen lediglich deklariert, eine Header-Datei enthält somit keinen ausführbaren Code.
Die Verwendung von Header-Dateien ist dann sinnvoll, wenn eine Variable oder eine Funk-
tion von mehreren Dateien aus benutzt werden soll.
50
Präprozessor, Compiler und Linker
Ein klassischer C-Compiler besteht aus drei Teilen: Einem Präprozessor, dem eigentlichen
Compiler, und einem Linker:
Der Präprozessor bereitet einerseits den Quellcode vor (entfernt beispielsweise Kom-
mentare und Leerzeilen); andererseits kann er mittels der im nächsten Abschnitt
näher beschriebenen Präprozessor-Anweisungen Ersetzungen im Quellcode vorneh-
men.
Der Compiler analysiert den Quellcode auf lexikalische oder syntaktische Fehler,
nimmt gegebenenfalls Optimierungen vor und wandelt schließlich die aufbereiteten
Quellcode-Dateien in binäre Objekt-Dateien (Endung: .o) um.
Der Linker ergänzt die Objekt-Dateien um verwendete Bibliotheken und setzt die
einzelnen Komponenten zu einem ausführbaren Gesamt-Programm zusammen.
Präprozessor-Anweisungen
Der Präprozessor lässt sich im Wesentlichen durch zwei Anweisungen steuern, die jeweils
durch ein Hash-Symbol # zu Beginn der Anweisung gekennzeichnet sind und ohne einen
Strichpunkt abgeschlossen werden:
51
#define – Definition von Konstanten und Makros
Mittels #define können Konstanten oder Makros definiert werden. Bei der Definition
einer Konstanten wird zunächst der zu ersetzende Name anschließend der zugehörige
Wert angegeben:
Eine Großschreibung der Konstantennamen ist nicht zwingend nötig, ist in der Praxis
jedoch zum Standard geworden, um Konstanten- von Variablennamen unterscheiden zu
können. Nicht verwendet werden dürfen allerdings folgende Konstanten, die im Präpro-
zessor bereits vordefiniert sind:
Eine Festlegung mittels #define bleibt allgemein bis zum Ende der Quelldatei beste-
hen. Soll eine erneute Definition einer Konstanten NAME erfolgen, so muss die bestehende
Definition erst mittels #undef NAME rückgängig gemacht werden.
Bei der Definition eines Makros mittels #define wird zunächst der Name des Makros
angegeben. In runden Klammern stehen dann, wie bei der Definition einer Funktion , die
1
Argumente, die das Makro beim Aufruf erwartet. Unmittelbar anschließend wird der
Code angegeben, den das Makro ausführen soll.
Bei der Definition von Makros muss beachtet werden, dass der Präprozessor die Ersetzun-
gen nicht wie ein Taschenrechner oder Interpreter, sondern wie ein klassischer Text-Editor
vornimmt. Steht im Quellcode beispielsweise die Zeile result = QUADRAT(n), so wird die-
se durch den Präprozessor gemäß dem obigen Makro zu result = ((n)*(n)) erweitert.
In diesem Fall erscheinen die Klammern als unnötig. Steht allerdings im Quellcode die
Zeile result = QUADRAT(n+1), so wird diese mit Hilfe der Klammern zu ((n+1)*(n+1))
erweitert. Ohne die zusätzlichen Klammern in der Makro-Definition würde der Ausdruck
zu n+1*n+1 erweitert werden, was ein falsches Ergebnis liefern würde.
Innerhalb von Makro-Definitionen kann ein spezieller Operatoren verwendet werden: Der
Operator # kann auf einen Argumentnamen angewendet werden und setzt den Namen der
2
konkret angegebenen Variablen in doppelte Anführungszeichen:
1 Zu beachten ist, dass bei der Definition eines Makros kein Leerzeichen zwischen dem Makronamen
und der öffnenden runden Klammer der Argumentenliste vorkommen darf. Der Präprozessor würde an-
sonsten den Makronamen als Namen einer Konstanten interpretieren und den geamten Rest der Zeile als
Wert dieser Konstanten interpretieren.
2 Zudem können mit dem zweiten möglichen Makro-Operator ## die Namen von zwei oder mehreren
übergebenen Argumenten zu einer neuen Bezeichnung verbunden werden. Dieser Operator wird allerdings
nur sehr selten eingesetzt.
52
# define QUADRAT(x) print("Der Quadrat-Wert von %s ist %i.\n", #x, (x)*(x))
// Datei: makro-beispiel-1
// Compilieren: gcc -o makro-beispiel-1 makro-beispiel-1
// Aufruf: ./makro-beispiel-1
# include <stdio.h>
void main()
{
int num=11;
QUADRAT(num);
}
Ist eine #define-Anweisung zu lange für eine einzelne Code-Zeile, so kann die Anweisung
an einer Whitespace-Stelle mittels \ unterbrochen und in der nächsten Zeile fortgesetzt
werden. Eventuelle Einrückungen (Leerzeichen, Tabulatoren) werden dabei vom Präpro-
zessor automatisch entfernt.
Mittels #if, #ifdef oder #ifndef können Teile einer Datei zur „bedingten Compilierung“
vorgemerkt werden. Ein solcher Code-Teil wird nur dann vom Compiler berücksichtigt,
wenn die angegebene Bedingung erfüllt ist.
Beispielsweise kann auf diese verhindert werden, dass Header-Dateien oder Quellcode-
Bibliotheken mehrfach geladen werden. Beispielsweise kann man in einer Header-Datei
input.h gleich zu Beginn prüfen, ob eine Konstante INPUT_H definiert ist. Falls nicht, so
kann wird der folgende Code berücksichtigt, wobei darin auch die Konstante INPUT_H mit
dem Wert 1 definiert wird:
// Datei: input.h
# ifndef INPUT_H
# define INPUT_H = 1
53
(Fortsetzung der vorherigen Seite)
//#endif
Die Variable INPUT_H ist nur beim ersten Versuch, die Datei mittels #include zu impor-
tieren, nicht definiert. Ein mehrfaches Importieren wird somit verhindert. Ebenso kann
beispielsweise mittels #ifdef DEBUG ein Code-Teil nur zu Testzwecken eingefügt werden
(der durch eine Zeile #define DEBUG 1 am Beginn der Datei aktiviert wird). Es kann auch
ein Teil eines Codes nur in Abhängigkeit von einer Versionsnummer ausgeführt werden,
indem beispielsweise #if VERSION < 1.0 geprüft wird.
Compiler-Optionen
Der Standard-C-Compiler kann mit einer Vielzahl an Optionen aufgerufen werden, mit
denen der Compilier-Ablauf gesteuert werden kann. Möchte man beispielsweise lediglich
überprüfen, welche Ersetzungen vom Präprozessor vorgenommen wurden, aber den Quell-
code nicht kompilieren, so kann die Option -E verwendet werden:
In diesem Beispiel wird die Ausgabe, die der Präprozessor bei der Verarbeitung der Datei
mycode.c erzeugt, in die Datei mycode.i geschrieben. Mit der Option -o („output“) wird
bei gcc allgemein der Name der Ausgabedatei angegeben.
Jeder Compiler bringt mehrere so genannte Bibliotheken („Libraries“) mit sich. Diese
enthalten fertige Funktionen in bereits compilierter Form, die von anderen C-Programmen
genutzt werden können. Der Linker sucht die benötigten Funktionen aus den Bibliotheken
heraus und fügt sie dem zu compilierenden Programm hinzu.
54
Laufzeiten von Algorithmen
Bisweilen können für die selbe Aufgabe mehrere Lösungen gefunden werden, die sich
teilweise jedoch erheblich in ihrer Effizienz unterscheiden. Bei der Effizienz-Analyse eines
Algorithmus, also eines „Rezepts“ zur Lösung eines Problems, ist es insbesondere von
Interesse, wie sich die Laufzeit in Abhängigkeit von der Anzahl 𝑛 der zu bearbeitenden
Daten ändert.
Mittels eines Benchmarks wird ein Programm oder Algorithmus mit einem möglichst
typischen Satz an Daten aufgerufen und dabei die benötigte Zeit gemessen.
Ein Werkzeug, das hierfür unter Linux genutzt werden kann, ist gprof . Dieses Pro-
gramm misst nicht nur die Laufzeit eines Programms und der im Programmverlauf
aufgerufenen Funktionen, sondern zählt auch, wie häufig die einzelnen Funktionen
aufgerufen wurden. Damit erhält man einen guten Überblick, welche Funktionen für
eine weitergehende Analyse „wichtig“ sind.
Mit einer Laufzeit-Analyse kann anhand der Struktur des Quellcodes, beispielsweise
anhand der Anzahl an Schleifendurchläufen, Lese- oder Schreibvorgängen, die Grö-
ßenordnung der Laufzeit eines Algorithmus in Abhängigkeit von der Anzahl 𝑛 an
zu bearbeiteten Daten abgeschätzt werden.
Die „Big-O“-Notation
Wie lange die Ausführung eines Algorithmus tatsächlich benötigt, hängt nicht zuletzt von
der Rechenleistung des Computers ab, auf dem der Code ausgeführt wird; Benchmarks
müssen daher auf einem einheitlichen System durchgeführt und unter Angabe der Rech-
nerleistung (CPU, RAM, usw.) angegeben werden. Allgemeinere Vergleiche sind hingegen
möglich, welche die Laufzeit 𝑡 eines Algorithmus allgemein als Funktion 𝑡(𝑛) des Datenum-
fangs 𝑛 ausgedrückt wird. Wird beispielsweise im Verlauf eines Programms eine Funktion
mit einer konstanten Laufzeit 𝑐 insgesamt 𝑛 mal aufgerufen, so ergibt sich dadurch eine
Laufzeit von 𝑡(𝑛) = 𝑐 · 𝑛.
Beim Zählen von Laufzeiten wird üblicherweise die vereinfachende Vereinbarung, dass die
folgenden Prozess-Schritte zur Ausführung jeweils eine Zeiteinheit benötigen:
Jede Wertzuweisung
Jeder Wertevergleich
55
Jede Iteration einer Schleifenvariablen
Finden beispielsweise beim Durchlaufen einer Schleife 𝑛 Iterationen statt, so nimmt die
𝑛 zu. Man sagt, dass in die-
Laufzeit für einen Aufruf einer solchen Schleife linear mit
sem Fall die Laufzeit proportional zur Größenordnung von 𝑛 ist, und schreibt hierfür in
Kurzform 𝒪(𝑛). Wird hingegen eine verschachtelte Liste mit 𝑛 Teillisten durchlaufen, die
2
wiederum 𝑛 Einträge haben, so sind insgesamt 𝑛 · 𝑛 = 𝑛 Iterationen nötig. Entsprechend
ergibt sich für einen Aufruf einer derartigen Schleife eine Laufzeit in der Größenordnung
2
von 𝒪(𝑛 ).
56
Dynamische Datenstrukturen
Verkettete Listen
Eine verkettete Liste besteht aus einer Vielzahl von Elementen, bei der jedes Element einen
Zeiger seinen Nachfolger enthält; bei einer doppelt verketteten Liste besitzt jedes Element
zusätzlich einen Zeiger auf seinen Vorgänger. Eine derartige Struktur bietet eine einfache
Möglichkeit zusätzliche Elemente in die Liste aufzunehmen oder Elemente wieder aus der
Liste zu entfernen. Verkettete Listen können somit dynamisch wachsen oder schrumpfen.
Bei einer einfach verketteten Liste hat jedes Element einen Zeiger, der auf seinen un-
mittelbaren Nachfolger zeigt; der Zeiger des letzten Elements zeigt auf NULL. Verkettete
Listen haben stets einen Zeiger, der auf das erste Element („Head“) zeigt, und oftmals
auch einen Zeiger auf das letzte Element der Liste („Tail“).
Die einzelnen Elemente einer verketteten Liste haben den Datentyp struct. Da sie aller-
dings bereits bei ihrer Deklaration einen Pointer auf ein weiteres Element mit gleichem
Datentyp angeben, muss der Name der Struktur dem Compiler schon im Vorfeld bekannt
sein. Man kann dies auf folgendem Weg erreichen:
struct element_prototype
{
// Eigentlicher Inhalt (hier: int):
int value;
57
an zwar noch nicht die Größe der Struktur, aber zumindest ihren Namen sowie ihren
Datentyp, was für die Erstellung eines Pointers bereits genügt. Anschließend kann der
Strukturtyp mittels typedef umbenannt werden, um im Folgenden anstelle von struct
element_prototype einfacher element_type für die Bezeichnung des Datentyps schrei-
ben zu können.
int init_list()
{
// Dynamischen Speicherplatz für Elemente reservieren:
e0 = (element_type *) malloc(sizeof *e0);
e1 = (element_type *) malloc(sizeof *e1);
// Fehlerkontrolle:
if (e0 == NULL) || (e1 == NULL)
return 1;
// Referenzen anpassen:
e0->next = e1;
e1->next = NULL;
// Normaler Rückgabewert:
return 0;
}
Möchte man ein weiteres Element in die verkettete Liste aufnehmen, so muss einerseits der
Speicherplatz für das zusätzliche Element reserviert werden. Andererseits muss der Zeiger
des Elements, hinter dem das neue Element eingefügt werden soll, aktualisiert werden:
// Referenzen anpassen:
(continues on next page)
58
(Fortsetzung der vorherigen Seite)
e_new->next = e->next;
e->next = e_new;
Der Zeiger des neuen Elements e_new muss nach dem Einfügen auf die Stelle verweisen,
auf die der Zeiger des Vorgänger-Elements e bislang gezeigt hat. Dafür muss der Zeiger
des Vorgänger-Elements e nach dem Einfügen auf das neue Element e_new verweisen.
Um das Nachfolger-Element eines bestimmten Element aus einer einfach verketteten Liste
zu entfernen, muss einerseits der Zeiger des dieses Elements auf das übernächste Element
umgelenkt werden; andererseits muss der dynamisch reservierte Speicherplatz für das zu
entfernende Element wieder freigegeben werden:
// Referenzen anpassen:
e->next = e->next->next;
// Speicherplatz freigeben:
free(e->next);
// Normaler Rückgabewert:
return 0;
}
Soll nicht das Nachfolger-Element eines angegebenen Elements, sondern dieses selbst ge-
löscht werden, so muss zuerst der Vorgänger des Elements ermittelt werden. Dies kann
man erreichen, indem man vom Head-Element aus die Zeigerwerte der einzelnen Elemente
mit dem Zeigerwert des angegebenen Elements vergleicht:
59
(Fortsetzung der vorherigen Seite)
Das Löschen eines Elements kann mit Hilfe der obigen Funktion beispielsweise folgender-
maßen implementiert werden:
// Speicherplatz freigeben:
(continues on next page)
60
(Fortsetzung der vorherigen Seite)
free(e);
// Normaler Rückgabewert:
return 0;
}
Offensichtlich ist das Löschen eines bestimmten Elements bei einfach verketteten Listen
mit einigem Rechenaufwand verbunden, da im ungünstigsten Fall die gesamte Liste durch-
laufen werden muss. Das Suchen nach einem bestimmten Wert in der Liste funktioniert
auf ähnliche Weise:
return e_pos;
}
Auch beim Suchen eines bestimmten Werts muss die verkettete Liste im ungünstigsten
Fall komplett durchlaufen werden. Um eine verlinkte Liste wieder zu löschen, werden
nacheinander die einzelnen Elemente mittels free() wieder freigegeben:
void delete_list()
{
// Temporäre Zeiger definieren:
element_type *e_pos;
element_type *e_tmp;
61
(Fortsetzung der vorherigen Seite)
e_pos = tmp;
}
Enthält jedes jedes Element einer verketteten Liste nicht nur einen Zeiger auf seinen
Nachfolger, sondern ebenso einen Zeiger auf seinen Vorgänger, so spricht man von einer
doppelt verketteten Liste. Die Deklaration eines Listenelements sowie die Erzeugung einer
Liste ist im Wesentlichen mit der einer einfach verketteten Liste identisch:
struct element_prototype
{
// Eigentlicher Inhalt (hier: int):
int value;
int init_list()
{
// Dynamischen Speicherplatz für Elemente reservieren:
e0 = (element_type *) malloc(sizeof *e0);
e1 = (element_type *) malloc(sizeof *e1);
// Fehlerkontrolle:
if (e0 == NULL) || (e1 == NULL)
return 1;
// Referenzen anpassen:
e0->prev = NULL;
e0->next = e1;
e1->prev = e0;
e1->next = NULL;
// Normaler Rückgabewert:
return 0;
}
Ein Vorteil von doppelt verketteten Listen liegt darin, dass man sowohl vor- als auch
rückwärts in der Liste nach Inhalten suchen kann. Ebenso kann man – im Vergleich zu
62
einfach verketteten Listen – ein bestimmtes Listenelement mit weniger Aufwand an einer
bestimmten Stelle einfügen oder löschen.
63
Hilfreiche Werkzeuge
Im folgenden werden kurz einige Programme beschrieben, die bei der Entwicklung von
C-Programmen hilfreich sein können. Bei den meisten Linux-Systemen (Debian, Ubuntu,
Linux Mint) lassen sich diese unmittelbar mittels apt installieren:
aptitude install astyle cdecl cflow doxygen gdb graphviz splint valgrind
astyle – Code-Beautifier
Das Programm astyle kann verwendet werden, um C-Code in eine einheitliche Form zu
bringen. Die Syntax dafür lautet:
Als Option kann mittels -A1 bis -A12 ein gewünschter Code-Style angegeben werden. Eine
Übersicht über die möglichen Style-Varianten ist in der Dokumentation des Programms
zu finden. In den Beispielen dieses Tutorials wird der Codestyle „Allman“ (Option -A1)
verwendet.
for i in *.c ; \
do astyle -A1 < $i > $(basename $i).tmp && mv $(basename $i).tmp $i; \
done
cdecl – Deklarations-Übersetzer
Das Programm cdecl kann verwendet werden, um komplexe Deklarationen, auf die
man beispielsweise beim Lesen von Quellcode stoßen kann, in einfachem Englisch zu be-
schreiben. Umgekehrt kann man durch die Angabe eines Strukturtyps in entsprechender
Englisch-Syntax die entsprechende C-Deklaration zu erhalten.
64
Üblicherweise wird cdecl mittels der Option -i im interaktiven Modus gestartet:
cdecl -i
Anschließend kann durch Eingabe von explain und einer beliebigen C-Deklaration
diese in einfachem Englisch angezeigt werden, beispielsweise liefert explain int
myfunc(int, char *); als Ergebnis: declare myfunc as function (int, pointer
to char) returning int. Umgekehrt kann declare in Verbindung mit einer solchen
Englisch-Syntax aufgerufen werden, um C-Code zu erhalten, beispielsweise liefert declare
mylist as array 20 of pointer to char das Ergebnis char *mylist[20].
Mit help kann Hilfe angezeigt werden, mit quit wird cdecl wieder beendet.
cflow – Funktionsstruktur-Viewer
Mittels cflow kann angezeigt werden, welche Funktionen schrittweise von einer Quelldatei
aufgerufen werden, und falls es sich um externe Funktionen handelt, in welcher Datei und
an welcher Stelle sich diese befinden.
cflow quelldatei.c
doxygen – Dokumentations-Generator
Mittels doxygen kann eine Dokumentation eines C-Projekts erzeugt werden, ohne dass
innerhalb der Code-Dateien irgendeine Markup-Sprache verwendet werden muss. Dafür
werden beispielsweise Übersichts- und Strukturdiagramme automatisch erzeugt, sofern
auch das Programm graphviz installiert ist.
Möchte man die von doxygen erstellte Dokumentation in einem eigenen Ordner abgelegt
haben, so sollte man zudem beispielsweise mittels mkdir doxygen im Projektverzeichnis
einen neuen Unterordner erstellen.
Als Optionen zur Erzeugung von C-Code-Übersichten halte ich für sinnvoll:
65
Option in der Doxyfile Beschreibung
PROJECT_NAME = Toolname Namen des Projekts angeben
OUTPUT_DIRECTORY = ./ Verzeichnis für HTML- und LaTeX-Dokumentation
doxygen festlegen
OUTPUT_LANGUAGE = German Sprache auswählen
EXTRACT_ALL = YES Alle Informationen des Quellcodes verwenden
SOURCE_BROWSER = YES Immer Links zu den entsprechenden Funktionen und
Dateien erzeugen
HAVE_DOT = YES Nützliche Aufrufdiagramme mittels graphviz erzeu-
gen
CALL_GRAPH = YES Funktionsaufrufe als Graphen erzeugen
CALLER_GRAPH = YES Als Graphen darstellen, von wo aus die einzelnen
Funktionen aufgerufen werden
FILE_PATTERNS = *.c *.h Alle .c und .h-Dateien berücksichtigen
Nach dem Anpassen der Doxyfile muss im Projektpfad nur doxygen ohne weiteren
Argumente aufgerufen werden, um die Dokumentation zu erstellen und im doxygen-
Unterverzeichnis abzulegen. Anschließend kann man die Indexdatei ./doxygen/html/
index.html mit Firefox oder einem anderen Webbrowser öffnen.
gdb – Debugger
Fehler übersieht man gerne. Bei der Fehlersuche in C-Code kann der Debugger gdb einge-
setzt werden, um das Verhalten eines Programms schrittweise zu überprüfen sowie Teile
des Quellcodes, die als Fehlerquelle in Frage kommen, näher eingrenzen zu können.
Um den gdb-Debugger nutzen zu können, muss das zu untersuchende Programm mit der
Option -g oder -ggdb compiliert werden, um für den Debugger relevante Informationen
zu generieren.
# Compilieren zu Debug-Zwecken:
gcc -ggdb -o myprogram myprogram.c
Die Option -ggdb erzeugt ausführlichere, auf gdb zugeschnittene Informationen und dürfte
in den meisten Fällen zu bevorzugen sein.
1
Anschließend kann das compilierte Programm mit gdb geladen werden:
gdb myprogram
Der Debugger wird dabei im interaktiven Modus gestartet. Um das angegebene Programm
myprogram zu starten, kann run (oder kurz: r) eingegeben werden; dabei können dem
Programm mittels run arg_1 arg_2 ... beliebig viele Argumente übergeben werden, als
ob der Aufruf aus der Shell heraus erfolgen würde. Das Programm kann dabei abstürzen,
1 Alternativ kann man gdb auch ohne Angabe eines Programmnamens starten und dieses im interak-
tiven Modus mittels file myprogram öffnen.
66
wobei eine entsprechende Fehlermeldung und die für den Absturz relevante Code-Zeile
angezeigt wird, oder (anscheinend) fehlerfrei durchlaufen.
Wird ein Fehler angezeigt, beispielsweise eine „Arithmetic exception“, wenn versucht wird
durch Null zu dividieren, so kann mittels print varname der Wert der angegebenen Va-
riable zu diesem Zeitpunkt ausgegeben werden.
Um sich den Programmablauf im Detail anzuschauen, können mit break (oder kurz: b)
so genannte „Breakpoints“ gesetzt werden. An diesen Stellen stoppt das Programm, wenn
es mit run gestartet wird, automatisch. Die Breakpoints werden von gdb automatisch
ausgewählt, beispielsweise werden sie vor Funktionsaufrufen gesetzt, um mittels print
die Werte der übergebenen Variablen prüfen zu können.
Mittels eines Aufrufs von break num kann auch eine weiterer Breakpoint unmittelbar vor
der Code-Zeile num manuell gesetzt werden. Ist in dem Programm eine Funktion myfunc()
definiert, so werden mittels break myfunc Breakpoints vor jeder Stelle gesetzt, an denen
die angegebene Funktion aufgerufen wird.
Ist man nach dem Setzen der Breakpoints und dem Aufruf von run am ersten Breakpoint
angekommen, so kann man mittels continue (oder kurz: c) bis zum nächsten Breakpoint
mit der Ausführung des Programms fortfahren. Alternativ kann next (oder kurz: n) be-
ziehungsweise step (oder kurz: s) eingegeben werden, um nur die unmittelbar nächste
Quellcode-Zeile auszuführen. Der Unterschied zwischen next und step liegt darin, dass
next die nächste Code-Zeile als eine einzige Anweisung ausführt, während step im Falle
eines Funktionsaufrufs den Code der Funktion zeilenweise durchläuft.
Drückt man in gdb die Enter-Taste, so wird die unmittelbar vorher gegebene Anwei-
sung erneut ausgeführt. Dies kann insbesondere in Verbindung mit next oder step viel
Schreibarbeit ersparen.. ;-)
Ebenso wie Breakpoints die Ausführung des Programms an bestimmten Code-Zeilen ge-
zielt unterbrechen, kann man mit so genannten „Watchpoints“ das Programm jedes mal
automatisch stoppen, wenn sich der Wert einer angegebenen Variablen ändert. Befindet
sich beispielsweise im Programm eine Variable myvar, so kann mittels watch myvar ein
zu dieser Variablen passender Watchpoint definiert werden.
Backtraces
Wird eine Funktion aufgerufen, so erzeugt gdb einen so genannten „frame“, in dem der
Funktionsname und die übergebenen Argumente festgehalten werden, beispielsweise exis-
main, der gegebenenfalls die beim Aufruf übergebe-
tiert immer ein Frame für die Funktion
nen Argumente argv sowie ihre Anzahl argc beinhaltet. Mit jedem Aufruf einer weiteren
Funktion wird, solange deren Ausführung dauert, ein weiterer Frame angelegt.
67
Tritt ein Fehler auf, so genügt es unter Umständen, wenn die Zeile des Codes angezeigt
wird, die den Fehler verursacht hat. Mitunter ist es jedoch auch gut zu wissen, wie das
Programm zur fehlerhaften Zeile gelangt ist. Dies kann in gdb mittels einer Eingabe von
backtrace (oder kurz: bt) geprüft werden. Ein solcher Backtrace gibt in umgekehrter
Reihenfolge an, durch welche Funktionsaufruf das Programm an die Fehlerstelle gelangt
ist. Somit können beim nächsten Durchlauf von gdb gezielt Breakpoints gesetzt bzw.
Variablenwerte überprüft werden.
Möchte man gdb mit einer graphischen Oberfläche nutzen, so können optional die Pakete
ddd und xterm via apt installiert werden:
sudo aptitude install ddd xterm
gprof – Profiler
Der Profiler gprof kann verwendet werden, um zu untersuchen, wie häufig die einzel-
nen Funktionen eines Programms aufgerufen werden und wie viel Zeit sie dabei für ihre
Ausführung benötigen. Dies soll kurz anhand des folgenden Beispielprogramms gezeigt
werden:
// Datei: gprof_test.c
# include <stdio.h>
void new_func1(void);
void func_1(void)
{
int i;
printf("\n Now: Inside func_1 \n");
return;
}
68
(Fortsetzung der vorherigen Seite)
int i;
printf("\n Now: Inside func_2 \n");
return;
}
int main(void)
{
int i;
printf("\n Now: Inside main()\n");
func_1();
func_2();
return 0;
}
Um gprof nutzen zu können, muss als erstes das zu untersuchende Programm zunächst
mit der Option -pg compiliert werden, um für den Profiler relevante Informationen zu
generieren; als zweites muss das Programm einmal aufgerufen werden, um die für gprof
relevante Datei gmon.out zu erzeugen:
./gprof_test
Anschließend kann der Profiler mittels gprof ./gprof_test aufgerufen werden. Ruft
man gprof allerdings ohne zusätzliche Optionen auf, so wird eine ziemlich lange Ausgabe
auf dem Bildschirm erzeugt, wobei die meisten beschreibenden Kommentare in den Regel
nicht benötigt werden; gprof sollte daher mit der Option -b aufgerufen werden, um die
ausführlichen Kommentare auszublenden. Verwendet man zusätzlich die Option -p, so
wird die Ausgabe auf ein Minimum reduziert:
gprof -b -p ./gprof_test
# Ergebnis:
# Flat profile:
#
# Each sample counts as 0.01 seconds.
# % cumulative self self total
# time seconds seconds calls s/call s/call name
# 67.28 4.89 4.89 1 4.89 4.89 func_2
(continues on next page)
69
(Fortsetzung der vorherigen Seite)
Bei dieser Ausgabe sieht man auf den ersten Blick, welche Funktion im Laufe des Pro-
gramms am meisten Zeit benötigt beziehungsweise wie viel Zeit sie je Aufruf braucht.
Wird anstelle der Option -p die Option -P verwendet, so wird neben dieser Aufgliede-
rung angezeigt, an welcher Stelle eine Funktion aufgerufen wird:
gprof -b -P ./gprof_test
# Ergebnis:
# Call graph
#
#
# granularity: each sample hit covers 2 byte(s) for 0.14% of 7.36 seconds
#
# index % time self children called name
# <spontaneous>
# [1] 100.0 0.02 7.34 main [1]
# 4.89 0.00 1/1 func_2 [2]
# 2.45 0.00 1/1 func_1 [3]
# -----------------------------------------------
# 4.89 0.00 1/1 main [1]
# [2] 66.4 4.89 0.00 1 func_2 [2]
# -----------------------------------------------
# 2.45 0.00 1/1 main [1]
# [3] 33.3 2.45 0.00 1 func_1 [3]
# -----------------------------------------------
#
#
# Index by function name
#
# [3] func_1 [2] func_2 [1] main
Unmittelbar im Anschluss an die Optionen-p oder -P kann auch ein Funktionsname aus-
gegeben werden, um die Ausgabe von gprof auf die angegebene Funktion zu beschränken;
zudem kann mittels der Option -a die Aufgabe auf alle nicht als statisch (privat) dekla-
rierten Funktionen beschränkt werden.
make – Compilier-Hilfe
Das Shell-Programm make ist ein äußert praktisches Hilfsmittel beim Compilieren von C-
Quellcode zu fertigen Programmen. Die grundlegende Funktionsweise von make ist unter
Linux und Open Source: Makefiles beschrieben.
70
splint – Syntax Checker
Wendet man den Syntax-Prüfer lint oder die verbesserte Variante splint auf eine C-
Datei an, so reklamiert dieser nicht nur Fehler, sondern auch Stilmängel.
splint quelldatei.c
Bisweilen kann splint auch Code-Zeilen beanstanden, in denen man bewusst gegen ein-
zelne „Regeln“ verstoßen hat. In diesem Fall muss man das Ergebnis der Syntax-Prüfung
selbst interpretieren und/oder gegebenenfalls Warnungen mittels der jeweiligen Option
abschalten (diese wird bei der Ausgabe von splint gleich als Möglichkeit mit angege-
ben).
time – Timer
Der Timer time kann verwendet werden, um die Laufzeit eines Programms zu messen. Dies
ist nützlich, um verschiedene Algorithmen hinsichtlich ihrer Effizienz zu vergleichen. Als
Beispiel soll die Laufzeit zweier Algorithmen verglichen werden, welche alle Primzahlen
zwischen 1 und 10000 bestimmen sollen:
// Datei: prim1.c
// (Ein nicht sehr effizienter Algorithmus)
# include <stdio.h>
# define N 10000
int main()
{
int num, factor;
int is_prim;
{
if (num % factor == 0) // Test, ob num den Faktor␣
˓→factor enthält
{
if(num == factor) // num ist genau dann Primzahl,␣
˓→wenn sie
else
(continues on next page)
71
(Fortsetzung der vorherigen Seite)
printf("\n");
return 0;
}
Übersetzt man dieses Programm mittels gcc -o prim1 prim1.c und ruft anschließend
time ./prim1 auf, so erhält man (neben der Auflistung der Primzahlen) folgende Aus-
gabe:
# Ergebnis:
# ...
# real 0m0.179s
# user 0m0.175s
# sys 0m0.003s
Die Ausgabe besagt, dass das Programm zur Ausführung insgesamt 0, 179 s benötigt hat,
wobei die zur Ausführung von Benutzer- und Systemanweisungen benötigten Zeiten ge-
trennt aufgelistet werden. Beide zusammen ergeben (von Rundungsfehlern abgesehen) die
Gesamtzeit.
2
Im Vergleich dazu soll ein zweiter, wesentlich effizienterer Algorithmus getestet werden:
// Datei: prim2.c
// (Ein wesentlich effizienterer Algorithmus)
// ("Das Sieb des Eratosthenes")
# include <stdio.h>
# define N 10000
int main()
{
int num = 1;
int factor_1, factor_2;
int numbers[N];
(continues on next page)
2 Eratosthenes entwickelte ein einfaches Schema zur Bestimmung aller Primzahlen kleiner als 100:
Zunächst schrieb er die Zahlen in zehn Zeilen mit je zehn Zahlen auf ein Blatt. Anschließend strich er
zunächst alle geraden Zahlen (jede jede zweite) durch, dann alle durch 3 teilbaren Zahlen (also jede
dritte), dann alle durch 5 teilbaren Zahlen (die 4 war ja bereits durchgestrichen), usw. Alle verbleibenden
Zahlen mussten Primzahlen sein, denn sie waren nicht als Vielfache einer anderen Zahl darstellbar.
72
(Fortsetzung der vorherigen Seite)
# Ergebnis:
# ...
# real 0m0.003s
# user 0m0.002s
# sys 0m0.001s
Der zweite Algorithmus gibt das gleiche Ergebnis aus, benötigt dafür aber nur rund 1/60
der Zeit. Dieser Unterschied im Rechenaufwand wird noch wesentlich deutlicher, wenn
man in den Quelldateien den Wert N statt auf 10 000 auf100 000 setzt: In diesem Fall
ist der erste Algorithmus auf meinem Rechner erst nach 14.397 s (!!) fertig, während der
zweite nur 0, 032 s benötigt.
valgrind - Speicher-Testprogramm
Das Programm valgrind prüft bei einem ausführbaren Programm, wieviel Speicher dy-
namisch reserviert bzw. wieder freigegeben wurde.
valgrind programmname
73
Man kann valgrind auch auf Standard-Programme anwenden, beispielsweise wird mittels
valgrind ps -ax der Speicherbedarf des Programms ps analysiert, wenn dieses mit der
Option -ax aufgerufen wird.
74
Die C-Standardbibliothek
void assert(logical_expression);
Diese Funktion kann – wie eine if-Bedingung – an beliebigen Stellen im Code ein-
gesetzt werden. Ergibt der angegebene logische Ausdruck allerdings keinen wahren
(von Null verschiedenen) Wert, so bricht assert() das Programm ab und gibt auf
dem stderr-Kanal als Fehlermeldung aus, welche Zeile beziehungsweise notwendige
Bedingung den Absturz verursacht hat.
double sin(double x)
Gibt den Sinus-Wert eines in Radiant angegebenen 𝑥-Werts an.
double cos(double x)
Gibt den Cosinus-Wert eines in Radiant angegebenen 𝑥-Werts an.
double tan(double x)
Gibt den Tangens-Wert eines in Radiant angegebenen 𝑥-Werts an.
double asin(double x)
Gibt den Arcus-Sinus-Wert eines 𝑥-Werts an, wobei 𝑥 ∈ [−1; +1] gelten
muss.
double acos(double x)
Gibt den Arcus-Cosinus-Wert eines 𝑥-Werts an, wobei 𝑥 ∈ [−1; +1] gelten
muss.
double atan(double x)
Gibt den Arcus-Tangens-Wert eines 𝑥-Werts an.
75
double sinh(double x)
Gibt den Sinus-Hyperbolicus-Wert eines 𝑥-Werts an.
double cosh(double x)
Gibt den Cosinus-Hyperbolicus-Wert eines 𝑥-Werts an.
double tanh(double x)
Gibt den Tangens-Hyperbolicus-Wert eines 𝑥-Werts an.
double exp(double x)
Gibt den Wert der Exponentialfunktion 𝑒𝑥 eines 𝑥-Werts an.
double log(double x)
Gibt den Wert der natürlichen Logarithmusfunktion ln (𝑥) an, wobei 𝑥 > 0
gelten muss.
double log10(double x)
Gibt den Wert des Logarithmus zur Basis 10 an, wobei 𝑥 > 0 gelten muss.
double pow(double x)
Gibt den Wert von 𝑥𝑦 an. Ein Argumentfehler liegt vor, wenn 𝑥=0 und
𝑦<0 gilt, oder wenn 𝑥<0 und 𝑦 nicht ganzzahlig ist.
double sqrt(double x)
Gibt den Wert der Quadratwurzel eines 𝑥-Werts an, wobei 𝑥 ≤ 0.
double ceil(double x)
Gibt den kleinsten ganzzahligen Wert als double an, der nicht kleiner als
𝑥 ist.
double floor(double x)
Gibt den größten ganzzahligen Wert als double an, der nicht größer als 𝑥
ist.
double fabs(double x)
Gibt den Absolutwert |𝑥| eines 𝑥-Werts an.
double ldexp(double x, n)
Gibt den Wert des Ausdrucks 𝑥 · 2𝑛 an.
76
Zerlegt 𝑥 in einen ganzzahligen Teil und einen Rest, die beide das gleiche
Vorzeichen wie 𝑥 besitzen. Der ganzzahlige Teil wird bei *ip abgelegt, der
Rest wird als Ergebnis zurückgegeben.
string.h – Zeichenkettenfunktionen
In der Definitionsdatei <string.h> gibt es zwei Gruppen von Funktionen für Felder und
Zeichenketten. Die Namen der ersten Gruppe von Funktionen beginnen mit mem; diese
sind allgemein zur Manipulation von Feldern vorgesehen. Die Namen der zweiten Gruppe
von Funktionen beginnen mit str und ist speziell für Zeichenketten gedacht, die mit dem
Zeichen \0' abgeschlossen sind.
Wichtig: Bei der Verwendung der mem- und str-Funktionen muss der Programmierer
darauf achten, dass sich die Speicherplätze der zu kopierenden oder zu vergleichenden
Zeicherketten nicht überlappen, da das Verhalten der Funktionen sonst nicht definiert ist.
77
mem-Funktionen
Die mem-Funktionen sind zur Manipulation von Speicherbereichen gedacht. Sie behandeln
den Wert \0 wie jeden anderen Wert, daher muss immer eine Bereichslänge angegeben
werden.
str-Funktionen
78
Fügt höchstens 𝑛 Zeichen der Zeichenkette str_2 hinten an die Zeichen-
kette str_1 an und schließt str_1 mit \0 ab. Gibt str_1 als Ergebnis
zurück.
79
Gibt einen Zeiger auf erstes Vorkommen von der Zeichenkette str_2 in-
nerhalb der Zeichenkette str_1 als Ergebnis zurück, oder NULL, falls diese
nicht vorkommt.
char * strerror(size_t n)
Gibt einen Zeiger auf diejenige Zeichenkette als Ergebnis zurück, die dem
Fehler mit der Nummer 𝑛 zugewiesen ist.
Die Datei stdio.h definiert Typen und Funktionen zum Umgang mit Datenströmen
(„Streams“). Ein Stream ist Quelle oder Ziel von Daten und wird mit einer Datei oder
einem angeschlossenen Gerät verknüpft.
Unter Windows muss zwischen Streams für binäre und für Textdateien unterschieden
werden, unter Linux nicht. Ein Textstream ist eine Folge von Zeilen, die jeweils kein oder
mehrere Zeichen enthalten und jeweils mit '\n' abgeschlossen sind.
Ein Stream wird mittels der Funktion open() mit einer Datei oder einem Gerät verbun-
den; die Verbindung wird mittels der Funktion close() wieder aufgehoben. Öffnet man
eine Datei, so erhält man einen Zeiger auf ein Objekt vom Typ FILE, in welchem alle
Information hinterlegt sind, die zur Kontrolle des Stream nötig sind.
Wenn die Ausführung eines Programms beginnt, sind die drei Standard-Streams stdin,
stdout und stderr bereits automatisch geöffnet.
Dateioperationen
Die folgenden Funktionen beschäftigen sich mit Datei-Operationen. Der Typ size_t ist
der vorzeichenlose, ganzzahlige Resultattyp des sizeof -Operators.
FILE *fopen(const char *filename, const char *mode)
Öffnet die angegebene Datei; gibt als Ergebnis einen Datenstrom zurück,
oder NULL falls das Öffnen fehlschlägt.
80
– "a": Text anfügen; Datei zum Schreiben am Dateiende öffnen oder
erzeugen
– "a+": Datei neu erzeugen oder zum Ändern öffnen und Text anfügen
(Schreiben am Ende)
Mit freopen() ändert man normalerweise die Dateien, die mit stdin,
stdout oder stderr verknüpft sind.
int fflush(FILE *stream)
Sorgt bei einem Ausgabestrom dafür, dass gepufferte, aber noch nicht
geschriebene Daten geschrieben werden; bei einem Eingabestrom ist der
Effekt undefiniert. Die Funktion gibt normalerweise NULL als Ergebnis
zurück, oder EOF (Konstante mit Wert -1), falls ein Schreibfehler auftritt.
fflush(NULL) bezieht sich auf alle offenen Dateien.
81
Ändert den Namen einer Datei. Bei einem Fehler gibt die Funktion einen
von Null verschiedenen Wert zurück.
FILE * tmpfile(void)
Erzeugt eine temporäre Datei mit Zugriffsmodus "wb+", die automatisch
gelöscht wird, wenn der Zugriff abgeschlossen wird, oder wenn das Pro-
gramm normal zu Ende geht. Als Ergebnis gibt tmpfile() einen Daten-
strom zurück, oder NULL falls die Datei nicht erzeugt werden konnte.
Bei jedem Aufruf erzeugt die Funktion einen anderen Namen; man kann
höchstens von TMP_MAX verschiedenen Namen während der Ausführung
des Programms ausgehen. Zu beachten ist, dass ein Name und keine Datei
erzeugt wird.
Bei einem Fehler gibt die Funktion einen von Null verschiedenen Wert
zurück.
82
zurück, oder EOF (Konstante mit Wert -1), wenn ein Fehler aufgetreten
ist.
stdlib.h – Hilfsfunktionen
Die Definitionsdatei <stdlib.h> vereinbart Funktionen zur Umwandlung von Zahlen, für
Speicherverwaltung und ähnliche Aufgaben.
83
Wandelt den Anfang der Zeichenkette s in long um, dabei wird Zwi-
schenraum am Anfang ignoriert. Die Umwandlung wird beim ersten un-
brauchbaren Zeichen beendet. Die Funktion speichert einen Zeiger auf
den eventuell nicht umgewandelten Rest der Zeichenkette bei *endp, falls
endp nicht NULL ist. Hat base einen Wert zwischen 2 und 36, erfolgt die
Umwandlung unter der Annahme, dass die Eingabe in dieser Basis reprä-
sentiert ist.
Hat base den Wert Null, wird als Basis 8, 10 oder16 verwendet, je nach
s; eine führende Null bedeutet dabei oktal und 0x oder 0X zeigen eine
hexadezimale Zahl an. In jedem Fall stehen Buchstaben für die Ziffern
von 10 bis base-l; bei Basis 16 darf 0x oder 0X am Anfang stehen. Wenn
das Resultat zu groß werden würde, wird je nach Vorzeichen LONG_MAX
oder LONG_MIN geliefert und errno erhält den Wert ERANGE.
84
Gibt den Bereich frei, auf den der Pointer p zeigt; die Funktion hat keinen
Effekt, wenn p den Wert NULL hat. p muss auf einen Bereich zeigen, der
zuvor mit calloc(), malloc() oder realloc() angelegt wurde.
void abort(void)
Sorgt für eine anormale, sofortige Beendigung des Programms.
Die Elemente des Arrays base müssen aufsteigend sortiert sein. In size
muss die Größe eines einzelnen Elements übergeben werden. bsearch()
gibt als Ergebnis einen Zeiger auf das gefundene Element zurück, oder
NULL, wenn keines existiert.
85
Sortiert ein Arraybase[0] bis base[n-1] von Objekten der Größe size in
aufsteigender Reihenfolge. Für die Vergleichsfunktion cmp gilt das gleiche
wie bei bsearch().
int abs(int x)
Gibt den den absoluten Wert (Betrag) |𝑥| von 𝑥 als int an.
long labs(long x)
Gibt den absoluten Wert (Betrag) |𝑥| von 𝑥 als long an.
Die Definitionsdatei time.h vereinbart Typen und Funktionen zum Umgang mit Datum
und Uhrzeit. Manche Funktionen verarbeiten die Ortszeit, die von der Kalenderzeit zum
Beispiel wegen einer Zeitzone abweicht. clock_t und time_t sind arithmetische Typen,
die Zeiten repräsentieren, und struct tm enthält die Komponenten einer Kalenderzeit:
struct tm
{
// Sekunden nach der vollen Minute (0, 61)
// (Die zusätzlich möglichen Sekunden sind Schaltsekunden)
int tm_sec;
86
(Fortsetzung der vorherigen Seite)
tm_isdst ist positiv, wenn Sommerzeit gilt, Null, wenn Sommerzeit nicht gilt, und nega-
tiv, wenn die Information nicht zur Verfügung steht.
clock_t clock(void)
Gibt die Rechnerkern-Zeit an, die das Programm seit Beginn seiner Aus-
führung verbraucht hat, oder -1, wenn diese Information nicht zur Verfü-
gung steht.
87
%a abgekürzter Name des %A voller Name des Wo-
Wochentags. chentags.
%b abgekürzter Name des %B voller Name des Mo-
Monats. nats.
%c lokale Darstellung von %d Tag im Monat (01 - 31).
Datum und Zeit.
%H Stunde (00 - 23). %I Stunde (01 - 12).
%j Tag im Jahr (001 - 366). %m Monat (01 - 12).
%M Minute (00 - 59). %p lokales Äquivalent von
AM oder PM.
%S Sekunde (00 - 61). %U Woche im Jahr (Sonn-
tag ist erster Tag) (00 -
53).
%w Wochentag (0 - 6, Sonn- %W Woche im Jahr (Mon-
tag ist 0). tag ist erster Tag) (00 -
53).
%x lokale Darstellung des %X lokale Darstellung der
Datums. Zeit.
%y Jahr ohne Jahrhundert %Y Jahr mit Jahrhundert.
(00 - 99).
%Z Name der Zeitzone, falls %% %. (Gibt ein % aus)
diese existiert.
Die folgenden vier Funktionen liefern Zeiger auf statische Objekte, die von anderen Auf-
rufen überschrieben werden können.
88
Curses
Die C-Bibliothek Curses beziehungsweise ihre neuere Version NCurses bietet die Mög-
lichkeit, textbasierte Benutzeroberflächen zu erzeugen. Curses wird daher in vielen Shell-
Programmen verwendet, darunter aptitude, cmus, mc, usw.
Um Curses zu starten, muss zunächst die Funktion initscr() aufgerufen werden. Die-
se Funktion erzeugt einen leeres Fenster und weist ihm den Namen stdscr („standard
screen“) zu. Damit das neue Fenster angezeigt wird, muss anschließend die Funktion
refresh() aufgerufen werden, so dass das Shell-Fenster aktualisiert wird und die Än-
derungen sichtbar werden.
Ein minimales Curses-Programm, das nur kurz einen leeren Bildschirm erzeugt, auf die-
sem „Hallo Welt“ ausgibt und sich nach kurzer Zeit selbst beendet, kann folgendermaßen
aussehen:
// Datei: curses-beispiel-1.c
# include <ncurses.h>
int main(void)
{
initscr();
printw("Hallo Welt!");
refresh();
napms(3000);
(continues on next page)
89
(Fortsetzung der vorherigen Seite)
endwin();
return 0;
}
In diesem Beispiel wurde zudem die Curses-Funktion napms() verwendet, die eine weitere
Ausführung des Programms um die angegebene Anzahl in Millisekunden verzögert.
Mittels addch(c) kann ein einzelnes Zeichen auf dem Bildschirm ausgegeben werden.
Mittelsaddstr(*str) kann eine ganze Zeichenkette auf dem Bildschirm ausgegeben
werden. (Dabei wird intern die Funktion addch() aufgerufen, bis die Zeichenkette
abgearbeitet ist.)
Mittels printw() kann Text in der gleichen Weise in einem Curses-Fenster ausgege-
ben werden, wie dies mittels der Funktion printf() auf dem Standard-Ausgang der
Fall ist.
Damit der Text an der richtigen Stelle im Curses-Fenster erscheint, kann man mittels der
Funktion move() den Cursor an eine bestimmte Stelle bewegen. Als erstes Argument wird
dabei die Zeilennummer y, als zweites die Spaltennummer x angegeben, also move(y,
x).1 Da Curses, wie in C üblich, bei der Nummerierung mit Null beginnt, entspricht
move(0,0) einem Bewegen des Cursors in die obere linke Ecke; die erlauben Maximalwerte
für die Zeilen- und Spaltennummer in move() sind entsprechend um 1 kleiner als die
Zeilen- und Spaltenanzahl des Fensters. Diese beiden Werte können mittels der Funktion
getmaxyx(stdscr, maxrow, maxcol) bestimmt werden, wobei maxrow und maxcol im
2
Voraus als int deklariert werden müssen:
// Datei: curses-beispiel-2.c
# include <ncurses.h>
int main(void)
{
initscr();
1 Eine „Spalte“ in Curses der Breite eines Textzeichens; die meisten Fenster haben daher mehr Spalten
als Zeilen.
2 Für die Größe des Hauptfensters stdscr sind in Curses auch die Makros LINES und COLS definiert,
die vom Compiler durch die beim Programmstart vorliegenden Werte ersetzt werden.
90
(Fortsetzung der vorherigen Seite)
napms(3000);
endwin();
return 0;
}
Die Kombination von move() mit einer der Print-Anweisungen kommt in Curses-
Anwendungen sehr häufig vor; daher gibt es zu den drei Ausgabefunktionen addch(),
addstr() und printw() auch die kombinierten Funktionen mvaddch(), mvaddstr() und
mvprintw(). Diesen wird beim Aufruf zunächst die gewünschte Position des Cursor an-
gegeben, die übrigen Argumente sind mit den Basisfunktionen identisch. Beispielsweise
sind die folgenden beiden Aufrufe identisch:
// Kurzschreibweise:
mvaddstr(0, 3, "Hallo Curses!")
Zur Eingabe von Text gibt es in Curses ebenfalls drei grundlegende Funktionen:
Mittels getch(c) kann ein einzelnes Zeichen vom Bildschirm eingelesen werden;
das Zeichen wird dabei automatisch eingelesen, ohne dass die Enter-Taste gedrückt
werden muss.
Mittels scanw() kann Text in der gleichen Weise von einem Curses-Fenster einge-
lesen werden, wie dies mittels der Funktion scanf() aus dem Standard-Eingang der
Fall ist.
Als Standard geben alle Eingabefunktionen die vom Benutzer eingegebenen Zeichen un-
mittelbar auf dem Bildschirm aus, auch ohne dass dazu die refresh()-Funktion aufgeru-
fen werden müsste; zusätzlich stoppt das Programm, bis die Eingabe vom Benutzer erfolgt
ist. Ist dies nicht gewünscht, so müssen diese Einstellung, wie im folgenden Abschnitt be-
schrieben, deaktiviert werden.
91
Modifizierung der Ein- und Ausgabe
In Curses gibt es folgende Funktionen, die das Verhalten des Programms hinsichtlich
Eingabe und Ausgabe anzupassen:
keypad():
Diese Funktion sollte von jedem interaktiven Curses-Programm aufgerufen werden,
denn sie ermöglicht die Verwendung der Funktions- und Pfeiltasten. Um beispiels-
weise die Funktion für den Standard-Bildschirm stdscr zu aktivieren, gibt man
3
keypad(stdscr, TRUE); ein.
curs_set():
Diese Funktion kann verwendet werden, um den Cursor unsichtbar oder wieder sicht-
bar zu machen. Mit curs_set(0); wird der Cursor unsichtbar, mit curs_set(1);
wieder sichtbar.
halfdelay(n):
Mit dieser nur in Ausnahmefällen verwendeten Funktion kann festgelegt werden,
dass beim dem Einlesen eines Zeichens mittels getch() oder einer Zeichenkette ma-
ximal 𝑛 Zehntel Sekunden gewartet wird. Wird in dieser Zeit kein Text eingegeben,
so fährt das Programm fort. Dies kann beispielsweise für eine Timeout-Funktion bei
einer Passwort-Eingabe verwendet werden.
nodelay():
3 Die Konstanten TRUE und OK beziehungsweise FALSE sind in der Datei ncurses.h als 1 beziehungs-
weise 0 definiert.
92
Diese Funktion wird von den meisten interaktiven Curses-Programm zu Beginn auf-
gerufen, denn sie verhindert, dass das Programm bei der Verwendung der Funktion
getch() anhält. Anstelle dessen liefert getch() kontinuierlich den Wert ERR (ent-
spricht dem Wert -1) zurück, sofern der Benutzer keine Taste gedrückt hat.
Mit Hilfe von nodelay(stdscr, TRUE) kann beispielsweise eine mainloop() program-
miert werden, die einzelne von der Tastatur aus eingegebene Zeichen über eine switch -
4
Anweisung mit bestimmten Anweisungen verknüpft:
// Datei: curses-beispiel-3.c
# include <ncurses.h>
int main()
{
int c;
int quit = FALSE;
initscr();
cbreak();
noecho();
keypad(stdscr, TRUE);
nodelay(stdscr, TRUE);
while( !quit )
{
c = getch();
switch(c)
{
case ERR:
napms(10);
break;
case 'q':
quit = TRUE;
break;
default:
mvprintw(3, 0, "ASCII-Code des Zeichens: %3d;", c);
mvprintw(3, 30, "Zeichen wird dargestellt als: \'%c\'.", c);
break;
}
refresh();
}
endwin();
(continues on next page)
4 Mit nodelay(stdscr, FALSE) kann das ursprüngliche Verhalten von getch() wieder hergestellt
werden.
93
(Fortsetzung der vorherigen Seite)
return 0;
}
Wird keine Taste gedrückt (Rückgabewert: ERR), so wartet das Programm durch
Aufruf von napms(10) zehn Millisekunden lang, bis es mit der Ausführung fortfährt.
Ohne eine derartige Verzögerung würde das Programm die Schleife kontinuierlich
mit maximaler Geschwindigkeit abarbeiten und somit ständig maximale CPU-Last
verursachen; mit „nur“ zehn Millisekunden Pause reduziert sich die CPU-Auslastung
auf circa 1%.
Wird eine beliebige Taste außer q gedrückt, so wird der ASCII-Wert des Zeichens
und das Zeichen selbst ausgegeben. Die Darstellung funktioniert nur bei alphabeti-
schen und numerischen Zeichen wie gewohnt, bei Funktions- und Sondertasten kann
zumindest der ASCII-Wert des eingegebenen Zeichens abgefragt werden.
Editor-Funktionen
Die Curses-Bibliothek stellt, da sie auf textbasierte Programme ausgerichtet ist, einige
Funktionen bereit, die das Eingeben von Text ziemlich komfortabel gestalten.
Um einzelne Zeichen oder Zeilen einzugeben oder zu löschen, gibt es in Curses folgende
Funktionen:
insch()
Mit insch(c) kann ein einzelnes Zeichen an der Stelle des Cursors eingefügt wer-
den; der Rest der Zeile wird dabei automatisch um eine Zeichenbreite nach rechts
verschoben.
delch()
Mit delch() wird das Zeichen an der Stelle des Cursors gelöscht; der Rest der Zeile
wird dabei automatisch um eine Zeichenbreite nach links verschoben.
insertln()
Mit insertln() kann eine neue Zeile an der Stelle des Cursors eingefügt werden; alle
folgenden Zeilen werden dabei automatisch um eine Zeile nach unten verschoben.
deleteln()
94
Mit deleteln() wird die Zeile an der Stelle des Cursors gelöscht; alle folgenden
Zeilen werden dabei automatisch um eine Zeile nach oben verschoben.
Möchte man an der gleichen Stelle am Bildschirm aufeinander folgende Textstellen mit
unterschiedlicher Länge ausgeben, so werden durch refresh(); nur die jeweils neu dar-
zustellenden Zeichen auf dem Bildschirm aktualisiert; wird an der gleichen Startpositiion
zunächst eine lange und danach eine kurze Textstelle ausgegeben, so bleibt bei der Aus-
gabe der kurzen Textstelle ein Rest der langen Textstelle bestehen.
clrtoeol()
Mit clrtoeol() werden alle Zeichen von der Cursor-Position aus bis zum Ende der
Zeile gelöscht („clear to end of line“).
clrtobot()
Mit clrtobot() werden alle Zeilen von der Cursor-Position aus bis zum Ende des
Fensters gelöscht („clear to bottom of window“).
Text kann in Curses auf den meisten Shells auch farbig oder fettgedruckt dargestellt
werden. Eine solche Modifizierung wird mittels der folgenden Funktionen vorgenommen
werden:
attron(attr)
Mit dieser Funktion wird das angegebene Attribut attr aktiviert.
attroff(attr)
Mit dieser Funktion wird das angegebene Attribut attr deaktiviert.
attrset(attr)
Mit dieser Funktion wird das angegebene Attribut attr aktiviert; alle sonstigen
Attribute werden deaktiviert.
Die obigen Funktionen wirken sich auf die weitere Darstellung aller Zeichenketten aus. Um
den ausgegebenen Text wieder in „normaler“ Form darzustellen, kann attrset(A_NORMAL)
verwendet werden. Eine Übersicht aller Textattribute ist in der folgenden Tabelle zusam-
mengestellt.
95
A_NORMAL Normaler Text
A_BOLD Text in Fettschrift und mit erhöhter Hel-
ligkeit
A_DIM Text mit verringerter Helligkeit (wird
nicht von jeder Shell unterstützt)
A_REVERSE Text mit vertauschter Vorder- und Hinter-
grundfarbe
A_UNDERLINE Unterstrichener Text
A_BLINK Blinkender Text (wird nicht von jeder
Shell unterstützt)
A_STANDOUT Hervorgehobener Text (entspricht meist
A_REVERSE)
Farbiger Text
Um Text farbig auszugeben, sollte zunächst geprüft werden, ob eine farbige Darstellung
von der Shell unterstützt wird. Dazu gibt es in Curses die Funktion has_colors(), die
entweder TRUE oder FALSE als Ergebnis liefert. Ist farbiger Text auf der Shell möglich, so
kann in Curses die Farbunterstützung mittels der Funktion start_color() freigeschaltet
werden; dabei werden zugleich die in der folgenden Tabelle angegebenen Farbnamen als
symbolische Konstanten definiert.
96
if ( has_colors() == FALSE )
printw("Kein farbiger Text moeglich!");
else
start_color();
Neben der Angabe von COLOR_PAIR(n), die für das Farben-Paar mit der Nummer 𝑛
steht, können ebenfalls weitere Attribute mittels eines binärem Oders angegeben wer-
den. Wird ein Farbenpaar mit dem Attribut A_BOLD kombiniert, so erscheint der Text
nicht nur fettgedruckt, sondern auch in einer etwas helleren Farbe; aus Schwarz wird als
Vordergrundfarbe beispielsweise Grau. Bei einer gezielten Verwendung kann damit das
Farbspektrum etwas erweitert werden.
Es ist auch möglich dem Hintergrund ein Farben-Paar zuzuweisen; damit ändert sich das
Aussehen des Curses-Fensters, auch wenn kein Text ausgegeben wird. Die Attribute für
den Hintergrund werden mit der Funktion bkdg() gesetzt. Wird neben einem Farben-
paar und einem binärem Oder zusätzlich ein beliebiges Zeichen angegeben, so wird der
Hintergrund standardmäßig mit diesem Zeichen bedruckt:
In diesem Fall würde mit den obigen Definitionen das Curses-Fenster blau erscheinen und
an allen Stellen ohne Text mit gelben +-Zeichen aufgefüllt werden.
Ein neues Fenster wird mittels der Funktion newwin() erstellt. Als Rückgabewert liefert
diese Funktion entweder einen Zeiger auf ein WINDOW-Objekt, oder NULL, falls beim Er-
stellen des Fensters ein Fehler aufgetreten ist. Als Argumente für newwin() werden die
Anzahl an Zeilen und Spalten sowie die Startposition der oberen linken Ecke des Fensters
angegeben:
int nrows = 5;
int ncols = 20;
int starty = 3;
int startx = 5;
97
(Fortsetzung der vorherigen Seite)
Ein neues Fenster darf nicht größer sein als das Standard-Fenster stdscr, und muss
mindestens eine Zeile und eine Spalte beinhalten. Gibt man allerdings newwin(0,0,0,0);
ein, so wird ein neues Fenster erzeugt, das genauso groß ist wie das Fenster stdscr. Damit
das neue Fenster auf dem Bildschirm sichtbar wird, muss die Funktion wrefresh() mit
dem entsprechenden Namen des Fensters aufgerufen werden. Bei Bedarf müssen zudem
die Funktionen keypad() und nodelay für das jeweilige Fenster aufgerufen werden.
Die Funktionen move(), addch, addstr(), printw(), getch(), getstr() lassen sich auf
ein existierende Fenster werden, wenn an ihren Funktionsname vorne ein w angehängt
und als erstes Argument ein Zeiger auf das zu bearbeitende Fenster übergeben wird, also
beispielsweise waddstr(mywin, "Text").
Bei der Verwendung von mehreren sich überlappenden Fenstern ist nicht sichergestellt,
dass der Text von Curses wie erwartet dargestellt wird. Es wird daher dringend empfoh-
len, entweder neue Fenster mit voller Fenstergröße zu erzeugen, oder das Standard-Fenster
nicht zu benutzen und dafür mehrere nicht überlappende Fenster zu verwenden. Das Fens-
ter, das zuletzt mit einem Aufruf von wrefresh() aktualisiert wurde, wird als „oberstes“
angezeigt und verdeckt gegebenenfalls andere Fenster.
Um ein Fenster wieder zu schließen, wird die Funktion delwin() verwendet, wobei
als Argument wiederum ein Zeiger auf ein Fenster übergeben wird, also beispielsweise
delwin(mywin). Das Fenster, das nach dem Löschen aktiv angezeigt werden soll, muss
dabei mittels wrefresh() aktualisiert werden. Gegebenenfalls muss es dazu erst mittels
touchwin(win_name) zur vollständigen Aktualisierung vorgemerkt werden, falls ansons-
ten keine Änderungen vorgenommen wurden.
Unterfenster erstellen
Neben Fenstern können in Curses auch so genannte Unterfenster erstellt werden. Diese
können dazu verwendet werden, um einen Teil des Hauptfensters leichter ansteuern oder
mit anderen Farb- und Textattributen versehen zu können. Der Inhalt eines Unterfensters
hingegen stimmt mit dem Inhalt des Hauptfensters an der jeweiligen Stelle überein.
Ein neues Unterfenster kann, ebenso wie mit newwin() ein neues Fenster erstellt wird,
mittels subwin() erzeugt werden, wobei als erstes Argument der Name des übergeord-
neten Fensters und als weitere Argumente die Anzahl an Zeilen und Spalten sowie die
Startposition der oberen linken Ecke angegeben werden:
98
Die zweite Möglichkeit ein Unterfenster zu erstellen bietet die Funktion derwin(), wobei
in diesem Fall die Werte starty und startx relativ zum übergeordneten Fenster (und
nicht relativ zum Hauptfenster stdscr) angegeben werden.
Alle Funktionen, die auf ein „richtiges“ Fenster angewendet werden können, lassen sich
auch auf ein Unterfenster anwenden. Unterfenster haben einen eigenen Cursor und eigene
Text- und Farbattribute; sie können selbst wiederum Ausgangspunkt für neue Unterfenster
sein.
Pads
Neben normalen Fenstern gibt es in Curses auch so genannte „Pads“. Während die Funk-
tionen für Pads weitgehend mit den für normale Fenster identisch sind, ist ihre Größe
nicht auf die Größe des Hauptfensters beschränkt; die maximale Größe eines Pads ist
allerdings auf 32767 Zeilen beziehungsweise Spalten beschränkt.
Mittels den für Fenster üblichen Ausgabefunktionen, beispielsweise waddstr(), kann Text
auf einem Pad angezeigt werden. Damit die Änderungen auf dem Bildschirm sichtbar
werden, kann allerdings nicht wrefresh() verwendet werden, da zusätzlich angegeben
werden muss, von welcher Stelle aus das Pad angezeigt werden soll: Üblicherweise ist
ein Pad größer als der Bildschirm, es kann somit nur ein Ausschnitt des Pads angezeigt
werden. Dies wird bei der Funktion prefresh() berücksichtigt:
Hierbei bezeichnen pad_ymin und pad_xmin die Koordinaten der oberen linken Ecke in-
nerhalb des Pads, von der aus der Inhalt angezeigt werden soll. Die übrigen Argumente
geben die Koordinaten des Bereichs an, in dem das Pad relativ zum Hauptfenster ange-
zeigt werden soll.
5 Umgekehrt wird allerdings durch Funktionen wie wclear() der Inhalt beim Löschen des Inhalts
eines Fensters automatisch auch der Inhalt aller Unterfenster gelöscht.
99
Subpads
Ebenso wie Fenster ein oder mehrere Unterfenster haben können, können Pads auch ein
oder mehrere Subpads beinhalten. Ebenso wie bei den Unterfenstern ist der Inhalt eines
Subpads mit dem Hauptpad identisch, das Subpad kann allerdings beispielsweise eigene
Attribute und Farben aufweisen.
6
Ein neues Subpad kann mittels subpad() erzeugt werden:
Bei der Verwendung von Pads und Subpads ist zu beachten, dass diese nicht innerhalb
des Hauptfensters verschoben werden dürfen; die mvwin()-Funktion kann somit nicht auf
Pads angewendet werden. Ebenso sind die scroll()-Funktionen für Pads nicht definiert.
Mittels delwin(padname) kann ein (Unter-)Pad wieder gelöscht werden. Auch hierbei
sollten zunächst alle Subpads und erst zuletzt das Hauptpad gelöscht werden, um Spei-
cherlecks zu vermeiden.
Curses-Programme nutzen die Shell als Ein- und Ausgabefenster; sie lassen sich daher
nicht innerhalb der gleichen Shell aufrufen und mit dem gdb -Debugger analysieren. Fol-
gender Trick schafft hier Abhilfe:
Man öffnet ein zweites Shell-Fenster und gibt dort tty ein, um sich die Nummer
/dev/pts/23.
dieser Shell anzeigen zu lassen; das Ergebnis lautet beispielsweise
Anschließend gibt man in diesem Fenster sleep 1000000000000000000000 ein, um
alle weiteren Eingaben an diese Shell für eine lange Zeit zu ignorieren. (Bei Bedarf
kann der Schlafmodus mittels Strg C abgebrochen werden.)
100
tty /dev/pts/23
Nun kann run eingeben werden, um das Programm im Debugger ablaufen zu lassen.
Die Ausgabe des Programms erfolgt dabei im zweitem Shell-Fenster.
101
Links
Tutorials
C Tutorial
Handbücher, Nachschlagewerke
The C Book
C Wikibook
Spezielle Themen
Hilfe
102
Debugging
Quellen
103
Literaturverzeichnis
[Hall2007] Brian „Beej“ Hall: Beej’s Guide to C Programming. Creative Commons Licen-
se, 2007.
[Lopo2000] Erik de Castro Lopo, Peter Aitken und Bradley L. Jones: C-Programmierung
für Linux in 21 Tagen. Markt+Technik Verlag, 2000.
[Oesch2008] Ivo Oesch: Eine Einführung in die Programmiersprache C und die Grundla-
gen der Informatik. Skript Version 2.4, 2008.
104
Stichwortverzeichnis
Symbols casin(), 77
#define, 51 Cast-Operator, 25
#if, 53 catan(), 77
#ifdef, 53 cbreak(), 92
#ifndef, 53 cdecl, 64
#include, 51 ceil(), 76
cflow, 65
A char, 4
abort(), 85 clock(), 87
abs(), 86 cmath.h, 77
addch(), 90 const, 6
addstr(), 90 continue, 33
Adressoperator, 8 cos(), 75
Array, 10 cosh(), 76
ASCII-Tabelle, 14 ctime(), 88
asctime(), 88 curs_set(), 92
asin(), 75 Curses, 88
assert.h, 75
astyle, 64
D
atan(), 75 Debugger, 66
atof(), 83 Definition, 3
atoi(), 83 Deklaration, 3
atol(), 83 difftime(), 87
attroff(), 95 div(), 86
attron(), 95 double, 4
attrset(), 95
auto, 6
E
echo(), 92
B else, 31
else if, 31
Block, 27
endwin(), 89
break, 33
enum, 39
bsearch(), 85
exit(), 49, 85
C exp(), 76
extern, 5
cacos(), 77
Call by Reference, 28
Call by Value, 28
F
fabs(), 76
calloc(), 36, 84
fclose(), 81
case, 32
Feld, 10
105
feof(), 81 log10(), 76
ferror(), 81 long, 4
fflush(), 19, 81
fgets(), 20
M
File-Pointer, 45 make, 70
float, 4 Makefile, 70
floor(), 76 Makro, 52
fopen(), 80 math.h, 75
for, 33 memchr(), 78
freopen(), 81 memmove(), 78
frexp(), 76 memset(), 78
Funktion, 27 mktime(), 87
G
modf(), 76
move(), 90
gdb, 66 mvaddch(), 91
getch(), 91 mvaddstr(), 91
getenv(), 85 mvprintw(), 91
N
getmaxyx(), 90
getnstr(), 91
gets(), 20 newwin(), 97
getstr(), 91 nodelay(), 92
gmtime(), 88 noecho(), 92
O
gprof, 68
H Operator, 22
P
halfdelay(), 92
Header-Datei, 50
I Pad, 99
Pointer, 8
if, 31 pow(), 76
Inhaltsoperator, 9 Präprozessor, 51
init_pair(), 96 prefresh(), 99
Initialisierung, 3 printf(), 15
initscr(), 89 printw(), 90
int, 4 putchar(), 18
K puts(), 18
keypad(), 92 Q
Kommentar, 1 qsort(), 85
L R
labs(), 86
rand(), 84
ldexp(), 76
raw(), 92
ldiv(), 86
realloc(), 36, 84
localtime(), 88
refresh(), 89
log(), 76
register, 6
106
remove(), 81 T
rename(), 81 tan(), 75
return, 27 tanh(), 76
S time, 71
time(), 87
scanf(), 18
tmpfile(), 82
scanw(), 91
tmpnam(), 82
Schnittstelle, 50
typedef, 39
setbuf(), 82
setvbuf(), 82 U
short, 4 union, 42
signed, 5 unsigned, 5
V
sin(), 75
sinh(), 75
sizeof, 4, 25 valgrind, 73
splint, 70 Variable, 2
sprintf(), 83 volatile, 6
W
sqrt(), 76
srand(), 84
start_color(), 96 while, 34
static, 5 Whitespace, 19
stdio.h, 80
stdlib.h, 83
Z
strcat(), 37, 78 Zeichenkette, 12
strchr(), 79 Zeiger, 8
strcpy(), 37, 78
strcspn(), 79
Stream, 45
strerror(), 80
strftime(), 87
String, 12
string.h, 77
strlen(), 80
strncat(), 37, 78
strncmp(), 79
strncpy(), 37, 78
strpbrk(), 79
strrchr(), 79
strspn(), 79
strstr(), 79
strtod(), 83
strtok(), 80
strtol(), 83
strtoul(), 84
struct, 40
Subpad, 99
switch, 32
system(), 49, 85
107