»Ein Optimist ist ein Mensch, der ein Dutzend Austern bestellt, in der Hoffnung, sie mit der Perle, die er darin findet, bezahlen zu können.« -- Theodor Fontane
22 Crashkurs in C und Perl
Beim Thema »Programmieren unter Linux« beziehungsweise »Programmieren von Linux« gibt es vor allem drei wichtige Programmiersprachen: <Dieses Kapitel war eigentlich als Bestandteil des vorherigen Kapitels »Softwareentwicklung unter Linux« geplant, wurde aber so groß, dass wir es nun doch ausgegliedert haben.>
- Die Shell
- Mit der Shell sollten Sie bereits vertraut sein und sie dank vorheriger Kapitel auch programmieren können.
- C
- Die meisten Systemprogramme sowie fast der gesamte Kernel sind in dieser Sprache codiert.
- Perl
- Perl ist eine universelle Programmiersprache, die für alles nützlich ist, außer für die Kernel-Programmierung.
Wir geben Ihnen in diesem Buch eine Einführung in alle diese Sprachen – und da Sie die Shell schon kennen, folgt an dieser Stelle ein Crashkurs in C und Perl. Diese Sprachen bieten Ihnen dann die Möglichkeit, immer weiter in die Tiefen von Linux hinabzusteigen und immer mehr zu verstehen. Niemand kann Sie aufhalten. ;-)
22.1 Die Programmiersprache C – Ein Crashkurs
In diesem Buch wurde bereits die Programmierung mit der Shell bash besprochen, warum also noch mehr Programmiersprachen? Diese Frage ist leicht zu beantworten, da wir uns zwei ganz besondere Sprachen hierfür herrausgesucht haben.
Zum einen ist das die Programmiersprache C, die in diesem Abschnitt behandelt wird, und zum anderen ist dies die Skriptsprache Perl, die im nächsten Abschnitt folgt. <Warum wir uns ausgerechnet noch für Perl entschieden haben, erfahren Sie im nächsten Abschnitt. Hier beschäftigen wir uns zunächst nur mit C.>
Fast der gesamte Linux-Kernel (und das gilt auch für die BSD-Kernel) ist in der Sprache C geschrieben.
Des Weiteren sind fast alle wichtigen Basisprogramme eines Linux- und BSD-Systems ebenfalls in C geschrieben. Aus diesem Grund ist die Bedeutung von C für diese Systeme offenbar äußerst groß. Und ganz davon abgesehen handelt es bei C um eine äußerst schöne und performante Programmiersprache.
Es stimmt, C ist keine objektorientierte Sprache, aber wenn Sie auf objektorientierte Entwicklung verzichten können, dann ist C eine gute Wahl für Sie. <Ausserdem lässt sich mit C-Kenntnissen wohl am einfachsten C++ lernen.> Hier noch ein paar Gründe für C:
- C ist äußerst performant! Sie können nur mit äußerst ausgefeiltem Assembler die Performance von compiler-optimiertem C-Code erreichen oder gar übertreffen.
- Sie können NUR mit dieser Programmiersprache (und in einigen Bereichen mit noch etwas Know-How in Assembler) die tieferen Interna von Linux und BSD verstehen.
- (ANSI-)C ist wesentlich portabler als etwa Java.
- Und C sieht gut aus. ;-)
Da C eine sehr komplexe Programmiersprache ist, können wir Ihnen hier nur eine sehr grobe und kurze Einführung in diese schöne Sprache geben. Auf galileocomputing.de finden Sie allerdings das OpenBook »C von A bis Z« von Jürgen Wolf, das Sie auch in die tieferen Details der Sprache einführt. Anschließend sollten Sie dann noch einen Blick in »Expert C Programming: Deep C Secrets« von Peter Van Der Linden (SunSoft Press) werfen, das es leider nur in englischer Sprache gibt.
Zum Thema Linux/Unix/BSD-Programmierung in C gibt es auch diverse gute Bücher, die aber auch etwas Vorwissen verlangen:
- »Linux Unix Programmierung« von Jürgen Wolf, erschienen bei Galileo Computing und verfügbar als OpenBook. Dieses Buch geht auch auf diverse Libraries (etwa Gtk und MySQL) ein. Es ist für die typischen Ansprüche der heutigen Leser wahrscheinlich am besten geeignet, da man »alles« einmal kennenlernen möchte. Daher empfehlen wir dieses Buch auch generell erst inmal jedem. <Der Verlag hat uns übrigens nicht bestochen, es handelt sich tatsächlich um unsere Meinungen.>
- »Linux Unix Systemprogrammierung« von Helmut Herold, erschienen bei Addison-Wesley. Dieses Buch beschäftigt sich sehr intensiv mit der reinen Systemprogrammierung und geht fast gar nicht auf Libraries ein. Es ist unsere Empfehlung für die zukünftigen Unix-Nerds unter unseren Lesern.
- Es gibt noch einige andere deutschsprachive Grundlagenbücher, die wir aber nicht gelesen haben und daher auch nicht empfehlen können.
Natürlich gibt es noch weitere populäre Bücher, die erwähnenswert sind (wie »Advanced Programming in the Unix Environment« und »Programmieren von Unix-Netzwerken« von Richard Stevens). Viele weitere gute Bücher aus unserem Bücherschrank sind jedoch entweder zu alt oder zu speziell, um sie hier aufzulisten.
22.1.1 Hello World in C
Da wir Ihnen die Grundlagen dieser Sprache so schonend wie möglich beibringen möchten, haben wir uns dazu entschlossen, sie anhand von Beispielen zu erläutern. Das erste Programm, was zu diesem Zweck herhalten soll, ist natürlich wie in so ziemlich jedem C-Buch »Hello World«. Es dient zunächst als Einstieg, bevor wir etwas mehr ins Detail gehen.
#include <stdio.h> void main() { printf("Hello World!\n"); }
Listing 22.1 Ein schlechter »Hello World«-Code ohne Rückgabewert
Das sieht etwas kompliziert aus, ist aber ganz einfach. Wir beginnen mit dem Codeschnipsel int main(). Jedes Programm muss an einem bestimmten Punkt anfangen. Das heißt: Irgendeine Anweisung muss als Erste ausgeführt werden.
Aus der Shellskript-Programmierung kennen Sie bereits Funktionen. In C wird gleich zum Programmstart eine Funktion aufgerufen, und diese bestimmt dementsprechend, welche Anweisungen als Erstes ausgeführt werden. Diese Funktion heißt in C main.
Hinter einem Funktionsnamen stehen in C immer Klammern, die die Parameterliste der Funktion beinhalten (dazu später mehr). Hat eine Funktion keine Parameter, so können diese Klammern auch leer bleiben (wie im Falle der main-Funktion.
Zudem geben Funktionen immer entweder Daten eines bestimmten Datentyps zurück oder gar keinen. Der Rückgabetyp wird dabei vor die Implementierung der Funktion geschrieben. Ist der Rückgabetyp void, so wird nichts zurückgegeben.
Normalerweise ist dies bei der main-Funktion nicht so, doch der Einfachheit halber haben wir es im oberen Beispiel trotzdem so gelassen.
Die Implementierung einer Funktion wird, wie in der bash, durch geschweifte Klammern eingeschlossen. Ein Funktionsaufruf (wie der der Funktion printf) hingegen wird ohne diese Klammern erledigt und mit einem Semikolon beendet.
Im Falle der printf-Funktion wird nun auch erstmals ein Funktionsparameter übergeben: Diese Funktion gibt den Text, den man ihr übergibt, auf dem Monitor aus.
Zu guter Letzt, bevor wir uns mit den Details befassen, soll noch die erste Programmzeile erläutert werden. Es handelt sich dabei um eine Anweisung für den sogenannten »Präprozessor« des Compilers. Anweisungen an den Proprocessor beginnen generell mit einer Raute (#). Hinter der Raute steht ein spezieller Befehl, etwa include oder define, und dahinter stehen die Parameter für diesen Befehl.
Im Falle des Präprozessor-Befehls include wird der Inhalt einer anderen Datei durch den Präprozessor in den Quellcode eingebaut. Der Quellcode der anderen Datei wird genau dort in den eigentlichen Quellcode eingebaut, wo der Programmierer den include-Befehl eingetragen hat.
Die Datei stdio.h enthält dabei die Deklaration der printf-Funktion. In einer Funktionsdeklaration ist festgeschrieben ob eine Funktion einen Wert (und wenn ja: welchen Typ) zurückgibt und welche Parameter sie akzeptiert.
Außerdem ist festgelegt, von welchem Typ diese Parameter sind.
Den Code übersetzen
Wie Sie einige Seiten zuvor bereits gelernt haben, wird ein C-Programm mit dem GNU Compiler gcc folgendermaßen übersetzt (der Dateiname der Quellcode-Datei sei hello.c): <Ignorieren Sie die darauffolgende Warnmeldung zunächst einmal.>
$ gcc -o hello hello.c hello.c: In function 'main': hello.c:4: warning: return type of 'main' is not 'int'
Listing 22.2 hello.c übersetzen
Ausgeführt wird das Programm dann über die Shell als ganz normales Programm im Arbeitsverzeichnis:
$ ./hello Hello World
Listing 22.3 hello ausführen
22.1.2 Kommentare
Wie in jeder anderen vernünftigen Programmiersprache gibt es auch in C die Möglichkeit, Kommentare in den Quellcode zu schreiben. Diese Kommentare werden vom Compiler nicht in Maschinencode übersetzt. Ein Kommentar wird durch die zwei hintereinanderstehenden Zeichen /* eingeleitet und durch */ beendet. Ein Kommentar kann in C über mehrere Zeilen verteilt sein und endet nicht automatisch beim Zeilenende. <Einige Compiler unterstützen auch die C++-Kommentare. Diese werden durch // eingeleitet und durch das Zeilenende abgeschlossen, doch diese sind nicht im ANSI-C-Standard festgeschrieben!>
/* Die Datei stdio.h einbinden. Sie befindet sich im globalen * include-Verzeichnis des Rechners (meist /usr/include) */ #include <stdio.h> /* Die main-Funktion verwendet keine Parameter und gibt keinen * Wert zurück (was schlecht ist). */ void main() { /* Die printf-Funktion gibt Text aus */ printf("Hello World!\n"); /* Am Ende der main-Funktion wird das Programm beendet */ }
Listing 22.4 Das obige Programm mit Kommentaren
Übrigens: Kommentare werden nicht als Kommentare gewertet, wenn sie innerhalb von Zeichenketten stehen. Hier würde nur der Kommentar Teil der Textausgabe sein:
printf("/* Gib Text aus */ Hello World!\n");
Listing 22.5 Kein Kommentar
22.1.3 Datentypen und Variablen
In C gibt es etwas, was in nur wenigen anderen Sprachen so komplex ausgelegt ist: Datentypen. Anders als bei der Programmierung der Shell kann in C nicht einfach irgendeine Variable ohne Typ erzeugt werden, in der Sie dann fast alles speichern können. Variablen in C müssen immer von einem bestimmten Datentyp sein.
Ein Datentyp hat dabei immer einen Wertebereich. Dieser Wertebereich richtet sich nach der Zahl der Bits, die zum Speichern dieses Datentyps benutzt werden. In einem 8 Bit großen Datentyp können dementsprechend 28 = 256 verschiedene Werte gespeichert werden.
Variablen erzeugen
Bevor wir auf die eigentlichen Datentypen eingehen, möchten wir noch zeigen, wie Variablen allgemein angelegt werden. Zunächst wird der Typ der Variable angegeben, danach die Bezeichnung der Variable. Diese Erzeugungsanweisung wird mit einem Semikolon abgeschlossen.
Datentyp Variablenname;
Listing 22.6 Variable erzeugen
Deklaration
Man spricht hierbei von einer Deklaration einer Variablen.
Initialisierung
Wenn man möchte, dann kann man den Wert einer Variablen auch gleich bei der Deklaration setzen. Man spricht beim Setzen des ersten Wertes einer Variable von einer Initialisierung.
Datentyp Variablenname = Wert;
Listing 22.7 Variable deklarieren und initialisieren
Später im Programmcode kann man Variablen ebenfalls Werte zuweisen:
Variablenname = Wert;
Listing 22.8 Variable-Werte verändern
Bezeichner von Variablen
Die Namen, die man einer Variablen gibt, dürfen nicht völlig frei gewählt werden. Es gibt einige Regeln zu beachten:
- C unterscheidet zwischen Groß- und Kleinschreibung. Die Variablen ABC, abc, aBc, AbC und ABc sind daher unterschiedlich.
- Es dürfen keine C-Schlüsselwörter als Variablennamen verwendet werden.
- Variablen können entweder mit einem Groß-/Kleinbuchstaben oder einem Underscore (_) beginnen.
- Der restliche Teil des Variablennamens darf sich aus Groß-/Kleinbuchstaben, Underscores und Zahlen zusammensetzen.
- Die Namenslänge für Variablen wird vom Compiler beschränkt. Es empfiehlt sich aber, keine zu langen Bezeichner zu verwenden. Der ANSI-C-Standard beschränkt die gültige Länge eines Bezeichners auf 31 Zeichen.
/* gültige Variablennamen */ i, abc, _def, __gh, i_j, k0l9m, MeineKatzeKannFliegen /* ungültige Variablennamen */ 0MeineKatzeKannFliegen, =Sonderzeichen, mäh
Listing 22.9 (Un)gültige Variablennamen
Ausgabe von Variablen-Werten
Wir verwenden für die Ausgabe von Werten einer Variablen im Folgenden immer die Funktion printf. Dieser Funktion wird ein sogenannter Formatstring übergeben. Optional können – durch Kommata getrennt – nach dem Formatstring Variablen gelistet werden, die durch die Funktion ausgegeben werden sollen.
printf( Formatstring [, optionale Variablen] );
Listing 22.10 Schema eines printf-Aufrufs
Damit printf eine Variable verwendet, muss diese nicht nur übergeben, sondern auch im Formatstring angegeben werden. Wie dies funktioniert, erklären wir anschließend für jeden Datentyp einzeln.
Im Formatstring können printf zudem spezielle Escape-Zeichen übergeben werden, die von der Funktion umgewandelt werden. Der Einfachheit halber werden wir uns in diesem Crashkurs auf die Escape-Sequenz \n beschränken, das die aktuelle Zeile beendet und an den Anfang der nächsten Zeile geht.
Hier nur ein kleines Beispiel zur Ausgabe eines Zeichens:
printf("Das Zeichen ist %c\n", zeichenvar);
Listing 22.11 Schema eines printf-Aufrufs
In diesem Fall würde der Text »Das Zeichen ist « ausgegeben werden. An diese Ausgabe würde das Zeichen in der Variable »zeichenvar« angehängt werden. Die Ausgabe würde in einer neuen Zeile (\n) enden.
Die printf-Funktion unterstützt eine Vielzahl von Features, die wir hier nicht benannt haben (u.a. können die Links-/Rechtsbündigkeit einer Ausgabe und die Genauigkeit von Ausgaben festgelegt werden). Alles Weitere erfahren Sie in jedem Grundlagenbuch zur C-Programmierung.
Wichtiger Hinweis
Bevor wir nun auf die eigentlichen Datentypen zu sprechen kommen, sollen Sie noch erfahren, dass die Größe dieser Datentypen von System zu System stark variiert und auch der ANSI-C-Standard nicht exakt definiert, welche Größe die einzelnen Datentypen haben. Wir verwenden daher die für Linux-PCs »üblichen« Werte.
int
Der wohl am einfachsten zu verstehende Datentyp ist der Integer. In C wird dieser Datentyp durch int verwendet. Eine Integer-Variable speichert eine Ganzzahl, diese Zahl kann auch 0 oder negativ sein. Laut ANSI-C-Standard ist eine Integer-Variable immer mindestens zwei Byte (also 16 Bit) groß und kann daher 216 = 65.536 verschiedene Werte speichern. Auf den meisten heutigen Rechnern (etwa Linux-PCs) sind Integer allerdings 4 Byte groß.
Gemäß dem oben erläuterten Schema kann eine Integer-Variable durch folgende C-Anweisung erzeugt und initialisiert werden:
/* Deklaration */ int i; /* ... mit Initialisierung */ int i = 123; /* Wert von 'i' ändern */ i = –123;
Listing 22.12 Eine Integer-Variable erzeugen
unsigned und signed
Datentypen können in C entweder signed oder unsigned sein. Das bedeutet zunächst nur, dass diese Variablen entweder ein Vorzeichen besitzen können oder nicht. Dabei steht signed für eine Variable mit Vorzeichen und unsigned für eine Variable ohne Vorzeichen.
Dies hat allerdings Auswirkungen auf die Wertebereiche dieser Variablen, denn schließlich müssen im Fall einer vorzeichenlosen Variable nur positive Werte in ihren Bits Platz finden. Im Falle einer vorzeichenbehafteten Variable hingegen müssen auch negative Zahlen innerhalb ihrer Bits dargestellt werden können, womit sich der Wertebereich in einen positiven inklusive 0 und in einen negativen aufteilt.
Kehren wir nun zum Beispiel der Integer-Variable zurück. In einer 16-Bit-Integer-Variable können, wie Sie wissen 65.536 verschiedene Werte gespeichert werden.
Ist diese Integer-Variable nun unsigned, so kann der gesamte Platz für die positiven Zahlen und die 0 verwendet werden. Dementsprechend ist für 65.536 – 1 = 65.535 positive Zahlen und eine 0 Platz.
Teilt sich der Wertebereich allerdings in einen positiven und in einen negativen Teil auf, so wird ein Bit benötigt, um zu signalisieren, dass es sich um eine positive bzw. negative Zahl handelt. Demnach bleiben noch 16 – 1 = 15 Bits des Wertebereichs für die positiven und die negativen Zahlen übrig. Das sind jeweils 215 = 32.768 verschiedene Werte.
Da der positive Teil auch hier wiederum die 0 mit beinhaltet, können mit einem 16-Bit-signed-Integer 32.768 – 1 = 32.767 positive Zahlen gespeichert werden. Der negative Teil hingegen, der die 0 nicht beinhaltet, reicht von –1 bis –32.768.
Deklaration
Die Deklaration von signed und unsigned Variablen ist einfach. Sie müssen nur das entsprechende Schlüsselwort mit angeben.
unsigned int a = 65635; signed int b = 32767; /* Den Wert von b verändern */ b = –32768;
Listing 22.13 Deklaration von (un)signed Integern
/* signed int */ printf("%i", a); printf("%d", a); /* unsigned int */ printf("%u", a);
Listing 22.14 Ausgabe von Integer-Werten
Hex- und Oktalwerte
In C können Sie Variablen auch Hex- und Oktalwerte zuweisen. Das sollte man allerdings nur tun, wenn man wirklich weiß, was diese Werte genau bedeuten (im Besonderen bei negativen Werten, die im 2er-Komplement dargestellt werden). Es gibt tatsächlich Fälle, bei denen Hexwerte sinnvoll erscheinen. Dazu zählt beispielsweise das Setzen von bestimmten Flags/Bits.
Hexwerte werden mit dem Präfix »0x« und Oktalwerte mit einer führenden »0« angegeben. Im folgenden Beispiel wird der Variable dreimal der gleiche Wert (42) zugewiesen.
/* Dezimale Schreibweise, wie gewohnt. */ int a = 42; /* Hexadezimale Schreibweise: Die Buchstaben können * in Klein- und Großbuchstaben angegeben werden. */ a = 0x2a; /* Oktale Schreibweise */ a = 052;
Listing 22.15 Setzen des Wertes 42
char
Eine char-Variable dient zur Speicherung eines Zeichens und ist 1 Byte groß. Mit Ausnahme einiger Sonderfälle besteht ein Byte immer aus 8 Bit. Dementsprechend können 256 verschiedene Werte in einer solchen Variable gespeichert werden.
Deklaration und Initialisierung
Einer char-Variable können Sie auf die gleiche Weise Werte zuweisen wie einer Integer-Variable, nur dass die Größe dieser Werte eben auf 8 Bit beschränkt ist.
char a = 99; printf("%c", a);
Listing 22.16 Verwendung einer char-Variablen
Was Sie in diesem Fall tun, ist, der Variablen a den Wert 99 zuweisen. Dies wird bei Ausgaben als Wert für ein ASCII-Zeichen interpretiert. <Mehr zum ASCII-Standard erfahren Sie unter: http://de.wikipedia.org/wiki/ASCII.> Die 99 steht dabei für ein kleingedrucktes »c«.
Da man wohl kaum gern auf diese Weise Zeichen zuweisen will, gibt es aber noch eine wesentlich komfortablere Schreibweise für ASCII-Zeichen.
Dabei wird das entsprechende Zeichen in Hochkommas eingebettet.
char a = 'c'; char b = '9'; char c = 'M';
Listing 22.17 char-Deklaration und Initialisierung mit Zeichen
short
Eine short-Variable hat immer eine Länge von mindestens 16 Bits. short kann wie eine Integer-Variable verwendet werden. Die Wertebereiche einer (unsigned) short-Variablen Ihres Linux-Systems bekommen Sie übrigens ganz einfach über die Datei limits.h heraus.
$ egrep 'SHRT_MAX|SHRT_MIN' /usr/include/limits.h # define SHRT_MIN (-32768) # define SHRT_MAX 32767 # define USHRT_MAX 65535
Listing 22.18 Größe einer short-Variablen ermitteln
short a = 123; a = –123; /* signed short: * Zwei Möglichkeiten für die Ausgabe: */ printf("%hd", a); printf("%hi", a); /* unsigned short: */ printf("%hu", a);
Listing 22.19 Verwendung einer short-Variablen
long
Der Datentyp long hat unter Linux laut [Love05A] auf 32-Bit-Systemen immer eine Größe von 32 Bit und auf 64-Bit-Systemen immer eine Größe von 64 Bit. <Robert Love: Linux Kernel Handbuch, Addison-Wesley, 2005. S. 408 ff.>
long a = 123; a = –123; /* signed long: * Zwei Möglichkeiten für die Ausgabe: */ printf("%ld", a); printf("%li", a); /* unsigned long: */ printf("%lu", a);
Listing 22.20 Verwendung einer long-Variablen
Gleitkomma-Datentypen
Mit den bisherigen Datentypen war es nur möglich, ganze Zahlen zu benutzen. Nun werden wir uns mit float, double und long double die sogenannten Gleitkomma-Datentypen ansehen. In einer Gleitkomma-Variablen können Zahlen mit Nachkommastellen gespeichert werden.
Dabei gibt es einige Tatsachen zu beachten:
- Für all diese Datentypen existieren keine unsigned-Varianten.
- Nachkommastellen werden nicht durch ein richtiges Komma (,) sondern durch einen Punkt (.) von dem ganzzahligen Teil einer Zahl getrennt.
- Die verschiedenen Datentypen haben nicht nur eine unterschiedliche Bitgröße, sondern auch eine unterschiedliche Genauigkeit in ihren Nachkommastellen.
Größe
Der Datentyp float ist in der Regel 4 Byte groß. Es kann allerdings vorkommen, dass eine float-Variable die Größe einer double-Variable annimmt.
Eine double-Variable hat in der Regel eine Größe von 8 Byte. Eine double-Variable hat immer mindestens die Größe einer float-Variable und maximal die Größe einer long double-Variablen.
Der Datentyp long double hat immer mindestens die Größe einer double-Variablen. In der Regel ist er 10, 12 oder 16 Byte groß.
Kurz gesagt: Größe von float <= Größe von double <= long double.
Genauigkeit
Die Genauigkeit einer Gleitkomma-Variable nimmt mit ihrer Größe zu. Dies hängt damit zusammen, dass ein bestimmter Bereich der Bits, die für die Darstellung der Nachkommastellen verwendet werden, ebenfalls anwächst. Dieser Bereich wird als Mantisse bezeichnet.
Üblicherweise haben float-Variablen eine Genauigkeit von 6 Stellen, double eine Genauigkeit von 15 Stellen, und long double hat ganze 18 Stellen Genauigkeit vorzuweisen.
float a = 0.123; double b = –17.498568; /* Werte können auch in Zehnerpotenzen angegeben werden. * Dazu wird die Schreibweise [Zahl] [e] [Exponent] * benutzt. 3e2 würde dementsprechend für 3 * 10 * 10 * stehen. */ long double c = 384.599e10; /* Die Ausgabe erfolgt auch hier auf verschiedene Weisen: */ printf("float: %f", a); printf("double: %lf", b); printf("long double: %Lf", c);
Listing 22.21 Verwendung einer Gleitkomma-Variablen
Die Ausgabe dieser Zeilen würde folgendermaßen aussehen:
float: 0.123000 double: –17.485870 long double: 3845990000000.000000
Listing 22.22 Ausgabe der Gleitkommawerte
22.1.4 Operatoren
Nun, da Sie gelernt haben, Werte für Variablen zu setzen, ist der nächste Schritt, diese Werte zu verwenden und mit ihnen zu rechnen. Zu diesem Zweck werden wir uns die Operatoren der Programmiersprache C ansehen. <Mit der Ausnahme der Vergleichsoperatoren, die erst später behandelt werden.>
Rechenoperatoren
+,-,*,/
Die einfachsten Operatoren sind (besonders für Nicht-Informatiker) Addition, Subtraktion, Multiplikation und Division.
Zuweisung
Die Zuweisung eines Wertes erfolgt, wie Sie bereits wissen, mit dem Zeichen =. Dieses Zeichen funktioniert auch dann, wenn man einer Variablen das Ergebnis einer Rechenoperation zuweisen möchte. Hier ein paar Beispiele:
int a = 4; int b = 2; int c; int d; c = a + 1; b = 3 + 4 + 5; c = a + b – 1; d = 2 * 2; c = 4 / 2;
Listing 22.23 Anwendung von Rechenoperationen
Vorrang
In C hat jeder Operator eine bestimmte Wertigkeit. Diese Wertigkeit entscheidet, welche Operatoren eines C-Ausdrucks zuerst berechnet werden und wie die weitere Reihenfolge ist. Hierfür gibt es verschiedene Regeln, auf die wir hier der Einfachheit halber nicht eingehen werden, doch so viel sei gesagt: »Punktrechnung geht vor Strichrechnung«.
Der folgende Code würde dementsprechend den Wert 10 (= 9 + (3/3)) und nicht 4 (= (9 + 3) / 3) liefern.
printf("%i\n", 9 + 3 / 3);
Listing 22.24 Punkt vor Strich
Doch was passiert, wenn zwei Punktrechnungen gleichzeitig verwendet werden? In diesem Fall gilt »rechts vor links«, was bedeutet, dass der Ausruck von der rechten zur linken Seite hin ausgewertet wird.
printf("%i\n", 9 * 3 / 3);
Listing 22.25 Rechts vor links
In diesem Fall wird also zunächst 3 durch 3 geteilt (was 1 ergibt). Das Ergebnis 1 wird anschließend mit der 9 multipliziert. Damit ist das Ergebnis ebenfalls 9.
Klammerung
Wenn Sie die Rechenreihenfolge selbst bestimmen möchten, dann verwenden Sie wie in der Schule Klammern. Der obere Ausdruck könnte beispielsweise durch Einklammern der Rechenoperation 9*3 (= 27) den Wert 9 (= 27 / 3) liefern.
printf("%i\n", (9 * 3) / 3);
Listing 22.26 Klammerung
Nachkommastellen
Allerdings ist zu beachten, dass diese Rechenoperationen nicht immer zum beabsichtigten Ergebnis führen. Beispielsweise können Integer-Variablen nur ganze Werte speichern. Was aber liefert dann eine Zuweisung von 5/2 an einen Integer? Die Antwort ist: 2. Das liegt daran, dass die Nachkommastellen abgeschnitten werden. Möchten Sie Komma-Werte im Ergebnis, so müssen Sie eine Gleitkommavariable verwenden.
float a = 5, b = 2; float c; c = a/b; printf("%f\n", c);
Listing 22.27 Rechnen mit Kommastellen
Die Ausgabe dieses Codes liefert 2.500000.
Typen-Mix
Mischt man allerdings mehrere Datentypen, so wird es leicht problematisch. Hier kann es zu Speicherüberläufen, Problemen mit (nicht vorhandenen) Vorzeichen und zum Abschneiden von Kommastellen kommen. Auf diese Probleme können wir in diesem Rahmen leider nicht eingehen. Eine relativ sichere Vorgehensweise ist allerdings, keine Datentypen zu mixen.
Weitere Rechenoperatoren
Modulo
Eine in der Informatik sehr wichtige Rechenoperation ist das Modulo-Rechnen. Dahinter verbirgt sich nichts anderes, als dass das Ergebnis dieser Rechnung der Rest ist, der bei einer Division übrig bleibt. Teilen Sie beispielsweise 5 durch 2, dann bleibt ein Rest von 1 übrig.
Der Modulo-Operator ist ein Prozentzeichen (%).
int a, b; /* a wird 0, da kein Rest bleibt */ a = 10 % 2; /* a wird 4 */ a = 9 % 5;
Listing 22.28 Modulo
++/–-
Nun kommen wir zu zwei sehr beliebten Operatoren: den doppelten Plus- und Minuszeichen. Fast jede Programmiersprache kennt diese Operatoren und das, was sie tun, ist sehr einfach zu verstehen: Sie inkrementieren (erhöhen) bzw. dekrementieren (verringern) den Wert einer Variablen um 1.
int a = 10; a ++; /* a wird 11 */ a --; /* a wird 10 */ a --; /* a wird 9 */
Listing 22.29 Inkrementieren und Dekrementieren
In C unterscheidet man zwischen Pre- und Post-Inkrementierung bzw. -Dekrementierung. Der Unterschied besteht darin, ob der Operator vor oder hinter eine Variable geschrieben wird. Dies kann sich auf eine Rechnung auswirken, da hierbei entschieden wird, ob eine Variable vor ihrer Verwendung in-/dekrementiert wird oder ob sie erst nach einer Rechnung in-/dekrementiert wird.
int a, b, c; a = 10; ++a; /* a wird 11 */ --a; /* a wird 10 */ /* Beispiel für Pre-Inkrementierung */ a = 10; b = ++a; /* b = 1 + a = 11; a = 11; */ /* Beispiel für Post-Inkrementierung */ a = 10; c = a++; /* c = a = 10; a = 11; */
Listing 22.30 Vorher oder nachher?
Im Beispiel der Pre-Inkrementierung bekommt b den Wert 11, da zuerst a inkrementiert wird (a = 11) und dieser Wert dann c zugewiesen wird.
Im Beispiel der Post-Inkrementierung bekommt c den Wert 10. Erst danach wird a imkremeniert (womit a auch hier den Wert 11 bekommt). Wie Sie sehen, führen beide Rechenanweisungen durchaus zu unterschiedlichen Ergebnissen.
Verkürzte Schreibweisen
Ein weiteres sehr beliebtes Feature der Programmiersprache spart Schreibarbeit und ist mit fast allen Operatoren anwendbar. Es wird dabei eine Zuweisung der folgenden Form vereinfacht.
VarA = VarA [Operator] VarB
In C können Sie anstelle dieser Schreibweise nämlich auch diese verwenden:
VarA [Operator]= VarB
Klingt kompliziert? Ist es aber nicht. Nach diesem Beispiel werden Sie es ganz locker verstanden haben. Es soll die folgende Rechenoperation vereinfacht werden:
int a = 10, b = 2; b = b + a;
Listing 22.31 Vor der Vereinfachung
Nun wird das Additionszeichen vor das Gleichheitszeichen gezogen und die zweite Verwendung von Variable b entfernt:
int a = 10, b = 2; b += a;
Listing 22.32 Nach der Vereinfachung
Hier sehen Sie noch einige weitere Beispiele für andere Rechenoperationen, die auf diese Weise vereinfacht werden können. <Wir werden gleich noch weitere Operatoren kennenlernen, jedoch beschränken wir uns an dieser Stelle auf die uns bekannten Operatoren.>
/* Lange Schreibweise */ a = a + b; a = a * b; a = a – b; a = a / b; a = a % b; /* Kurze Schreibweise */ a += b; a *= b; a -= b; a /= b; a %= b;
Listing 22.33 Nach der Vereinfachung
Bitweise Operatoren
Die nächste große Klasse an Operatoren, die in C zur Verfügung stehen, sind die bitweise angewandten Operatoren. Um diese Operatoren anzuwenden, muss man über die Darstellung der Variablenwerte im dualen Zahlensystem (also binär mit Nullen und Einsen) Bescheid wissen. Dieses Thema würde locker den Rahmen dieses Abschnitts sprengen und kann daher leider auch nicht behandelt werden. In der Wikipedia und in C-Büchern finden Sie allerdings sehr vernünftige und verständliche Erklärungen. <Bei Problemen leihen Sie sich »Mathematik für Informatiker« von Brill oder ein anderes Buch zum Thema oder Informatiker-Bücher für das Grundstudium aus einer Bibliothek aus.>
Shiften
Die sogenannten Shift-Operatoren verschieben die Bits in einer Variable nach rechts beziehungsweise nach links. Nehmen wir an, in einer 8-Bit-Integer-Variable steht der Wert 4 (dezimal). Binär wird dieser Wert als 00000100 dargestellt. Wird dieser Wert nun um eine Stelle nach links verschoben, dann steht anschließend der Wert 00001000 (also 8) in der Variable. Wird er um 1 nach rechts verschoben, steht anschließend 00000010 (also 2) in der Variable. Die Operatoren hierfür sind doppelte Größer- bzw. Kleiner-als-Zeichen.
int a = 4; int b, c; /* Den Wert von a um eine Stelle nach rechts shiften */ b = a >> 1; /* Den Wert von a um zwei Stellen nach links shiften */ c = a << 2;
Listing 22.34 Shiften
Weitere Operatoren
Es gibt noch weitere (und genauso wichtige) Operatoren, die bitweise angewandt werden. Dazu zählen das »logische Und« (&, im Folgenden »UND«), das »logische Oder« (|, im Folgenden »ODER«) und das »exklusive ODER« (^, im Folgenden XOR). Des Weiteren gibt es noch das Einerkomplement (~).
UND
Bei einem UND zwischen zwei Variablen wird geprüft, welche Bits in beiden Variablen gesetzt sind. Das Ergebnis der Operation sind nur die Bits, die in jeder der beiden Variablen gesetzt sind. Würde beispielsweise der Wert 6 (110) mit dem Wert 5 (101) UND-verknüpft werden, so wäre das Ergebnis 4 (100), da nur das 4er-Bit in beiden Werten vorkommt. Geschrieben wird eine UND-Verknüpfung mit einem kaufmännischen &.
110 = 6 & 101 = 5 ----- 100 = 4
Listing 22.35 Beispiel einer UND-Verknüpfung
int a; int x = 6, y = 4; a = x & y;
Listing 22.36 Beispiel einer UND-Verknüpfung
ODER
Die ODER-Verknüpfung ist der UND-Verknüpfung sehr ähnlich. Der Unterschied besteht darin, dass alle Bits im Ergebnis landen, die entweder in einem der Werte oder in beiden vorkommen. Auf das obige Beispiel mit den Werten 6 (110) und 5 (101) angewandt, würde das Ergebnis 7 (111) sein. Geschrieben wird ein »logisches ODER« durch ein Pipe-Zeichen (|).
110 = 6 | 101 = 5 ----- 111 = 7
Listing 22.37 Beispiel einer ODER-Verknüpfung
int a; int x = 6, y = 4; a = x | y;
Listing 22.38 Beispiel einer ODER-Verknüpfung
XOR
Das »exklusive ODER« verhält sich wiederum ähnlich wie das »logische ODER«. Es landen alle Bits im Ergebnis, die entweder im ersten oder im zweiten Wert vorhanden sind. Allerdings landen nicht die Bits im Ergebnis, die in beiden Werten gesetzt sind. Würde 6 (110) mit 5 (101) XOR-verknüpft werden, dann wäre das Ergebnis 3 (011). Der XOR-Operator wird durch ein Dach-Zeichen (^) verwendet.
110 = 6 ^ 101 = 5 ----- 011 = 3
Listing 22.39 Beispiel einer XOR-Verknüpfung
int a; int x = 6, y = 4; a = x ^ y;
Listing 22.40 Beispiel einer XOR-Verknüpfung
Einerkomplement
Es bleibt nun noch das »Einerkomplement«. Hierbei werden die Bits eines Wertes negiert, das heißt umgekehrt. Aus einem 1er-Bit wird ein 0er-Bit, und aus einem 0er-Bit wird ein 1er-Bit.
Das Einerkomplement wird auf einen einzigen Wert angewandt und durch ein Tilde-Zeichen (~) benutzt.
Würden wir den Operator auf den Wert 6 (110) anwenden, so wäre das Ergebnis 1 (001).
~ 110 = 6 ----- 001 = 1
Listing 22.41 Beispiel eines Einerkomplements
Allerdings gibt es in C etwas zu beachten, das wir bisher nicht erwähnt haben: Nehmen wir an, Sie verwenden eine 32 Bit große Integer-Variable in der der Wert 6 gespeichert ist. In diesem Fall wird das Einerkomplement nicht 1 ergeben. Das liegt daran, dass vor den ersten 3 Bits (110) noch 29 weitere 0-Bits stehen, die durch die Rechenoperation zu einer 1 werden. Das Ergebnis wäre dann eine sehr große Zahl (unsigned int) beziehungsweise eine sehr kleine negative Zahl ((signed) int):
~ 00000000000000000000000000000110 = 6 ---------------------------------- 11111111111111111111111111111001 = 4.294.967.289
Listing 22.42 Beispiel eines Einerkomplements
int a; a = ~ 6;
Listing 22.43 Beispiel eines Einerkomplements
Der sizeof-Operator
Zum Schluss folgt noch eine sehr praktische C-Funktionalität: der sizeof-Operator. Dieser Operator gibt die Zahl der Bytes zurück, die eine Variable, auf die er angewandt wird, für sich beansprucht. Die Anzahl der Bytes, die zurückgegeben werden, ist daher niemals negativ und auch keine Gleitkommazahl.
#include <stdio.h> int main() { char q; short r; int s; long t; float u; double v; long double w; printf("Groesse von char: %i\n", sizeof(q)); printf("Groesse von short: %i\n", sizeof(r)); printf("Groesse von int: %i\n", sizeof(s)); printf("Groesse von long: %i\n", sizeof(t)); printf("Groesse von float: %i\n", sizeof(u)); printf("Groesse von double: %i\n", sizeof(v)); printf("Groesse von long double: %i\n", sizeof(w)); return 0; }
Listing 22.44 Beispielanwendung des sizeof-Operators
Dieses Programm liefert auf einem üblichen 32-Bit-x86-Linux-PC die folgende Ausgabe:
Groesse von char: 1 Groesse von short: 2 Groesse von int: 4 Groesse von long: 4 Groesse von float: 4 Groesse von double: 8 Groesse von long double: 12
Listing 22.45 Ausgabe des Programms
Übrigens kann der sizeof-Operator auch direkt auf einen Datentyp angewandt werden. Eine Anweisung wie sizeof(long) ist also gültig.
Übersicht der Operatoren
Zur Zusammenfassung folgt hier noch einmal eine Übersicht über die arithmetischen Operatoren.
Operator | Beispiel | Beschreibung |
+ |
x = x + 1 |
Addition |
- |
x = x – 1 |
Subtraktion |
* |
x = y * z |
Multiplikation |
/ |
z = x / y |
Division |
% |
m = x % y |
Modulo (Restwert bei Division) |
++ |
x++ |
Inkrement |
-- |
x- |
Dekrement |
& |
x = y & z |
UND |
| |
x = y | z |
ODER |
^ |
x = y ^ z |
XOR |
~ |
x = ~ z |
Negierung |
» |
x = y >> 2 |
rechts shiften |
» |
x = y << 2 |
links shiften |
sizeof |
x = sizeof(int) |
»Größe von« |
22.1.5 Bedingte Anweisungen
Ein äußerst wichtiges Element der Programmierung sind bedingte Anweisungen. Sie kennen bedingte Anweisungen bereits aus der Shellskript Programmierung. In der Shell hießen die zugehörigen Befehle if und case. Diese Namen sind in den meisten Programmiersprachen sehr ähnlich – so ist es auch mit C.
Hier noch einmal zur Erinnerung: Bei bedingten Anweisungen wird zunächst geprüft, ob eine »Bedingung« erfüllt ist (etwa ob der Wert der Variablen »anzahl« größer 1000 ist). Ist die Bedingung (nicht) erfüllt, dann wird eine bestimmte Anweisung (nicht) ausgeführt.
Vergleichsoperatoren
Bevor wir uns die einzelnen Anweisungen ansehen, soll noch ein Blick auf die Vergleichsoperatoren geworfen werden, die C kennt.
Positive Werte werden in C als »erfüllte« Bedingungen angesehen (man spricht auch von »wahren« oder »true-Bedingungen«). Werte, die negativ oder 0 sind, werden hingegen als »nicht erfüllt« (man spricht auch von »falschen« oder »false-Bedingungen«) bezeichnet.
Würde die Variable a als Vergleichstest verwendet, so wäre die Bedingung dann erfüllt, wenn in a ein positiver Wert steht. In Spezialfällen, bei denen signed- mit unsigned-Werten verglichen werden, kann es allerdings zu Problemen kommen.
Mehr zu diesem Thema erfahren Sie in vernünftigen C-Büchern und in unserem Buch »Praxisbuch Netzwerksicherheit« im Kapitel »Sichere Software entwickeln«.
Nehmen wir an, wir betreiben ein Verkaufssystem. Sobald ein Kunde für mehr als 100 EUR bestellt, sollen ihm die Versandkosten erlassen werden. Würde der Gesamtwert des Einkaufs in der Variable »wert« stehen, dann könnte man prüfen, ob »wert« größer oder gleich 100 EUR wäre. Die Versandkosten werden erlassen (etwa durch Setzen der Variable »vkosten« auf 0), wenn diese Bedingung erfüllt ist.
Operator | Beispiel | Beschreibung |
== |
a == 1 |
Gleichheit: Die Bedingung ist erfüllt, wenn die Werte auf beiden Seiten gleich sind. |
!= |
a != 1 |
Ungleichheit: Die Bedingung ist erfüllt, wenn die Werte sich auf beiden Seiten unterscheiden. |
> |
a > 1 |
Größer: Die Bedingung ist erfüllt, wenn der linke Wert größer als der rechten ist. |
>= |
a >= 1 |
Größer-Gleich: Die Bedingung ist erfüllt, wenn der linke Wert größer oder gleich dem rechten ist. |
< |
a < 1 |
Kleiner: Die Bedingung ist erfüllt, wenn der linke Wert kleiner dem rechten ist. |
<= |
a <= 1 |
Kleiner-Gleich: Die Bedingung ist erfüllt, wenn der linke Wert kleiner oder gleich als der rechte ist. |
&& |
a && 1 |
Und: Die Bedingung ist erfüllt, wenn sowohl die linke als auch die rechte Bedingung erfüllt ist. |
|| |
a || a |
Oder: Die Bedingung ist erfüllt, wenn die rechte oder die linke oder beide Bedingungen erfüllt sind. |
! |
! a |
Negation: Die Bedingung ist erfüllt, wenn die rechts von ihr stehende Bedingung nicht erfüllt ist. |
Die if-Anweisung
Die verständlichste Möglichkeit, eine bedingte Anweisung zu formulieren, ist wohl die if-Anweisung. Ihr Aufbau ist dabei dem Aufbau der if-Anweisung der Shell sehr ähnlich:
if ( Bedingung) { Anweisung(en) } else if ( Nebenbedingung ) { Anweisung(en) } else if ( Weitere Nebenbedingung ) { Anweisung(en) } ... [ weitere Nebenbedingungen ] ... } else { Anweisung(en) }
Listing 22.46 Aufbau einer if-Anweisung
Ist die Bedingung des if-Blocks erfüllt, so werden die entsprechenden Anweisungen ausgeführt. Dabei können die geschweiften Klammern weggelassen werden, wenn nur eine einzige Anweisung ausgeführt werden soll. Diese beiden Anweisungen sind also gleich:
if ( 1 ) { printf("True!"); } if ( 1 ) printf("True!");
Listing 22.47 Nur eine Anweisung in if
else if
Ist die eigentliche if-Bedingung nicht erfüllt, dann gibt es die Möglichkeit, weitere Bedingungen (else if) abzufragen. Diese Bedingungen werden nur überprüft, wenn die erste Bedingung nicht erfüllt ist. Sobald eine dieser weiteren Bedingungen erfüllt ist, werden die entsprechenden Anweisungen ausgeführt, und es wird keine weitere Bedingung, die noch folgt, überprüft.
Versuchen wir auf diese Weise einmal die Variable »anzahl« auf drei verschiedene Werte zu überprüfen.
if ( a < 1000 ) { printf("a ist kleiner als 1000"); } else if ( a == 2948 ) { printf("a ist 2948"); } else if ( a == 494859) { printf("a ist 494859"); }
Listing 22.48 Testen auf verschiedene Werte
else
Was passiert aber, nur dann, falls keine dieser Bedingungen erfüllt ist, eine bestimmte Aktion ausgeführt werden soll? Nehmen wir an, dass »a« den Wert 123 zugewiesen bekommen soll, wenn a weder kleiner als 1000 noch eine der anderen Zahlen ist.
Für diesen Fall gibt es die else-Anweisung. Die Anweisungen in einem else-Anweisungsblock werden nur dann ausgeführt, wenn alle anderen Bedingungen nicht erfüllt sind.
if ( a < 1000 ) { printf("a ist kleiner als 1000"); } else if ( a == 2948 ) { printf("a ist 2948"); } else if ( a == 494859) { printf("a ist 494859"); } else { printf("a hat einen anderen Wert"); }
Listing 22.49 Testen auf verschiedene Werte
Mehrere Bedingungen, eine Anweisung
Möchte man die gleiche Anweisung bei mehreren verschiedenen Bedingungen ausführen, dann ist auch das in C kein Problem. Nehmen wir an, es soll der Text »Aktien kaufen« ausgegeben werden, wenn »a« entweder kleiner 1000 ist oder größer als 2000 ist. Mit der Oder-Anweisung aus Tabelle 2.2.2 ist dies kein Problem:
if ( a < 1000 || a > 2000) { printf("Aktien kaufen"); }
Listing 22.50 Testen auf verschiedene Werte
Diese Bedingung wäre übrigens auch erfüllt, wenn »a« gleichzeitig kleiner als 1000 und größer als 2000 wäre, was aber nicht möglich ist. Würde man aber prüfen, ob »a« kleiner 1000 und »b« größer 2000 ist, dann könnten beide Bedingungen gleichzeitig erfüllt sein.
Klammerung
Mit Klammern kommt man allerdings noch einen Schritt weiter. Möchte man etwa Prüfen ob die obige Bedingung erfüllt ist, aber den Text nur ausgeben, wenn die Variable »t« kleiner als 10 ist, dann klammert man die Oder-Bedingung ein. Warum das so ist, zeigt das Listing.
/* Ohne Klammern: Es ist nicht klar, ob entweder a > 2000 * UND t < 10 sein soll ODER a < 1000 sein soll. Oder aber, * ob (wie es eigentlich gedacht ist) a < 1000 ODER > 2000 * sein soll UND zudem t < 10 sein soll. */ if ( a < 1000 || a > 2000 && t < 10) { printf("Aktien kaufen"); } /* Mit Klammern: Es ist klar: Sowohl die Bedingung in der * Klammer als auch t < 10 müssen erfüllt sein. */ if ( ( a < 1000 || a > 2000 ) && t < 10) { printf("Aktien kaufen"); }
Listing 22.51 Ein Beispiel zur Klammerung
Zur Erinnerung: Bei einer einzigen auszuführenden Anweisung hätte dieses Beispiel auch ohne die Klammern geschrieben werden können.
Klammerung
Mit Klammern kommt man allerdings noch einen Schritt weiter. Möchte man etwa prüfen, ob die obige Bedingung erfüllt ist, aber den Text nur ausgeben, wenn die Variable »t« kleiner als 10 ist, dann klammert man die Oder-Bedingung ein. Warum das so ist, zeigt das Listing.
if ( ( a < 1000 || a > 2000 ) && t < 10) printf("Aktien kaufen");
Listing 22.52 Ein Beispiel zur Klammerung
Die switch-Anweisung
In der bash gab es neben der if-Anweisung noch die case-Anweisung. Diese gibt es (nur unter anderem Namen) auch in C. In C heißt diese Anweisung switch. Ihr übergibt man einen Wert (direkt oder durch eine Variable) und kann anschließend die Vergleichswerte und die zugehörigen Anweisungen aufführen.
switch ( Wert ) { case Testwert1: Anweisung(en) [break;] case Testwert2: Anweisung(en) [break;] ... ... default: Anweisung(en) [break;] }
Listing 22.53 Schema einer switch-Anweisung
Hierbei wird geprüft, ob der Wert dem Testwert 1 oder Testwert 2 (oder weiteren Testwerten) entspricht. Jeweils werden die entsprechenden Bedingungen ausgeführt. Ist keine dieser Testwerte gleich dem übergebenen Wert, dann werden die Anweisungen des default-Blocks ausgeführt. Ist kein default-Block vorhanden, so wird gar keine Anweisung ausgeführt.
Ein default-Fall muss übrigens nicht angegeben werden. Eine switch-Anweisung kann entweder einen oder mehrere casees, eine default-Anweisung, beides oder nichts enthalten. Im Falle einer leeren switch-Anweisung werden natürlich auch keine Werte überprüft und dementsprechend auch keine Anweisungen ausgeführt.
break
Mit der break-Anweisung wird erzwungen, dass keine weiteren Anweisungen mehr ausgeführt werden und dass das switch-Statement verlassen wird. Würden Sie eine break-Anweisung am Ende der Anweisungen eines case-Bereiches vergessen, so würden die folgenden Anweisungen (egal welcher Bedingung) ebenfalls ausgeführt werden, bis entweder eine break-Anweisung auftritt oder das Ende der switch-Anweisung erreicht ist.
int q = 99; switch ( q ) { case 10: q = 12; break; case 99: q = 102; /* An dieser Stelle fehlt ein 'break' */ case 100: q = 112; break; }
Listing 22.54 Beispiel für break
Der ?-Operator
C kennt noch eine weitere Möglichkeit, eine bedingte Anweisung zu formulieren: den Fragezeichen-Operator. Dieser gibt im Gegensatz zu den anderen Varianten einen Wert zurück.
( Bedingung ? Anweisung bei erfüllter Bedingung : Anweisung bei nicht erfüllter Bedingung )
Listing 22.55 Aufbau einer ?-Anweisung
Nehmen wir an, die Variable »a« soll auf den Wert 77 gesetzt werden, wenn die Variable »q« den Wert 99 enthält. Ist dem nicht so, soll »a« den Wert 2 verpasst bekommen.
a = ( q == 99 ? 77 : 2 )
Listing 22.56 Beispiel zum ?-Operator
Sie können durch diese Anweisung natürlich auch Zeichen für char-Variablen, Gleitkomma-Werte und sogar ganze Zeichenketten
(sogenannte Strings) zurückgeben lassen. Hier ein Beispiel für eine Textausgabe: Im Falle einer Begrüßung (das bedeutet, dass die Variable »beg« den Wert 1 hat) soll printf() »Hallo«, andernfalls »Tschuess« ausgeben. <Der Formatstring-Parameter %s besagt übrigens, dass es sich bei der Ausgabe nicht um eine Zahl oder ein einzelnes Zeichen, sondern um eine Zeichenkette handelt. Dazu später mehr.>
printf("%s\n", ( beg == 1 ? "Hallo" : "Tschuess" ) );
Listing 22.57 Zeichenketten
22.1.6 Schleifen
Sie haben noch nicht aufgegeben? Das ist schön! Jetzt, wo Sie bedingte Anweisungen und Datentypen von Variablen kennen, können wir ein weiteres Thema behandeln: Schleifen. Schleifen sind nach all dem, was Sie bisher wissen, sehr einfach zu verstehen, und ihr Nutzen ist riesig.
Im Prinzip verhält es sich hierbei auch wieder ähnlich wie bei der Shellskriptprogrammierung mit der bash: Auch in C gibt es eine while- und eine for-Schleife.
Zur Erinnerung: Eine Schleife führt bedingte Anweisungen so lange aus, wie die Bedingung, die ihr übergeben wurde, erfüllt ist.
Die while-Schleife
Die einfachste Schleife ist die while-Schleife. Ihr Aufbau ist dem der if-Anweisung sehr ähnlich:
while ( Bedingung ) { Anweisung(en) }
Listing 22.58 Aufbau einer while-Schleife
Nehmen wir an, es soll zehnmal in Folge der Text »Hallo Welt« ausgegeben werden. Sie könnten nun zehnmal eine printf-Anweisung untereinanderschreiben oder eine lange Zeichenkette mit vielen Newlines und vielen »Hallo Welt« übergeben. Doch viel schöner (und platzsparender) ist es, eine Schleife hierfür zu verwenden.
int i = 10; while ( i > 0 ) { printf("Hallo Welt"); i = i – 1; }
Listing 22.59 10x Hallo Welt!
Am Ende jedes Schleifendurchlaufs haben wir einfach die Variable »i« dekrementiert. Die Bedingung der Schleife ist somit genau zehnmal erfüllt. Sobald »i« gleich 0 ist, ist die Bedingung nicht mehr erfüllt, und die Schleife wäre beendet.
Die Überprüfung auf die Erfüllung der Bedingung findet immer nur statt, nachdem der gesamte Anweisungsblock ausgeführt wurde. Auf das obige Beispiel angewandt bedeutet dies etwa, dass Sie die Anweisung, »i« zu dekrementieren, auch vor die Ausgabe stellen können.
int i = 10; while ( i > 0 ) { i = i – 1; printf("Hallo Welt"); }
Listing 22.60 Eine weitere Variante
Richtig praktisch werden Schleifen aber erst, wenn man mit verschiedenen Variablenwerten arbeitet. Möchte man etwa die Zahlen von Eins bis Zehntausend ausgeben, dann geht dies mit genauso wenig Code-Zeilen, als wenn man die Zahlen von Eins bis Zwei oder bis Hundert oder von Eins bis Zehn Millionen ausgeben würde.
int i = 1; while ( i <= 1000000 ) { printf("%i\n", i); i++; }
Listing 22.61 Bis 1.000.000 zählen
Natürlich sind auch komplexere Bedingungen sowie Unterschleifen möglich. Möchte man etwa zehn Zahlen pro Zeile ausgeben, dann ist auch das kein Problem.
Hier ein Beispiel: Es werden zehn Zahlen pro Zeile ausgegeben, und jeweils steigt die Zahl, die ausgegeben wird, an.
Nach zehn Zahlen wird also ein Newline-Zeichen eingefügt. Nach zehn Zeilen (das bedeutet, dass i % 10 den Wert 0 ergibt) wird eine Trennlinie ausgegeben.
#include <stdio.h> int main() { int i = 0; int k; int wert; while ( i <= 100 ) { k = 0; while ( k < 10 ) { wert = (i * 10) + k; printf("%i ", wert); k++; } printf("\n"); if ( (i % 10) == 0) { printf("---------------------------n"); } i++; } return 0; }
Wie erwartet gibt das Programm 10x10er-Blöcke von Zahlen aus:
$ gcc -o zahlen zahlen.c $ ./zahlen 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 ---------------------------- 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 ---------------------------- 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 ... ... 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 ---------------------------- 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 ----------------------------
Listing 22.62 Die Ausgabe des Programms (gekürzt)
Die do-while-Schleife
Eine Abwandlung der while-Schleife ist die do-while-Schleife. Bei dieser Schleife wird der Anweisungsblock immer ein erstes Mal ausgeführt. Erst danach wird er nur noch ausgeführt, wenn die Bedingung erfüllt ist.
do { Anweisung(en) } while ( Bedingung );
Listing 22.63 Aufbau der do-while-Schleife
Vor Kurzem hatten wir mit einer while-Schleife bis 1.000.000 gezählt. In einer do-while-Schleife müsste in diesem Fall keine große Veränderung stattfinden:
/* Die while-Schleife: */ int i = 1; while ( i <= 1000000 ) { printf("%i\n", i); i++; } /* Die do-while-Schleife: */ int i = 1; do { printf("%i\n", i); i++; } while ( i <= 1000000);
Listing 22.64 Bis 1.000.000 zählen
Der Unterschied ist allerdings der, dass die erste Ausgabe auch dann erfolgt, wenn »i« größer als 1.000.000 ist.
Die for-Schleife
Wie Sie bereits sahen, wird in fast jeder Schleife gezählt. In der for-Schleife ist genau das kein Problem. Sie ist dazu gedacht, Werte während der Schleifendurchläufe zu verändern, und ist daher sehr beliebt.
for ( Initialisierung ; Bedingung; Werte-Veränderung ) { Anweisung(en) }
Listing 22.65 Aufbau einer for-Schleife
Bei der Initialisierung werden die Werte von Variablen für den Schleifendurchlauf festgelegt. Der Bedingungsteil ist wie bei anderen Schleifen zu verwenden, und die Werte-Veränderung kann für jegliche Anpassung (etwa Inkrementierung) von Variablen verwendet werden.
Im Gegensatz zu C++ können in C keine Variablen im Initialisierungsbereich einer for-Schleife angelegt werden, auch wenn einige Compiler (jedoch nicht der gcc) das erlauben.
Unser Lieblingsbeispiel ließe sich folgendermaßen auf die for-Schleife übertragen:
int i; for ( i = 1 ; i <= 1000000 ; i++ ) { printf("%i\n", i); }
Listing 22.66 Bis 1.000.000 zählen
In einer for-Schleife können sowohl der Initialisierungsteil als auch die beiden anderen Teile fehlen. Eine Variable könnte beispielsweise vorher initialisiert werden (wodurch der Initialisierungsteil überflüssig wäre).
Endlosschleifen
Manchmal möchte man, dass eine Schleife unendlich lange läuft (etwa in Prozessen von Netzwerkservern, die 24 Stunden am Tag in einer Schleife Verbindungen annehmen und an Kindprozesse weitergeben). Mit jeder C-Schleife ist dies möglich. Dazu wird einfach eine Bedingung angegeben, die immer wahr ist.
while ( 1 ) { Anweisung(en) } do { Anweisung(en) } while ( 1 ); for ( ; 1 ; ) { Anweisung(en) }
Listing 22.67 Endlosschleifen
for(;;)
In der for-Schleife können Sie auch die Bedingung für diesen Fall weglassen.
for (;;) { Anweisung(en) }
Listing 22.68 Endlose for-Schleife
goto
Sprünge sind eine sehr unbeliebte Möglichkeit, Schleifen zu programmieren, die eigentlich nur mit »Jumps« (auch »Branches« genannt) in Assembler ihren Nutzen finden sollten. In höheren Programmiersprachen wie C gelten Sprünge als ein schlechter Programmierstil und sollten nicht verwendet werden. Nichtsdestotrotz gibt es sie.
Für einen Sprung wird zunächst ein »Label« definiert. In C geht dies, indem man den Namen des Labels, gefolgt von einem Doppelpunkt, schreibt. Zu diesem Label wird »gesprungen«, indem man es als Sprungziel für die Anweisung goto benutzt.
Sprungziel: Anweisung(en) goto Sprungziel;
Listing 22.69 Aufbau einer goto-Anweisung
Auf diese Weise lassen sich natürlich auch ganz einfach Endlosschleifen programmieren. In einigen Quelltexten (etwa dem OpenBSD-Kernel) werden goto-Anweisungen benutzt, um frühzeitig an den Endteil einer Funktion zu springen. Dies kann unter Umständen die Performance verbessern, sollte aber trotzdem vermieden werden.
/* Eine Endlosschleife mit goto */ endlos: printf("Unendlich oft wird diese Zeile ausgegeben.\n"); goto endlos; /* Bis 1000000 zählen mit goto */ int i = 1; nochmal: printf("%i\n", i); i++; if ( i <= 1000000 ) goto nochmal;
Listing 22.70 Endlosschleife und Zähler
22.1.7 Funktionen
Möchte man bestimmte Anweisungen mehrmals ausführen, dann verwendet man in der Regel eine Schleife. Doch was ist, wenn man die gleichen Anweisungen (eventuell mit unterschiedlichen Werten) an verschiedenen Stellen des Programms mehrmals ausführen möchte? Dafür gibt es Funktionen. Eine Funktion führt eine Reihe von Anweisungen aus, indem man sie anstelle der Anweisungen aufruft.
Datentyp Funktionsname ( [Parameterliste] ) { Anweisung(en) }
Listing 22.71 Aufbau einer Funktion
In C können Funktionen Werte übergeben bekommen und zurückgeben. Beginnen wir aber mit einer sehr einfachen Funktion.
void sage_hallo() { printf("Hallo"); }
Listing 22.72 Eine einfache C-Funktion
Diese Funktion gibt nichts zurück (void) und hat den Namen »sage_hallo«. Ihre Parameterliste ist leer, und sie führt auch nur eine Anweisung (die Ausgabe von »Hallo«) aus.
Wie man eine Funktion aufruft, wissen Sie bereits durch die vielfach verwendete Funktion »printf«.
sage_hallo();
Listing 22.73 Aufruf der Funktion
Funktionsparameter
Nun gehen wir einen Schritt weiter und lernen, wie man Funktionen Parameter übergibt (auch diese Prinzipien wurden bereits in den Kapiteln zur Shell besprochen). Dazu implementieren wir eine Funktion, die die Summe aus zwei übergebenen Parametern berechnet.
/* Berechne die Summe aus a und b */ void summe(short a, short b) { /* Eine int-Variable ist groß genug, um das * Ergebnis zweier short-Variablen aufzunehmen. */ int ergebnis = a + b; /* Ausgabe der Integer-Variable 'ergebnis' */ printf("%i\n", ergebnis); }
Listing 22.74 Funktion zur Berechnung einer Summe
Diese Funktion wird aufgerufen, indem man ihr zwei entsprechende Werte übergibt:
summe(777, 333);
Listing 22.75 Aufruf von summe()
Rückgabewerte
Diese Funktion kann aber immer noch stark verbessert werden. Wie wäre es etwa, wenn wir das Ergebnis der Rechenoperation weiter verwenden wollen, um damit etwas anderes zu berechnen? Dies ist mit der obigen Funktion nicht möglich, lässt sich aber durch Rückgabewerte erledigen.
Wenn eine Funktion einen Wert zurückgibt, dann muss zunächst der Datentyp dieses Rückgabewertes angegeben werden. void bedeutet, wie Sie bereits wissen, dass kein Wert zurückgegeben wird.
Da das Ergebnis ein Integer-Wert ist, können wir in diesem Fall int zurückgeben. Die Rückgabe eines Wertes wird in einer Funktion mit dem Befehl return [Wert oder Variable] erledigt.
int summe(short a, short b) { int ergebnis = a + b; return ergebnis; }
Listing 22.76 Rückgabe des Ergebnisses
Diese Funktion nun zu benutzen ist ebenfalls recht einfach. Sie kann direkt an eine Variable zugewiesen werden:
int x; /* Den Rückgabewert in 'x' speichern: */ x = summe(485, 3921);
Listing 22.77 Benutzung von Rückgabewerten
»Stattdessen kann ich aber auch einfach x = 485 + 3921 schreiben!«, werden Sie nun sagen.
Das ist richtig. Dieses einfache Beispiel sollte aber auch nur zeigen, wie leicht es ist, Funktionen in C zu benutzen. Hier ein etwas nützlicheres Beispiel zur Berechnung der Fakultät einer Zahl:
#include <stdio.h> long fac(short n) { long ergebnis; ergebnis = n; for (n = n – 1; n > 0; n--) { ergebnis *= n; } return ergebnis; } int main() { short val = 7; long ret; ret = fac(7); printf("Die Fakultaet von %hi ist %ld\n", val, ret); return 0; }
Listing 22.78 Fakultät berechnen
22.1.8 Präprozessor-Direktiven
Bevor ein C-Compiler den eigentlichen ausführbaren Code eines Programmes erzeugt, wird der Quellcode auf syntaktische Fehler und auf Präprozessor-Direktiven (engl. preprocessor directives) hin untersucht. <Ein Compiler erledigt noch einige weitere Aufgaben, beispielsweise erstellt er Objectfiles, ruft den Linker auf, erstellt Assembler-Code aus C-Code, ...>
Diese Präprozessor-Direktiven werden nicht als C-Anweisungen interpretiert, sondern sind direkte Anweisungen an den Compiler. Präprozessor-Direktiven beginnen immer mit einer Raute, auf die ein Schlüsselwort und je nach Direktive auch Parameter folgen.
# Schlüsselwort [Parameter]
Listing 22.79 Aufbau von Präprozessor-Direktiven
#define
Mit der Anweisung #define lassen sich Makros erstellen. Makros werden dort, wo sie im Programmcode eingefügt werden, durch den Programmcode ersetzt, der für sie definiert wurde. <Mit der Ausnahme, dass Makros innerhalb von Zeichenketten wirkungslos sind.>
Üblicherweise schreibt man Makros in Großbuchstaben, um sie von Variablen und Funktionsnamen zu unterscheiden.
#define ANZAHL 10
Listing 22.80 Ein Makro erstellen
Die Verwendung erfolgt wie gesagt über den Namen des Makros.
for ( i = 1 ; i < ANZAHL ; i++ ) { printf("%i\n", i); }
Listing 22.81 Verwenden eines Makros
Komplexere Ausdrücke
Makros können auch komplexere Ausdrücke enthalten:
#define CHECK if ( a < 0 ) { printf("Fehler: a zu klein!"); }
Listing 22.82 Komplexere Makros sind auch kein Problem.
Verteilung auf mehrere Zeilen
Wird ein Makro zu lang, dann kann es auch auf mehrere Zeilen verteilt werden. Am Ende einer Zeile muss dazu ein Slash (\) stehen. Dies weist den Compiler an, die nächste Zeile auch noch dem Makro zuzuordnen.
#define CHECK \ if ( a < 0 ) \ { \ printf("Fehler: a zu klein!"); \ }
Listing 22.83 Makros über mehrere Zeilen
Makros mit Parametern
Doch C-Makros können noch mehr: Sie können Parameter benutzen und somit dynamisch verwendet werden. Nehmen wir einmal an, ein Makro soll einen Wert überprüfen und zudem einen zu übergebenden Text ausgeben. Dazu wird eine Schreibweise benutzt, die der einer Funktion ähnelt. Allerdings muss hierfür kein Datentyp angegeben werden.
#define CHECK (str) \ if ( a < 0 ) \ { \ printf(str); \ }
Listing 22.84 Ein Makro mit Parameter
Der Aufruf – Sie ahnen es sicher schon – entspricht fast dem der Funktion:
CHECK("Fehler: a ist zu klein");
Listing 22.85 Aufruf des check-Makros mit Parameter
#undef
Ein Makro ist nur innerhalb der Datei definiert, in der es implementiert wurde. <Man kann Dateien mit der #include-Anweisung in andere Dateien einbinden und Makros damit in mehreren Dateien verfügbar machen.> Außerdem ist ein Makro nur ab der Zeile, in der es implementiert wurde, bis zum Ende einer Quelldatei bekannt. Möchte man ein Makro vor dem Dateiende löschen, dann benutzt man den Befehl #undef Makroname.
Unser CHECK-Makro ließe sich etwa folgendermaßen löschen:
#undef CHECK
Listing 22.86 Das Makro »CHECK« löschen.
#if, #ifdef, #elif, #endif und #if defined
Auch bedingte Anweisungen gibt es für den Präprozessor. Mit diesen lassen sich die Werte und das Vorhandensein von Makros überprüfen.
Die Überprüfung auf Werte wird dabei mit den Anweisungen #if (einfacher Test, wie if) und #elif (Test auf alternative Werte wie else if) erledigt. Am Ende einer solchen Anweisung muss der Befehl #endif stehen, der mit dem bash-Befehl fi und mit der geschlossenen geschweiften Klammer der if-Anweisung vergleichbar ist. #endif signalisiert also nur das Ende einer bedingten Anweisung.
#if ANZAHL < 100 printf("Anzahl ist kleiner als 100"); #elif ANZAHL == 100 printf("Anzahl ist genau 100"); #elif ANZAHL == 101 printf("Anzahl ist genau 101"); #else printf("Anzahl ist größer 101"); #endif
Listing 22.87 Überprüfen des Wertes des Makros ANZAHL
Definierte Makros
Es ist zudem möglich, darauf zu prüfen, ob Makros überhaupt definiert sind.
#ifdef ANZAHL printf("ANZAHL ist definiert."); #else printf("ANZAHL ist nicht definiert."); #endif
Listing 22.88 Ist ANZAHL definiert?
Sie können auch gleichzeitig auf das Vorhandensein mehrerer Makros prüfen. Zudem können einige logische Operatoren verwendet werden.
#if !defined (ANZAHL) && !defined(MAXIMAL) ... #endif
Listing 22.89 Ist ANZAHL definiert?
#include
Neben der Präprozessor-Anweisung #define gibt es noch eine weitere besonders wichtige Anweisung namens #include. Sie wird dazu eingesetzt, andere Dateien an einer bestimmten Stelle in eine Datei einzubinden.
Es gibt dabei zwei Schreibweisen für eine #include-Anweisung:
- Entweder man schreibt den Dateinamen in eckige Klammern. Dann werden die dem Compiler bekannten Include-Verzeichnisse des Systems durchsucht. <Typischerweise /usr/include bekannten Include-Verzeichnisse des Systems durchsucht. <Typischerweise /usr/include
- Oder, man benutzt Anführungszeichen für den Dateinamen. In diesem Fall wird das aktuelle Arbeitsverzeichnis nach der Datei durchsucht. Ist die Datei dort nicht zu finden, werden die dem Compiler zusätzlich angegebenen Include-Pfade nach der Datei durchsucht. <Diese include-Pfade werden über -IPfad gesetzt.>
#include <Dateiname> #include "Dateiname"
Listing 22.90 So verwendet man #include.
Hier ein kleines Beispiel. Die Datei main.h, die ein paar Makros und die #include-Anweisung für die Datei stdio.h enthält, soll in die Quellcode-Datei main.c eingefügt werden. Beide Dateien befinden sich im gleichen Verzeichnis.
#include <stdio.h> #define MIN 1 #define MAX 9
Listing 22.91 Die Datei main.h
#include "main.h" int main() { int i; for ( i = MIN ; i < MAX ; i++) printf("%i\n", i); return 0; }
Listing 22.92 Die Datei main.c
Der Compiler wird in diesem Fall wie immer aufgerufen:
$ gcc -o main main.c
Listing 22.93 Compiler aufrufen
-I
Typischerweise befinden sich die Header-Dateien in einem Unterverzeichnis (etwa include oder inc). Würde sich die Datei main.h dort befinden, dann müsste der gcc das Verzeichnis nach Header-Dateien untersuchen. Das erreicht man (wie bereits erwähnt) mit dem Parameter -I. <Es können mehrere Include-Verzeichnisse angegeben werden. In diesem Beispiel werden sowohl include/ als auch inc/ nach der Datei main.h durchsucht.>
$ gcc -o main main.c -Iinclude -Iinc
Listing 22.94 Compiler mit -I aufrufen
Relative Pfadangabe
Eine relative Pfadangabe ist auch möglich. Hier sind ein paar Beispiele:
#include "../include/main.h" #include "include/main.h" #include "inc/main.h"
Listing 22.95 Relative Pfadangabe: Beispiele
#error
Trifft der Präprozessor auf die Anweisung #error, bricht der Compiler den Übersetzungsvorgang ab.
#error "Lieber User, ich habe keine Lust mehr!"
Listing 22.96 Relative Pfadangabe: Beispiele
Der gcc bricht dann mit folgender Fehlermeldung ab:
a.c:1:2: error: #error "Lieber User, ich habe keine Lust mehr!"
Listing 22.97 Relative Pfadangabe: Beispiele
Nutzen?
Wann ist diese Anweisung nützlich? Nun, dem Compiler können dynamisch Makros inklusive Werte übergeben werden. Außerdem bringt der Compiler standardmäßig bestimmte Makros (teilweise mit Werten) mit, die beim Übersetzungsvorgang abgefragt werden können.
Der folgende Code überprüft, ob die vordefinierten Makros __OpenBSD__ oder __linux__ nicht definiert sind. <Diese Makros sind nur definiert, wenn das System, auf dem der Quellcode kompiliert wird, dem Namen des Makros entspricht.>
#if !defined (__OpenBSD__) && !defined(__linux__) #error "Programm Tool laeuft nur unter Linux/OpenBSD" #endif
Listing 22.98 Ist __OPENBSD__ oder __linux__ definiert?
#pragma
Die Direktive #pragma wird sehr unterschiedlich verwendet. Ihre Funktionsweise ist abhängig von der Plattform und dem Compiler sowie von dessen Version. <Für Parallelprogrammierung werden beispielsweise »#pragma omp parallel for ...« und ähnliche Befehle verwendet.>
Vordefinierte Makros
Es gibt einige vordefinierte Makros, die im gesamten Programmcode verwendet werden können. Dazu zählen Makros die compiler-spezifisch sind (und mit denen man etwa die Version der C-Library, die des Compilers oder den Namen des Betriebssystems abfragen kann), und einige, die jeder ANSI-C-Compiler kennen sollte.
Wir beschränken uns an dieser Stelle auf solche, die jeder Compiler kennen sollte. Diese Makros sind manchmal für Debugging-Zwecke nützlich.
Zeile
Möchte man im Quellcode erfahren, in welcher Zeile sich eine Anweisung befindet, so kann das Makro __LINE__ verwendet werden. Es beinhaltet eine Integer-Zahl die etwa mit printf ausgegeben werden kann.
Datei
Den Namen der Datei, in der man sich derzeit befindet, erfährt man über das Makro __FILE__, das eine Zeichenkette des Dateinamens enthält.
Datum und Uhrzeit
Das Datum, an dem ein Programm kompiliert wurde, sowie die genaue Uhrzeit erfährt man durch die Makros __DATE__ und __TIME__.
ISOC
Ist ein Compiler ANSI-C-kompatibel, definiert er das Makro __STDC__.
Neu in ISOC99
Seit dem ISOC99-Standard kamen noch weitere Makros hinzu, die jeder ANSI-C99-kompatible Compiler kennen muss. <Für weitere – im Übrigen sehr interessante Informationen – werfen Sie bitte einen Blick in gcc.gnu.org/onlinedocs/gcc/Standards.html.> Für Einsteiger ist davon eigentlich nur __func__ interessant, das den aktuellen Funktionsnamen enthält.
Hier noch ein Beispiel:
#include <stdio.h> #ifndef __STDC__ #error "Kein ANSI-C-Compiler!" #endif #define ANZAHL 999 int main() { if (ANZAHL < 1000) printf("%s %s: Fehler in Datei %s, Zeile %i\n", __DATE__, __TIME__, __FILE__, __LINE__); return 0; }
Listing 22.99 Nutzen vordefinierter Makros
22.1.9 Grundlagen der Zeiger (Pointer)
Nun kommen wir zu einem der letzten Themen unseres C-Crashkurses: den Zeigern. Dieses Thema macht C für viele Programmierer zu einer furchtbaren, unlernbaren Sprache und lässt einige Informatikstudenten im Grundstudium an ihren Fähigkeiten zweifeln. Im Grunde genommen ist das Thema »Zeiger« (engl. Pointer) aber gar nicht so schwer, also nur Mut!
Im Übrigen lassen sich durch Zeiger aufwendige Kopieraktionen im Speicher verhindern und sich somit Programme beschleunigen. Überhaupt sind Zeiger so praktisch, dass wir sie niemals missen wollten. Man spricht in diesem Zusammenhang auch von Referenzierung, da ein Zeiger eine Referenz auf eine Speicheradresse ist.
Adressen von Variablen
Adressoperator
Der Wert einer Variablen steht an einer Position im Speicher. Die Variable kann vereinfacht gesagt als »Name« dieser Speicherposition angesehen werden. Mit diesem Namen wird (ohne, dass Sie etwas davon erfahren müssen) auf die zugehörige Speicheradresse zugegriffen und deren Wert entweder gelesen oder geschrieben. Auf die Speicheradresse einer Variable wird mit dem Adressoperator (&) zugegriffen.
Im Folgenden soll die Adresse der Variable »a« in der Variable »adresse« gespeichert werden.
int a = 10; long adresse; adresse = & a; printf("Adresse der Variable a: %li \n", adressse);
Listing 22.100 Für den Adressoperator
Zeiger auf Adressen
Ein Zeiger zeigt nun auf genau so eine Adresse. Man arbeitet also nicht mehr mit der eigentlichen Variable, sondern mit einer Zeigervariable, die die Adresse des Speichers kennt, auf die man zugreifen möchte.
Anders formuliert: Ein Zeiger ist nichts weiter als eine Variable. Diese Variable enthält die Adresse eines Speicherbereichs.
Einen Zeiger deklariert man mit dem Referenz-Operator (*). Dieser ist nicht mit dem Multiplikationsoperator zu verwechseln, der durch das gleiche Zeichen repräsentiert wird.
int *zeiger;
Listing 22.101 Deklaration eines Zeigers
Möchte man einen Zeiger verwenden, so benötigt man natürlich irgendeine Speicheradresse. Entweder wird dafür – wie wir in diesem Buch allerdings nicht zeigen können – dynamisch Speicher reserviert, oder man benutzt die Adresse einer Variable.
Wir lassen die Variable »zeiger«, die ein Zeiger ist, auf die Adresse der Variable »wert« zeigen. An dieser Speicheradresse steht der Wert 99.
int *zeiger; int wert = 99; /* Zeiger = Adresse von 'wert' */ zeiger = & wert;
Listing 22.102 Ein Zeiger auf eine Integer-Variable
Werte aus Zeigern lesen
Mit dem Referenz-Operator (*) können auch Werte aus Zeigern gelesen werden. Man spricht in diesem Fall von Dereferenzierung.
Das Ergebnis einer solchen Operation ist der Wert, der an dieser Speicherstelle steht.
int *zeiger; int wert1 = 99; int wert2 = 123; /* Zeiger = Adresse von 'wert1' */ zeiger = &wert1 /* 'wert2' = Wert an Adresse von Zeiger * (das ist der Wert an der Adresse von * 'wert1', also 99.) */ wert2 = *zeiger;
Listing 22.103 Dereferenzierung eines Zeigers
Werte lassen sich ändern
Zeigt ein Zeiger auf eine Variable und ändert man den Wert einer Variablen so steht an der Adresse der Variablen natürlich auch dieser Wert. Demnach zeigt ein Zeiger auch immer auf den aktuellen Wert einer Variablen.
int a = 99; int *zeiger_a; zeiger_a = &a; /* a = 100 */ a++; /* a = 101 */ *zeiger_a = *zeiger_a + 1; printf("Zeiger: %i, Wert an Zeiger-Adresse: %i\n", zeiger_a, *zeiger_a); printf("Wert von a: %i\n", a);
Listing 22.104 Verändern von Werten
Eine mögliche Ausgabe des Programms wäre die folgende. Die Adresse des Zeigers wird auf Ihrem Rechner mit sehr hoher Wahrscheinlichkeit eine andere sein, doch die beiden Werte von je 101 müssen dieselben sein. <Tatsächlich können sich die Speicheradressen bei jedem Programmstart ändern.>
Zeiger: 925804404, Wert an Zeiger-Adresse: 101 Wert von a: 101
Listing 22.105 Die Ausgabe des Codes
Call by Reference in C
Bevor wir den kleinen Ausflug in die Zeiger beenden (es gibt noch ein paar weitere Themen, die auf dem bisher Gesagten aufbauen), werden wir uns aber noch eine recht nützliche Funktion von Speicheradressen anschauen: Call by Reference.
Unter Call by Reference versteht man den Aufruf einer Funktion nicht mit den Werten von Variablen, sondern mit den Adressen der Variablenwerte. Verändern die Funktionen dann die Werte an der Adresse einer Variablen, so sind die Werte auch in der übergeordneten Funktion gesetzt.
Dies ist sehr nützlich, da Funktionen immer nur einen Wert zurückgeben können. Auf diese Weise jedoch ist es möglich, mehr als einen Wert zurückzugeben. Die Schreibweise für einen Funktionsparameter, der als Referenz übergeben wird, ist so wie bei der Deklaration einer Zeigervariable: Es wird der *-Operator verwendet.
#include <stdio.h> void func(int *z) { *z = *z + 1; } int main() { int a = 99; int *z = &a; func(&a); printf("a = %i\n", a); func(z); printf("a = %i\n", a); return 0; }
Listing 22.106 Beispiel für Call by Reference
Die Ausgabe wird Sie nicht überraschen: Der Wert von »a« wurde nach jedem Funktionsaufruf inkrementiert:
$ gcc -Wall -o cbr cbr.c $ ./cbr a = 100 a = 101
Listing 22.107 Ausgabe des Programms
22.1.10 Grundlagen der Arrays
Hat man in C eine Variable mit mehreren Elementen, so spricht man von einem Array. Sie kennen Arrays schon aus dem Kapitel zur Shellskript-Programmierung, doch wir werden gleich noch einmal an Beispielen erklären, worum es sich hierbei handelt. <Viele deutsche C-Bücher nennen diesen Datentyp auch Feld.> Arrays können in C mehrere Dimensionen haben, doch wir werden uns hier nur auf eindimensionale Arrays beschränken.
Am besten lassen sich Arrays wohl an einem Beispiel erklären. Nehmen wir an, es soll das Gewicht von zehn Personen gespeichert werden. Nun können Sie zu diesem Zweck zehn einzelne Variablen anlegen. Das wäre allerdings recht umständlich.
Besser ist es da, man nimmt eine Variable und nennt sie etwa »gewicht«. Dieser Variable verpasst man zehn Elemente, von denen jedes einen Wert speichern kann.
/* Integer-Array mit 10 Elementen deklarieren */ int gewicht[10]; gewicht[0] = 77; gewicht[1] = 66; gewicht[2] = 55; gewicht[3] = 67; gewicht[4] = 65; gewicht[5] = 78; gewicht[6] = 80; gewicht[7] = 105; gewicht[8] = 110; gewicht[9] = 65;
Listing 22.108 Deklaration und Intialisierung eines Arrays
Übrigens: Alle Elemente eines C-Arrays sind vom gleichen Datentyp. Außerdem ist das erste Array-Element in C immer das Element 0. Bei einem Array mit zehn Elementen ist das letzte Element also Element 9.
Der Zugriff auf Array-Elemente erfolgt mit name[Index]. Dies gilt sowohl für die Zuweisung von Werten an Array-Elemente wie auch für das Auslesen aus Werten von Array-Elementen:
/* Integer-Array mit 10 Elementen deklarieren */ int dreier[3]; int a, b, c; a = 3; dreier[0] = a; dreier[1] = dreier[0] * 2; /* = 6 */ dreier[2] = 9; b = dreier[3]; /* = 9 */ c = dreier[2] – 3; /* = 3 */
Listing 22.109 Benutzen von Arrays
Variablen als Index
Der Array-Index kann auch durch eine ganzzahlige Variable angegeben werden. So lassen sich hervorragend Schleifen bauen. Dazu eignen sich also die Datentypen int, short und char.
int main() { char c; short s; int i; int array[10]; for (c = 0; c < 10; c++) array[i] = 99; for (s = 0; s < 10; s++) array[s] = 88; for (i = 0; i < 10; i++) array[i] = 77; return 0; }
Listing 22.110 Variablen als Array-Index
22.1.11 Strukturen
Eine Struktur (engl. struct) stellt einen Datentyp dar, der aus mindestens einem Datentyp, meist aber aus mehreren Datentypen, besteht. In diversen anderen Programmiersprachen heißen Strukturen Records.
Die einzelnen Variablen in einer Struktur müssen von einem Datentyp sein (wobei es sich bei diesem auch um Strukturen, Arrays und Zeiger auf Variablen, Zeiger auf Strukturen, Zeiger auf Arrays und Zeiger auf Funktionen handeln kann). Außerdem muss jede Variable einer Struktur einen anderen Namen erhalten.
struct Name { Datentyp Variablenname [:Anzahl der Bits]; Datentyp Variablenname [:Anzahl der Bits]; Datentyp Variablenname [:Anzahl der Bits]; ... };
Listing 22.111 Aufbau einer C-Struktur
Optional kann hinter jeder Variable noch – durch einen Doppelpunkt getrennt – die Anzahl der Bits angegeben werden, die für diese Variable benötigt wird. Dies ist besonders in der Netzwerkprogrammierung sinnvoll, wenn es darum geht, bestimmte Protokollheader abzubilden. Wir werden uns auf Strukturen mit »ganzen« Variablen, also Variablen ohne Bit-Angabe, beschränken. <Wird die Bit-Anzahl der normalen Bit-Anzahl des Datentyps angepasst, ist die Variable natürlich auch »ganz«.>
Beim Zuschauen lernt man ja bekanntlich am besten. Nehmen wir also an, es sollen mehrere Daten einer Person in einer Struktur gespeichert werden. Dazu zählen Alter, Gewicht und Größe. Die zugehörige Struktur benötigt daher drei verschiedene Variablen.
struct person { short gewicht; short alter; short groesse; };
Listing 22.112 Die Struktur 'person'
Initialisierung
Zur Zuweisung von Werten benötigen wir zunächst eine »Variable« von unserem neuen Datentyp »person«. Dazu erzeugen wir mit struct person Name die Variable »name« vom Typ der Struktur »person«. Werte können dann in der Form Name.Variable = Wert zugewiesen werden.
Hier also der entsprechende Code in einem Beispielprogramm: <Dreimal dürfen Sie raten, wessen Bauchansatz das ist ;-)>
struct person { short gewicht; short alter; short groesse; }; int main() { struct person p; p.gewicht = 72; p.alter = 22; p.groesse = 182; return 0; }
Listing 22.113 Verwenden der Struktur 'person'
Arrays und Strukturen
Richtig spaßig werden Strukturen aber meist erst in Array-Form. Sollen etwa drei Personen auf diese Weise gespeichert werden, dann ist auch das kein Problem. Dazu erzeugen wir uns von unserer Variable einfach ein Array mit drei Elementen.
#include <stdio.h> struct person { short gewicht; short alter; short groesse; }; int main() { struct person p[3]; p[0].gewicht = 70; p[0].alter = 22; p[0].groesse = 182; p[1].gewicht = 88; p[1].alter = 77; p[1].groesse = 166; p[2].gewicht = 95; p[2].alter = 50; p[2].groesse = 190; return 0; }
Listing 22.114 Drei Personen als Struktur-Array
22.1.12 Arbeiten mit Zeichenketten (Strings)
Zeichenketten bestehen aus einzelnen Zeichen. Einzelne Zeichen können, wie Sie bereits wissen, in einer char-Variablen gespeichert werden. Die Lösung, ein Array aus char-Variablen für eine Zeichenkette zu verwenden, sollte also nicht sehr überraschend sein.
char zeichenkette[3]; zeichenkette[0] = 'A'; zeichenkette[1] = 'B'; zeichenkette[2] = 'C';
Listing 22.115 Eine erste Zeichenkette
Dies geht allerdings auch wesentlich einfacher. Dazu muss man allerdings wissen, dass C normalerweise ein abschließendes \0-Zeichen hinter jeder Zeichenkette benutzt. Dieses abschließende Null-Zeichen signalisiert nur das Ende der Zeichenkette und verhindert in vielen Fällen, dass Ihr Programm einfach abstürzt, weil Funktionen, die mit einer Zeichenkette arbeiten, sonst immer mehr Zeichen lesen würden und irgendwann in Speicherbereiche kommen würden, auf die sie keinen Zugriff haben.
Ein einfaches Verfahren, Text in einem Array zu speichern, besteht darin, die Anzahl der Elemente wegzulassen und nur den Text für das Array zu speichern. C setzt in diesem Fall automatisch die Anzahl der Array-Elemente sowie das abschließende \0-Zeichen.
char zeichenkette[] = "ABC";
Listing 22.116 So geht es einfacher.
Möchte man eine »leere« Zeichenkette anlegen, so sollte man den Speicherbereich des Arrays immer mit \0-Zeichen überschreiben, um sicherzugehen, dass keine zufälligen Daten enthalten sind. Die Schreibweise hierfür ist die folgende: <Es gibt viele alternative Möglichkeiten, dies zu erreichen, etwa die Funktion bzero(), die Funktion memset() oder eine Schleife.>
char zeichenkette[100] = { '\0' };
Listing 22.117 Nullen-Füller
Ausgeben von Zeichenketten
Die Ausgabe ließe sich natürlich in einer Schleife abwickeln, doch das wäre sehr umständlich. Stattdessen gibt es für die printf-Funktion den Formatparameter %s. Dieser bedeutet, dass eine Zeichenkette ausgegeben werden soll. Auch hierfür wird ein \0-Zeichen am Ende einer Zeichenkette benötigt.
printf("Zeichenkette: %s\n", zeichenkette);
Listing 22.118 Ausgabe einer Zeichenkette
Kopieren von Zeichenketten
Nun, da Sie wissen, wie man eine Zeichenkette anlegt, können wir einen Schritt weiter gehen und Zeichenketten kopieren. Dazu verwendet man entweder eine umständliche Schleife, oder man lässt diese Arbeit von einer Funktion erledigen. Zum Kopieren von Daten und speziell zum Kopieren von Zeichenketten gibt es verschiedenste Funktionen in C. Vorstellen werden wir die zwei wichtigsten: strcpy und strncpy. Beide Funktionsprototypen befinden sich in der Datei string.h.
strcpy
strcpy werden zwei Argumente übergeben. Das erste Argument ist das Ziel des Kopiervorgangs, und das zweite Argument ist die Quelle. Möchten Sie also die Zeichenkette aus dem Array »z1« in das Array »z2« kopieren, dann würde das so ablaufen:
#include <stdio.h> #include <string.h> int main() { char z2[] = "Hallo"; char z1[10] = { '\0' }; strcpy(z1, z2); printf("%s = %s\n", z2, z1); return 0; }
Listing 22.119 Kopieren einer Zeichenkette
strncpy
Die Funktion strncpy benötigt noch ein drittes Argument: die Anzahl der zu kopierenden Zeichen. Soll vom obigen String etwa nur ein Zeichen kopiert werden, dann läuft das so:
#include <stdio.h> #include <string.h> int main() { char z2[] = "Hallo"; char z1[10] = { '\0' }; strncpy(z1, z2, 1); printf("%s != %s\n", z2, z1); return 0; }
Listing 22.120 Anwenden von strncpy
22.1.13 Einlesen von Daten
In C können Werte für Variablen und ganze Zeichenketten sowohl von der Tastatur als auch aus Dateien eingelesen werden.
Auch hierfür gibt es diverse Funktionen wie getc, gets, fgets, scanf, fscanf, sscanf, vscanf und viele weitere. Wir werden uns allerdings auf die Funktionen scanf und fscanf beschränken, mit denen die meisten Aufgaben erledigt werden können. Beide Funktionsprototypen befinden sich in der Datei stdio.h.
scanf
Die Funktion scanf liest Werte direkt von der Standardeingabe (wenn man es nicht im Code umprogrammiert, dann ist dies fast immer die Tastatur beziehungsweise Daten aus einer Pipe). Ähnlich wie die Funktion printf wird dabei ein Formatstring übergeben. Dieser Formatstring enthält diesmal jedoch nicht die Werte, die auszugeben sind, sondern die Werte, die einzulesen sind.
Möchten Sie etwa einen Integer einlesen, so verwenden Sie den Parameter %i im Formatstring. Das Ergebnis wird in der entsprechend folgenden Variable gespeichert. Damit scanf den Wert einer Variablen setzen kann, benötigt es allerdings die Speicheradresse der Variablen (die Funktion arbeitet mit Zeigern). Daher müssen Variablen entsprechend übergeben werden.
#include <stdio.h> int main() { int wert; printf("Bitte geben Sie eine ganze Zahl ein: "); scanf("%i", &wert); printf("Sie haben %i eingegeben\n", wert); return 0; }
Listing 22.121 Einlesen eines Integers
Zeichenketten einlesen
Übergibt man ein Array an eine Funktion, dann wird dieses Array in C durch seine Adresse übergeben. Sie müssen in diesem Fall also nicht den Adressoperator (&) verwenden.
char wort[100]; printf("Bitte geben Sie ein Wort ein: "); scanf("%s", &wort); printf("Sie haben %s eingegeben\n", wort);
Listing 22.122 Eine Zeichenkette einlesen
Würde man in diesem Fall ein Wort mit mehr als 99 Zeichen eingeben, so könnte es zu einem sogenannten Speicherüberlauf kommen. Dies führt zu unvorhersehbarem Verhalten, meistens jedoch zu einem Programmabsturz. Mehr zu diesem Thema erfahren Sie in unserem Buch »Praxisbuch Netzwerksicherheit«. Entgegen vieler Meinungen gibt es allerdings einige Techniken, um mit diesem Problem umzugehen. Mehr dazu folgt im nächsten Kapitel.
fscanf
Die Funktion fscanf unterscheidet sich von scanf dadurch, dass die Eingabequelle im ersten Parameter angegeben wird. Damit ist es auch möglich, aus einer Datei zu lesen. Der erste Parameter ist dabei ein Zeiger vom Typ FILE.
22.1.14 FILE und das Arbeiten mit Dateien
Ein sehr spannendes Thema ist das Arbeiten mit Dateien. Zum Ende unseres kleinen C-Crashkurses lernen Sie nun also, wie aus Dateien gelesen und wie in sie geschrieben wird. Auch hier gibt es verschiedenste Möglichkeiten, dies zu tun. Man könnte etwa die Funktionen open, read, write und close benutzen. Wir empfehlen Ihnen, diese Funktionen einmal anzuschauen, sie sind in vielerlei Hinsicht (etwa auch bei der Netzwerkprogrammierung) von Nutzen. Wir werden uns allerdings mit den ANSI-C-Funktionen fopen, fwrite, fread und fclose beschäftigen. <Es gibt noch so viele weitere Funktionen wie etwa fseek(). Werfen Sie einen Blick in eines der genannten guten Bücher zur Linux-Programmierung, um mehr zu erfahren. Es lohnt sich!>
Die Funktionsprototypen der Funktionen fopen, fwrite, fread und fclose sowie die Definition des Datentyps FILE befinden sich in der Header-Datei stdio.h.
Öffnen und Schließen von Dateien
Dateien öffnen
Das Öffnen einer Datei erfolgt mit der Funktion fopen. Der Funktion werden zwei Argumente, der Dateiname und die Zugriffsart, übergeben. Bei der Zugriffsart unterscheidet man unter Linux zwischen den folgenden Arten:
- r Die Datei wird zum Lesen geöffnet. Es wird vom Anfang der Datei gelesen.
- r+ Die Datei wird zum Lesen und Schreiben geöffnet. Es wird vom Anfang der Datei gelesen und am Anfang der Datei geschrieben.
- w Die Datei wird auf die Länge 0 verkürzt (oder, falls sie nicht existiert, neu angelegt) und zum Schreiben geöffnet. Es wird vom Anfang der Datei geschrieben.
- w+ Die Datei wird wie im Fall von w geöffnet. Zusätzlich kann in die Datei geschrieben werden.
- a Die Datei wird zum Schreiben geöffnet bzw. erzeugt, wenn sie nicht existiert. Es wird an das Ende der Datei geschrieben.
- a+ Die Datei wird wie im Fall von a geöffnet. Allerdings kann auch von der Datei gelesen werden.
FILE
Die Funktion fopen gibt die Adresse eines Dateideskriptors vom Datentyp FILE zurück. Über diesen Deskriptor kann eine geöffnete Datei identifiziert werden. Für Lese-, Schreib- und Schließoperationen auf Dateien ist ein FILE-Deskriptor übrigens nötig.
Fehler
Für den Fall, dass eine Datei (etwa weil sie nicht existiert oder weil das Programm nicht die nötigen Zugriffsrechte auf die Datei hat) nicht geöffnet werden konnte, gibt fopen den Wert NULL zurück. <Meistens ist NULL als Makro für ((void *)0), also einen Zeiger auf die Adresse 0, definiert. Weiter kann in diesem Crashkurs leider nicht auf diesen Wert eingegangen werden.>
fclose
Ein »geöffneter« FILE-Deskriptor wird mit der Funktion fclose wieder geschlossen, indem er ihr als Parameter übergeben wird. Nachdem ein Deskriptor geschlossen wurde, können weder Lese- noch Schreibzugriffe über den Deskriptor erfolgen. Daher sollten Deskriptoren erst geschlossen werden, wenn man sie nicht mehr benötigt. Vergisst ein Programm, einen Deskriptor zu schließen, wird er nach dem Ende des Programms automatisch geschlossen. Es zählt allerdings zum guten Programmierstil, Deskriptoren selbst zu schließen und damit den Verwaltungsaufwand für offene Deskriptoren zu verringern und keine unnützen offenen Dateien im Programm zu haben.
#include <stdio.h> int main() { FILE * fp; /* Oeffnen der Datei /etc/hosts im Nur-Lesen-Modus * am Dateianfang. */ fp = fopen("/etc/hosts", "r"); /* Konnte die Datei geoeffnet werden? */ if (fp == NULL) { printf("Konnte die Datei nicht oeffnen!\n"); /* Das Programm mit einem Fehler-Rueckgabewert * verlassen */ return 1; } /* Die Datei schliessen */ fclose(fp); return 0; }
Listing 22.123 Eine Datei öffnen und schließen
Lesen aus Dateien
fread
Aus einer zum Lesen geöffneten Datei kann mit der Funktion fread gelesen werden. Die gelesenen Daten werden dazu in einem char-Array gespeichert (beziehungsweise in einem dynamisch reservierten Speicherbereich aus char-Werten, den Sie in diesem Fall aber mit einem char-Array gleichsetzen können, und was wir im Rahmen dieses Crashkurses nicht behandeln können).
fread benötigt als Parameter die Speicheradresse des char-Arrays (den Adressoperator muss man in diesem Fall, wie bereits gesagt, nicht anwenden), die Größe der zu lesenden Datenblöcke, die Anzahl der zu lesenden Datenblöcke und einen Zeiger auf einen Deskriptor, von dem gelesen werden soll.
Wir erweitern das obige Beispiel nun um die Funktion, 1000 Bytes aus der geöffneten Datei zu lesen. Dazu legen wir ein 1000 Byte großes char-Array an (das letzte Byte wird für das abschließende \0-Zeichen benötigt) und lesen einmal einen Block von 999 Bytes aus der Datei. Anschließend geben wir den gelesenen Inhalt mit printf aus.
#include <stdio.h> int main() { FILE * fp; char inhalt[1000] = { '\0' }; fp = fopen("/etc/hosts", "r"); if (fp == NULL) { printf("Konnte die Datei nicht oeffnen!\n"); return 1; } fread(inhalt, 999, 1, fp); printf("Inhalt der Datei /etc/hosts:\n%s\n", inhalt); fclose(fp); return 0; }
Listing 22.124 Eine Datei öffnen und schließen
$ gcc -o file file.c $ ./file Inhalt der Datei /etc/hosts: 127.0.0.1 localhost 127.0.1.1 hikoki.sun hikoki 192.168.0.1 eygo.sun eygo 192.168.0.2 milk.sun milk 192.168.0.5 yorick.sun yorick 192.168.0.6 hikoki.sun hikoki 192.168.0.11 amilo.sun amilo
Listing 22.125 Aufruf des Progamms (gekürzt)
Schreiben in Dateien
fwrite
Die Parameter der Funktion fwrite sind denen der Funktion fread sehr ähnlich. Der Unterschied ist nur, dass man nicht den Buffer angibt, in den die gelesenen Daten geschrieben werden sollen, sondern den, von dessen Inhalt Daten geschrieben werden sollen. Der zweite Parameter gibt wieder die Größe der zu schreibenden Datenelemente an und der dritte Parameter gibt deren Anzahl an. Der vierte Parameter ist der Deskriptor der geöffneten Datei, in die geschrieben werden soll.
Als Beispiel soll die Zeichenkette »Hallo Welt!« in die Datei /tmp/irgendwas geschrieben werden. An dieser Stelle verwende ich den sizeof-Operator, um die Größe des Arrays zu erfahren. Eine andere Möglichkeit dafür wäre es, die Funktion strlen zu verwenden, die die Länge einer Zeichenkette zurückgibt, die wir in diesem Rahmen aber nicht behandeln können.
#include <stdio.h> int main() { FILE * fp; char inhalt[] = "Hallo Welt!\n"; fp = fopen("/tmp/irgendwas", "w"); if (fp == NULL) { printf("Konnte die Datei nicht oeffnen!\n"); return 1; } fwrite(inhalt, sizeof(inhalt), 1, fp); fclose(fp); return 0; }
Listing 22.126 Schreiben in /tmp/irgendwas
Zum Beweis und auch zum Schluss unseres Crashkurses zeigen wir, hier noch einmal den Compileraufruf für das Schreibprogramm, den Aufruf des Programms und das Anschauen der geschrieben Datei.
$ gcc -o file2 file2.c $ ./file2 $ cat /tmp/irgendwas Hallo Welt!
Listing 22.127 Compileraufruf und Test
22.1.15 Das war noch nicht alles!
C bietet Ihnen noch eine Menge weiterer Möglichkeiten als die, die wir Ihnen bisher gezeigt haben. So gibt es beispielsweise Aufzählungstypen (enums), Unions, mehrdimensionale Arrays, Zeiger auf Arrays, Pointer auf Arrays aus Pointern, Pointer auf ganze Funktionen (und natürlich auch Arrays aus Pointern auf Funktionen), Pointer auf Pointer auf Pointer auf Pointer (usw.), diverse weitere Schlüsselwörter für Datentypen (static, const, extern, ...), globale Variablen, unzählige weitere Funktionen des ANSI-C-Standards und Funktionen zur Systemprogrammierung aus Standards wie POSIX, weitere Operatoren, Casts, dynamische Speicherverwaltung (ein besonders tolles Feature von C!), Tonnen von Header-Dateien, ...