vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 3

Tag 20

Compiler für Fortgeschrittene

In dieser Lektion werden einige fortgeschrittene Möglichkeiten des C-Compilers besprochen. Heute lernen Sie:

Der C-Präprozessor

Der Präprozessor ist Teil eines jeden C-Compiler-Pakets. Wenn Sie ein C-Programm kompilieren, wird es als erstes vom Präprozessor bearbeitet. In den meisten Compilern, einschließlich GNU gcc, ist der Präprozessor Teil des Compiler- Programms. Das bedeutet, dass bei Aufruf des Compilers automatisch auch der Präprozessor gestartet wird.

Der Präprozessor verändert den Quelltext auf der Grundlage von Instruktionen, so genannten Präprozessor-Direktiven, die im Quelltext selbst stehen. Die Ausgabe des Präprozessors ist eine geänderte Quelltextdatei, die dann als Eingabe für den nächsten Kompilierschritt dient. Normalerweise bekommen Sie diese Datei nicht zu Gesicht, da der Compiler sie löscht, nachdem sie nicht mehr benötigt wird. Weiter hinten in diesem Kapitel werde ich Ihnen zeigen, wie Sie sich diese Zwischendatei anschauen können. Zuerst jedoch möchte ich Ihnen die Präprozessor-Direktiven vorstellen, die alle mit dem Symbol # beginnen.

Die Präprozessor-Direktive #define

Die Präprozessor-Direktive #define hat zwei Einsatzbereiche: Sie erzeugt sowohl symbolische Konstanten als auch Makros.

Einfache Substitutionsmakros mit #define

Substitutionsmakros haben Sie bereits am Tag 2, »Die Komponenten eines C- Programms: Quellcode und Daten«, kennen gelernt, auch wenn sie damals noch mit dem Begriff symbolische Konstanten umschrieben wurden. Substitutionsmakros erzeugt man, um einen Text durch einen anderen Text zu ersetzen. Um zum Beispiel text1 durch text2 zu ersetzen, schreiben Sie Folgendes:

#define text1 text2

Diese Direktive veranlasst, dass der Präprozessor die gesamte Quellcode-Datei durchgeht und jedes Vorkommen von text1 durch text2 ersetzt. Die einzige Ausnahme davon sind Stellen, in denen text1 in doppelten Anführungszeichen steht (also String-Literale). In einem solchen Fall wird keine Ersetzung vorgenommen.

Am häufigsten werden Substitutionsmakros für die Erzeugung symbolischer Konstanten eingesetzt, wie Sie es bereits am Tag 2 gesehen haben. Wenn Ihr Programm zum Beispiel die folgenden Zeilen enthält:

#define MAX 1000
x = y * MAX;
z = MAX - 12;

sieht der Quelltext, nachdem der Präprozessor ihn durchgegangen ist, folgendermaßen aus:

x = y * 1000;
z = 1000 - 12;

Der Effekt ist der gleiche, als hätten Sie in Ihrer Textverarbeitung den Suchen&Ersetzen-Befehl aufgerufen, um sämtliche Vorkommen von MAX in 1000 zu ändern. Ihre originale Quelltextdatei wird durch den Präprozessor aber nicht geändert. Statt dessen wird eine temporäre Kopie mit den Änderungen angelegt. Beachten Sie, dass #define nicht nur auf die Erzeugung symbolischer numerischer Konstanten beschränkt ist. So können Sie zum Beispiel schreiben:

#define ZINGBOFFLE printf
ZINGBOFFLE("Hallo, Welt.");

auch wenn es dafür eigentlich keinen Grund gibt. Außerdem sollten Sie sich darüber im Klaren sein, dass manche Autoren mit #define definierte Konstanten ebenfalls als Makros betrachten. In diesem Buch werden wir den Begriff Makro allerdings auf die Konstrukte beschränken, die im nächsten Abschnitt beschrieben werden.

Funktionsmakros mit #define

Sie können mit der Direktiven #define auch Funktionsmakros erzeugen. Ein Funktionsmakro ist eine Art Kurzform, die etwas ziemlich Kompliziertes in einfacher Form ausdrückt. Von Funktionsmakros spricht man, weil diese Art von Makro genau wie eine richtige C-Funktion Argumente übernehmen kann. Ein Vorteil der Funktionsmakros ist der, dass ihre Argumente nicht typspezifisch sind. Das heißt, Sie können einem Funktionsmakro, das ein numerisches Argument erwartet, jeden beliebigen numerischen Variablentyp übergeben.

Lassen Sie uns dies anhand eines Beispiels veranschaulichen. Die Präprozessor- Direktive

#define HAELFTEVON(wert) ((wert)/2)

definiert ein Makro namens HAELFTEVON, das einen Parameter namens wert übernimmt. Immer wenn der Präprozessor im Quelltext auf den Text HAELFTEVON(wert) stößt, ersetzt er diesen Text durch den Definitionstext und fügt das gewünschte Argument ein. So würde zum Beispiel die Quelltextzeile

ergebnis = HAELFTEVON(10);

durch die folgende Zeile ersetzt:

ergebnis = ((10)/2);

Entsprechend wird aus

printf("%f", HAELFTEVON(x[1] + y[2]));

die Anweisung:

printf("%f", ((x[1] + y[2])/2));

Ein Makro kann mehr als einen Parameter definieren und jeder Parameter kann mehr als einmal in dem Ersetzungstext verwendet werden. So hat zum Beispiel das folgende Makro, das den Durchschnitt von fünf Werten ermittelt, fünf Parameter:

#define MITTEL5(v, w, x, y, z) (((v)+(w)+(x)+(y)+(z))/5)

Das folgende Makro, in dem der Bedingungsoperator den größeren von zwei Werten bestimmt, verwendet seine Parameter zweimal. (Den Bedingungsoperator haben Sie am Tag 3, »Anweisungen, Ausdrücke und Operatoren«, kennen gelernt.)

#define GROESSER(x, y) ((x) > (y) ? (x) : (y))

Ein Makro kann so viele Parameter haben, wie es benötigt. Wenn Sie das Makro aufrufen, müssen Sie ihm die korrekte Anzahl an Argumenten übergeben.

In einer Makrodefinition muss die öffnende Klammer direkt auf den Makronamen folgen. Es darf kein Whitespace (Leerzeichen etc.) dazwischen stehen. Die öffnende Klammer teilt dem Präprozessor mit, dass es sich hierbei um die Definition eines Funktionsmakros handelt und nicht nur um die Substitution einer einfachen symbolischen Konstanten. Werfen wir einen Blick auf die folgende Definition:

#define SUMME (x, y, z) ((x)+(y)+(z))

Aufgrund des Leerzeichens zwischen SUMME und ( behandelt der Präprozessor diese Definition wie ein einfaches Substitutionsmakro. Jedes Vorkommen von SUMME im Quelltext wird durch (x, y, z) ((x)+(y)+(z)) ersetzt, was Sie mit Sicherheit nicht wollten.

Ich möchte Sie darauf aufmerksam machen, dass in dem Substitutionsstring jeder Parameter in Klammern steht. Damit sollen unerwünschte Nebeneffekte bei der Übergabe von Ausdrücken als Argumente an das Makro verhindert werden. Im folgenden Beispiel wird ein Makro ohne diese Klammern definiert:

#define QUADRAT(x) x*x

Wenn Sie dieses Makro mit einer einfachen Variablen als Argument aufrufen, gibt es keine Probleme. Was aber, wenn Sie einen Ausdruck als Argument übergeben?

ergebnis = QUADRAT(x + y);

Wird das Makro mit diesem Argument expandiert, erhalten Sie nicht das gewünschte Ergebnis, sondern:

ergebnis = x + y * x + y;

Mit Klammern an den richtigen Stellen können Sie dieses Problem, wie folgendes Beispiel zeigt, vermeiden:

#define QUADRAT(x) (x)*(x)

Die Expansion dieser Definition ergibt folgende Zeile und führt damit zum korrekten Ergebnis:

ergebnis = (x + y) * (x + y);

Noch flexibler wird die Makrodefinition durch die Verwendung des stringbildenden Operator (#), der manchmal auch String-Literal-Operator genannt wird. Wenn einem Makroparameter im Substitutionsstring ein # vorangestellt ist, wird das Argument beim Expandieren des Makros in einen String in Anführungszeichen umgewandelt. Wenn Sie also ein Makro wie folgt definieren

#define AUSGEBEN(x) printf(#x)

und es mit folgender Anweisung aufrufen

AUSGEBEN(Hallo Mama);

wird nach der Expansion daraus die Anweisung

printf("Hallo Mama");

Die vom stringbildenden Operator durchgeführte Umwandlung berücksichtigt auch Sonderzeichen. Wenn es im Argument ein Zeichen gibt, das normalerweise ein Escape-Zeichen benötigt, fügt der #-Operator vor diesem Zeichen einen Backslash ein. Greifen wir dazu noch einmal auf unser obiges Beispiel zurück. Der Aufruf

AUSGEBEN("Hallo Mama");

expandiert demnach zu

printf("\"Hallo Mama\"");

Ein Beispiel für den #-Operator finden Sie in Listing 20.1. Doch zuvor möchte ich Ihnen noch einen anderen Operator vorstellen, der in Makros verwendet wird, der Verkettungsoperator (##). Dieser Operator verkettet oder besser verbindet im Zuge der Makro-Expansion zwei Strings. Er verwendet keine Anführungszeichen und sieht auch keine Sonderbehandlung für Escape-Zeichen vor. Er dient hauptsächlich dazu, C-Quelltext-Sequenzen zu erzeugen. Wenn Sie zum Beispiel folgendes Makro definieren und aufrufen:

#define HACKEN(x) funk ## x
salat = HACKEN(3)(q, w);

wird das Makro, das in der zweiten Zeile aufgerufen wurde, folgendermaßen expandiert:

salat = funk3 (q, w);

Wie Sie sehen, ist es mit Hilfe des ##-Operators möglich, zwischen dem Aufruf verschiedener Funktionen auszuwählen. Sie programmieren praktisch die Erzeugung des C-Quellcodes.

Listing 20.1 zeigt eine Möglichkeit, den #-Operator zu verwenden.

Listing 20.1: Der #-Operator in der Makro-Expansion.

1: /* Einsatz des #-Operators in einer Makro-Expansion. */
2:
3: #include <stdio.h>
4:
5: #define AUSGABE(x) printf(#x " gleich %d.\n", x)
6:
7: int main(void)
8: {
9: int wert = 123;
10: AUSGABE(wert);
11: return 0;
12: }

wert gleich 123.

Durch die Verwendung des #-Operators in Zeile 5 wird der Variablenname wert bei der Expansion des Makros als String in Anführungszeichen an die Funktion printf() übergeben. Nach der Expansion sieht das Makro AUSGABE wie folgt aus:

printf("wert" " gleich %d.",  wert );

Makros kontra Funktionen

Sie haben gesehen, dass Sie anstelle von richtigen Funktionen auch Funktionsmakros verwenden können - zumindest dort, wo der sich ergebende Code relativ kurz ist. Funkionsmakros können sich zwar durchaus über mehrere Zeile erstrecken, werden aber in der Regel ziemlich unhandlich, wenn sie größer werden. Was sollen Sie jedoch machen, wenn Sie sowohl eine Funktion als auch ein Makro verwenden können? Da heißt es, zwischen Programmgeschwindigkeit und Programmgröße abzuwägen.

Die Definition eines Makros wird so oft im Code expandiert, wie das Makro im Quellcode anzutreffen ist. Wenn Ihr Programm ein Makro 100-mal aufruft, gibt es 100 Kopien dieses expandierten Makrocodes im endgültigen Programm. Im Gegensatz dazu gibt es den Code einer Funktion nur einmal. Deshalb wäre es hinsichtlich der Programmgröße besser, eine richtige Funktion zu wählen.

Wenn ein Programm eine Funktion aufruft, erfordert das einen bestimmten Verarbeitungsüberhang, der benötigt wird, um die Programmausführung an den Funktionscode zu übergeben und später wieder in das aufrufende Programm zurückzukehren. Beim »Aufrufen« eines Makros gibt es keinen Verarbeitungsüberhang, da der Code bereits direkt im Programm steht. Hinsichtlich der Geschwindigkeit liegen die Vorteile also bei den Funktionsmakros.

Das Abwägen zwischen Größe und Geschwindigkeit ist für den Programmieranfänger eher unerheblich. Wichtig werden diese Betrachtungen erst bei großen, zeitkritischen Anwendungen.

Expandierte Makros anzeigen lassen

Es kommt immer mal wieder vor, dass man sich ansehen möchte, wie die Makros expandiert wurden - besonders dann, wenn die Makros nicht ordnungsgemäß funktionieren. Um die expandierten Makros einzusehen, müssen Sie den Compiler anweisen, nach dem ersten Durchgang durch den Code (der die Makro-Expansion mit einschließt) eine Ausgabedatei zu erstellen.

Um zum Beispiel ein Programm namens programm.c mit dem GNU-C-Compiler zu präkompilieren, würden Sie den Compiler wie folgt aufrufen:

gcc -E programm.c

Der Präprozessor geht Ihren Quellcode als Erstes durch. Dabei werden alle Header- Dateien eingebunden, #define-Makros expandiert und andere Präprozessor- Direktiven ausgeführt. Die Ausgabe der Präprozessor-Stufe wird an stdout (das heißt, den Bildschirm) weitergeleitet. Leider ist es nicht besonders hilfreich, den verarbeiteten Code auf dem Bildschirm vorbeihuschen zu sehen! Sie können den Pipe-Befehl zusammen mit more verwenden, um den Inhalt der Datei abschnittsweise auf dem Bildschirm anzuzeigen, oder die Ausgabe direkt in eine Datei umleiten.

gcc -E program.c | more
gcc -E program.c > programm.pre

Anschließend können Sie die Datei in Ihren Editor laden, um sie auszudrucken oder anzuzeigen.

Was Sie tun sollten

Was nicht

Verwenden Sie #define vor allem für symbolische Konstanten. Symbolische Konstanten machen Ihren Code wesentlich lesbarer. Beispiele für Werte, die man als Konstanten definieren sollte, sind Farben, WAHR/FALSCH, JA/NEIN, die Tastaturtasten und Maximalwerte. Sie werden im ganzen Buch auf symbolische Konstanten treffen.

Übertreiben Sie es nicht mit den Makrofunktionen. Verwenden Sie sie dort, wo es nötig ist, aber stellen Sie vorher sicher, dass eine normale Funktion nicht die bessere Wahl wäre.

Die #include-Direktive

Sie haben bereits gelernt, wie Sie die Präprozessor-Direktive #include verwenden, um Header-Dateien in Ihr Programm einzubinden. Wenn der Präprozessor auf eine #include-Direktive trifft, liest er die angegebene Datei und fügt sie dort ein, wo die Direktive steht. Sie können in einer #include-Direktive keine Platzhalter wie * oder ? verwenden, um eine Gruppe von Dateien einzulesen. Sie können jedoch #include- Direktiven verschachteln. Mit anderen Worten, eine eingebundene Datei kann wiederum #include-Direktiven enthalten, die ebenfalls #include-Direktiven enthalten und so weiter. Die meisten Compiler haben einen Grenzwert hinsichtlich der Verschachtelungstiefe, aber normalerweise sind bis zu zehn Ebenen möglich.

Es gibt zwei Möglichkeiten, den Dateinamen für eine #include-Direktive anzugeben. Wenn der Dateiname in spitzen Klammern steht, wie in #include <stdio.h> (was Ihnen in diesem Buch schon häufig begegnet sein dürfte), sucht der Präprozessor die Datei zuerst im Standardverzeichnis. Wenn die Datei nicht gefunden wird oder kein Standardverzeichnis angegeben wurde, sucht der Präprozessor als Nächstes im aktuellen Verzeichnis.

»Was ist denn das Standardverzeichnis?«, werden Sie vielleicht fragen. Unter Linux sind das die Verzeichnisse /usr/include und /usr/local/include. Wenn Sie weitere Verzeichnisse in den include-Pfad mit aufnehmen wollen, verwenden Sie den Befehlszeilen-Schalter -I. Wenn Sie zum Beispiel für die Kompilation des Programms programm.c das Verzeichnis /home/user/erik/include in den include-Pfad aufnehmen wollen, würden Sie gcc wie folgt aufrufen:

gcc -Wall -I/home/user/erik/include programm.c -o programm

Die zweite Möglichkeit, die einzubindende Datei anzugeben, besteht darin, den Dateinamen in doppelte Anführungszeichen zu setzen: #include "meinedatei.h". In diesem Fall durchsucht der Präprozessor nicht die Standardverzeichnisse, sondern nur das Verzeichnis, in dem auch die gerade kompilierte Quelltextdatei steht. Allgemein lässt sich sagen, dass Sie Ihre selbst aufgesetzten Header-Dateien in demselben Verzeichnis wie die C-Quelltextdateien ablegen und mit doppelten Anführungszeichen einbinden sollten. Das Standardverzeichnis ist für systemweite Header-Dateien reserviert.

Die Verwendung von #if, #elif, #else und #endif

Diese vier Präprozessor-Direktiven steuern die bedingte Kompilierung. Bedingte Kompilierung bedeutet, dass bestimmte Blöcke von C-Code nur dann kompiliert werden, wenn gewisse Bedingungen erfüllt sind. In vielerlei Hinsicht entspricht die Familie der #if-Präprozessor-Direktiven in ihrer Funktionsweise der if-Anweisung von C. Der Unterschied liegt hauptsächlich darin, dass if kontrolliert, ob bestimmte Anweisungen ausgeführt werden, während #if kontrolliert, ob sie kompiliert werden.

Die Struktur eines #if-Blocks sieht folgendermaßen aus:

#if Bedingung_1
Anweisung_block_1
#elif Bedingung_2
Anweisung_block_2
...
#elif Bedingung_n
Anweisung_block_n
#else
Standard_anweisung_block
#endif

Als Bedingung kann fast jeder beliebige Ausdruck verwendet werden, der als Konstante ausgewertet wird. Nicht verwendet werden können der sizeof()-Operator, Typenumwandlungen oder Werte vom Datentyp float. Meist prüft man mit #if symbolische Konstanten, die mit der #define-Direktive erzeugt wurden.

Jeder Anweisung_block besteht aus einer oder mehreren C-Anweisungen. Erlaubt sind jegliche Formen von Anweisungen, auch Präprozessor-Direktiven. Die Anweisungen müssen nicht in geschweiften Klammern stehen, aber es schadet auch nicht.

Die Direktiven #if und #endif sind obligatorisch, wohingegen #elif und #else optional sind. Sie können so viele #elif-Direktiven verwenden, wie Sie wollen, aber nur ein #else. Wenn der Compiler auf eine #if-Direktive trifft, testet er die damit verbundene Bedingung. Wird die Bedingung als wahr (ungleich Null) ausgewertet, werden die auf das #if folgenden Anweisungen kompiliert. Wenn die Bedingung als falsch (Null) ausgewertet wird, testet der Compiler der Reihe nach die mit jeder #elif- Direktive verbundenen Bedingungen. Die Anweisungen, die mit dem ersten wahren #elif verbunden sind, werden kompiliert. Wenn keine der Bedingungen wahr ist, werden die Anweisungen, die auf die #else-Direktive folgen, kompiliert.

Beachten Sie, dass höchstens ein einziger Anweisungsblock innerhalb der #if...#endif-Konstruktion kompiliert wird. Wenn der Compiler keine #else-Direktive findet, wird unter Umständen keine Anweisung kompiliert.

Den Einsatzmöglichkeiten der bedingten Kompilierungsdirektiven sind nur durch Ihre Vorstellungskraft Grenzen gesetzt. Betrachten wir ein Beispiel. Angenommen Sie schreiben ein Programm, das eine Unmenge von landesspezifischen Informationen verwendet. Diese Informationen sind für jedes Land in einer eigenen Header-Datei untergebracht. Wenn Sie das Programm zum Einsatz in mehreren Ländern kompilieren, können Sie eine #if...#endif-Konstruktion wie die folgende verwenden:

#if ENGLAND == 1
#include "england.h"
#elif FRANKREICH == 1
#include "frankreich.h"
#elif ITALIEN == 1
#include "italien.h"
#else
#include "deutschland.h"
#endif

Wenn Sie dann mit #define die entsprechende symbolische Konstante definieren, können Sie steuern, welche Header-Datei während der Kompilierung eingebunden wird.

Debuggen mit #if...#endif

Die Direktiven #if...#endif werden auch häufig dazu verwendet, bedingten Debug- Code in ein Programm mit aufzunehmen. Sie könnten beispielsweise eine symbolische Konstante DEBUG definieren, die entweder auf 1 oder 0 gesetzt wird, und dann an kritischen Stellen des Programms speziellen Debug-Code einfügen:

#if DEBUG == 1
hier Debug-Code
#endif

Wenn Sie während der Programmentwicklung DEBUG als 1 definieren, wird der Debug- Code mit aufgenommen, um Ihnen bei der Fehlersuche zu helfen. Nachdem das Programm ordnungsgemäß läuft, können Sie DEBUG auf 0 setzen und das Programm ohne den Debug-Code neu kompilieren.

Der Operator defined() wird beim Aufsetzen bedingter Kompilierungsdirektiven verwendet. Dieser Operator prüft, ob ein bestimmter Name definiert wurde. So wird der Ausdruck

defined( NAME )

zu wahr oder falsch ausgewertet, je nachdem ob NAME definiert ist oder nicht. Durch die Verwendung von defined() können Sie die Kompilierung, ohne Rücksicht auf den besonderen Wert eines Namens, auf der Basis von vorangehenden Definitionen steuern. So könnten Sie den #if...#endif-Abschnitt aus dem obigen Beispiel wie folgt umformulieren:

#if defined( DEBUG )
hier Debug-Code
#endif

Sie können defined() auch dazu verwenden, einem bisher noch nicht definierten Namen eine Definition zuzuweisen. Neben defined kommt dabei der NOT-Operator (!) zum Einsatz:

#if !defined( TRUE )     /* wenn TRUE nicht definiert ist. */
#define TRUE 1
#endif

Beachten Sie, dass es für den defined()-Operator unerheblich ist, ob ein Name als Ersatz für irgendetwas definiert ist oder nicht. So wird zum Beispiel in der folgenden Programmzeile der Name ROT definiert, aber nicht als Synonym für irgendeinen Wert (oder Ähnliches) eingeführt:

#define ROT

Trotzdem wird der Ausdruck defined( ROT ) als wahr ausgewertet werden. Allerdings führt eine solche Definition auch dazu, dass alle Vorkommen von ROT im Quelltext entfernt und durch nichts ersetzt werden. Lassen Sie deshalb Vorsicht walten.

Die Mehrfacheinbindung von Header-Dateien vermeiden

Mit zunehmender Programmgröße und extensiver Verwendung von Header-Dateien erhöht sich das Risiko, aus Versehen eine Header-Datei mehr als einmal einzubinden. Das kann dazu führen, dass der Compiler verstört die Kompilierung abbricht. Mit den eben besprochenen Direktiven können Sie dieses Problem leicht vermeiden. Sehen Sie dazu das Beispiel in Listing 20.2.

Listing 20.2: Präprozessor-Direktiven für Header-Dateien.

1: /* PROG.H - eine Header-Datei, die Mehrfacheinbidungen verhindert! */
2:
3. #if defined( PROG_H )
4: /* die Datei wurde bereits eingebunden */
5: #else
6: #define PROG_H
7:
8: /* Hier stehen die Daten der Header-Datei... */
9:
10:
11:
12: #endif

Lassen Sie uns untersuchen, was diese Header-Datei genau macht. In Zeile 3 prüft sie, ob PROG_H bereits definiert ist. Beachten Sie, dass der Name PROG_H in Anlehnung an den Namen der Header-Datei gewählt wurde. Wenn PROG_H definiert ist, liest der Präprozessor als Nächstes den Kommentar in Zeile 4, und das Programm hält dann Ausschau nach dem #endif am Ende der Header-Datei. Konkret bedeutet dies, dass nichts weiter geschieht.

Wie wird PROG_H definiert? Die Definition steht in Zeile 6. Wenn dieser Header das erste Mal eingebunden wird, prüft der Präprozessor, ob PROG_H definiert ist. Da dies nicht der Fall ist, springt der Präprozessor zur #else-Anweisung. Nach #else wird zuerst einmal PROG_H definiert, so dass alle nachfolgenden Versuche, diese Datei einzubinden, den Rumpf der Datei überspringen. Die Zeilen 7 bis 11 können eine beliebige Anzahl an Befehlen oder Deklarationen enthalten.

Die Direktive #undef

Die #undef-Direktive ist das Gegenteil von #define - sie entfernt die Definition eines Namens. Sehen Sie dazu folgendes Beispiel:

#define DEBUG 1
/* In diesem Programmabschnitt werden die Vorkommen von DEBUG */
/* durch 1 ersetzt, und der Ausdruck defined( DEBUG ) wird als */
/* WAHR ausgewertet. */
#undef DEBUG
/* In diesem Programmabschnitt werden die Vorkommen von DEBUG */
/* nicht ersetzt und der Ausdruck defined( DEBUG ) wird als */
/* FALSCH ausgewertet. */

Sie können #undef und #define dazu verwenden, einen Namen zu erzeugen, der nur in Teilen Ihres Quellcodes definiert wird. Wenn Sie dies mit der #if-Direktive kombinieren (siehe oben), haben Sie noch mehr Kontrolle über die bedingte Kompilierung Ihres Quelltextes.

Vordefinierte Makros

Die meisten Compiler sind mit einer Reihe von vordefinierten Makros ausgestattet. Die nützlichsten unter ihnen sind __DATE__, __TIME__, __LINE__ und __FILE__. Merken Sie sich, dass jedes dieser Makros vorne und hinten jeweils doppelte Unterstriche aufweist. Unter der Prämisse, dass es höchst unwahrscheinlich ist, dass Programmierer ihre eigenen Definitionen mit führenden und abschließenden Unterstrichen erzeugen, soll Sie das davon abhalten, diese Makros neu zu definieren.

Diese Makros funktionieren genauso wie die heute bereits beschriebenen Makros. Wenn der Präcompiler auf eines dieser Makros trifft, ersetzt er das Makro durch den Makro-Code. __DATE__ und __TIME__ werden durch das aktuelle Datum beziehungsweise die aktuelle Zeit ersetzt. Unter »aktueller Zeit und aktuellem Datum« sind dabei Zeit und Datum der Präkompilierung der Quelltextdatei zu verstehen. Diese Information kann von Nutzen sein, wenn Sie mit verschiedenen Versionen eines Programms arbeiten. Indem Sie von einem Programm Zeit und Datum der Kompilierung ausgeben lassen, können Sie feststellen, ob Sie die letzte oder eine frühere Version des Programms ausführen.

Die anderen beiden Makros sind sogar noch wertvoller. __LINE__ wird durch die aktuelle Zeilennummer der Quelltextdatei ersetzt, __FILE__ durch den Dateinamen der Quelltextdatei. Diese beiden Makros eignen sich am besten zum Debuggen eines Programms oder zur Fehlerbehandlung. Betrachten wir einmal die folgende printf()- Anweisung:

31:
32: printf( "Programm %s: (%d) Fehler beim Öffnen der Datei ",
__FILE__, __LINE__ );
33:

Wenn diese Zeilen Teil eines Programm namens meinprog.c wären, lautete die Ausgabe:

Programm meinprog.c: (32) Fehler beim Öffnen der Datei

Im Moment mag dies vielleicht nicht allzu wichtig erscheinen. Wenn aber Ihre Programme an Umfang zunehmen und sich über mehrere Quelltextdateien erstrecken, wird das Aufspüren von Fehlern immer schwieriger. Mit den Makros __LINE__ und __FILE__ wird das Debuggen einfacher.

Was Sie tun sollten

Was nicht

Verwenden Sie in Debug- Fehlermeldungen die Makros __LINE__ und __FILE__.

Setzen Sie Klammern um die Werte, die einem Makro übergeben werden. Damit lassen sich Fehler vermeiden. Schreiben Sie zum Beispiel

#define KUBIK(x) (x)*(x)*(x)

anstelle von

#define KUBIK(x) x*x*x

Vergessen Sie nicht, #if-Anweisungen mit #endif abzuschließen.

Befehlszeilenargumente

C-Programme können auch Argumente auswerten, die dem Programm über die Befehlszeile übergeben werden. Gemeint sind damit Informationen, die bei Aufruf des Programms nach dem Programmnamen aufgeführt werden. Wenn Sie zum Beispiel ein Programm namens progname starten wollen, das sich im aktuellen Verzeichnis befindet, würden Sie am Befehlsprompt Folgendes eingeben:

 [erik@coltrane tag20]$ ./progname schmidt maier

Die beiden Befehlszeilenargumente schmidt und maier können vom Programm während der Ausführung eingelesen werden. Stellen Sie sich diese Informationen als Argumente vor, die der main()-Funktion des Programms übergeben werden. Solche Befehlszeilenargumente erlauben es dem Anwender, dem Programm bestimmte Infor- mationen gleich beim Start und nicht erst im Laufe der Ausführung des Programms zu übergeben - was in bestimmten Situationen durchaus hilfreich sein kann. Sie können beliebig viele Befehlszeilenargumente übergeben. Beachten Sie, dass Befehlszeilenargumente nur innerhalb von main() verfügbar sind und main() dazu wie folgt definiert sein muss:

int main(int argc, char *argv[])
{
/* hier stehen die Anweisungen */
}

Der erste Parameter, argc, ist ein Integer, der die Anzahl der verfügbaren Befehlszeilenargumente angibt. Dieser Wert beträgt immer mindestens 1, da der Programmname als erstes Argument zählt. Der Parameter argv[] ist ein Array von Zeigern auf Strings. Die gültigen Indizes für dieses Array reichen von 0 bis argc - 1. Der Zeiger argv[0] zeigt auf den Programmnamen (einschließlich der Pfadinformationen), argv[1] zeigt auf das erste Argument, das auf den Programmnamen folgt, und so weiter. Beachten Sie, dass die Namen argc und argv[] nicht obligatorisch sind - Sie können jeden beliebigen gültigen C-Variablennamen verwenden, um die Befehlszeilenargumente entgegenzunehmen. Die Verwendung von argc und argv[] ist jedoch schon fast so etwas wie Tradition, und es wäre am besten, Sie würden sie auch beibehalten.

Die Argumente in der Befehlszeile werden durch beliebige Whitespace-Zeichen getrennt. Wenn Sie ein Argument übergeben wollen, das ein Leerzeichen enthält, müssen Sie das ganze Argument in doppelte Anführungszeichen setzen. Wenn Sie das Programm zum Beispiel wie folgt aufrufen

./progname schmidt "und maier"

dann ist schmidt das erste Argument (auf das argv[1] zeigt) und und maier das zweite Argument (auf das argv[2] zeigt). Listing 20.3 veranschaulicht, wie man auf Befehlszeilenargumente zugreift.

Listing 20.3: Befehlszeilenargumente an main() übergeben

1: /* Zugriff auf Befehlszeilenargumente */
2:
3: #include <stdio.h>
4:
5: int main(int argc, char *argv[])
6: {
7: int count;
8:
9: printf("Programmname: %s\n", argv[0]);
10:
11: if (argc > 1)
12: {
13: for (count = 1; count < argc; count++)
14: printf("Argument %d: %s\n", count, argv[count]);
15: }
16: else
17: puts("Es wurden keine Befehlszeilenargumente eingegeben.");
18: return 0;
19: }

./list2003
Programmname: ./list2003
Es wurden keine Befehlszeilenargumente eingegeben.

./list2003 erstes zweites "3 4"
Programmname: ./list2003
Argument 1: erstes
Argument 2: zweites
Argument 3: 3 4

Dieses Programm beschränkt sich darauf, die Befehlszeilenparameter auszugeben, die der Anwender eingegeben hat. Beachten Sie, dass Zeile 5 die oben angesprochenen Parameter argc und argv aufführt. Zeile 9 gibt den Befehlszeilenparameter aus, der immer vorhanden ist, das heißt den Programmnamen. Wie schon gesagt, lautet dieser Parameter argv[0]. Zeile 11 prüft, ob es mehr als einen Befehlszeilenparameter gibt. Warum mehr als einen und nicht mehr als keinen? Weil es immer zumindest einen gibt - den Programmnamen. Wenn es weitere Argumente gibt, werden diese von der for-Schleife in den Zeilen 13 und 14 auf dem Bildschirm ausgegeben. Andernfalls wird eine entsprechende Meldung ausgegeben (Zeile 17).

Befehlszeilenargumente fallen normalerweise in zwei verschiedene Kategorien: obligatorische, ohne die das Programm nicht ausgeführt werden kann, und optionale, wie zum Beispiel Schalter, die die Arbeitsweise des Programms steuern. Angenommen Sie haben ein Programm, das Daten aus einer Datei sortiert. Wenn Sie das Programm so schreiben, dass es den Namen der zu sortierenden Datei über die Befehlszeile entgegennimmt, gehört der Name zu den obligatorischen Informationen. Wenn der Anwender vergisst, den Dateinamen in der Befehlszeile anzugeben, muss das Programm irgendwie mit dieser Situation fertig werden (meist gibt man in so einem Fall eine Fehlermeldung aus, die den korrekten Aufruf des Programms beschreibt). Das Programm könnte weiterhin nach dem Argument /r Ausschau halten, das eine Sortierung in umgekehrter Reihenfolge veranlasst. Dieses Argument ist optional. Das Programm hält zwar Ausschau nach dem Argument, wird aber auch korrekt ausgeführt, wenn das Argument nicht übergeben wird.

Was Sie tun sollten

Was nicht

Verwenden Sie argc und argv als Variablennamen für die Befehlszeilenargumente zu main(). Den meisten C-Programmierern sind diese Namen vertraut.

Gehen Sie nicht davon aus, dass die Anwender die korrekte Anzahl an Befehlszeilenparametern eingeben. Prüfen Sie, ob die korrekte Anzahl an Argumenten eingegeben wurde, und geben Sie eine Meldung aus, die die einzugebenden Argumente erläutert, wenn zu wenig oder zu viel Argumente übergeben wurden.

Befehlszeilenargumente mit getopt()

Das Einlesen von Befehlszeilenoptionen mit argc und argv[] ist relativ einfach, solange die Zahl der Optionen klein ist. Viele Programme akzeptieren jedoch eine große Anzahl an Befehlszeilenoptionen, und das Auswerten dieser Optionen mit argc und argv[] kann recht kompliziert werden. Zum Glück gibt es unter Linux und den meisten anderen Unix-ähnlichen Betriebssystemen die Funktion getopt(), die Ihnen bei der Analyse der Befehlszeilenoptionen hilft. Diese Funktion ist in getopt.h wie folgt definiert:

int getopt(int argc, char * const argv[],const char *optstring);

Die ersten zwei Parameter an getopt() sind die beiden Argumente, die der main()- Funktion übergeben wurden, der dritte Parameter ist ein Zeichenstring. Das Schlüsselwort const im zweiten Parameter bedeutet, dass argv ein Array von konstanten Zeigern auf Strings ist. Konstante Zeiger sind Zeiger, die einmal initialisiert, auf nichts anderes mehr zeigen können. Der dritte Parameter dagegen ist ein Zeiger auf einen konstanten String. Bitte beachten Sie die Position des Schlüsselwortes const in diesem Fall. Zeiger auf eine Konstante bedeuten, dass diese Konstante eben nicht mehr verändert werden kann, wohl aber kann der Zeiger selbst noch auf andere Variablen umgelenkt werden.

Bei wiederholtem Aufruf durchsucht die Funktion getopt() das Array argv[] nach Strings, die mit »-« beginnen und von einem Zeichen aus dem String optstring gefolgt werden. Für jedes in optstring gefundene Zeichen liefert getopt() das Zeichen zurück oder EOF, wenn es in optstring keine Zeichen mehr gibt oder alle Elemente in argv[] bearbeitet wurden. Wenn irgendein Zeichen in optstring von einem Doppelpunkt gefolgt wird, wird das folgende Stringelement des Arrays argv[] als Parameter betrachtet und der char*-Zeiger optarg auf diesen Parameter gesetzt. Wenn getopt() EOF zurückliefert, wird dem Integer-Wert optind der Index des ersten Elements von argv[] zugewiesen, das von getopt() nicht analysiert wurde. Die Variablen optarg und optind sind in getopt.h als extern definiert. Falls Ihnen dies alles etwas kompliziert erscheint, möchte ich Sie auf das Beispiel in Listing 20.4 verweisen, das die Verwendung von getopt() etwas klarer machen sollte.

Listing 20.4: Befehlszeilenargumente mit getopt() auswerten.

1 : /* Befehlszeilenargumente mit getopt() auswerten. */
2 :
3 : #include <stdio.h>
4 : #include <getopt.h>
5 :
6 : int main(int argc, char *argv[])
7 : {
8 : int option;
9 :
10: while ((option = getopt (argc, argv, "abi:o:z")) != EOF)
11: {
12: switch (option)
13: {
14: case 'a' :
15: case 'b' :
16: case 'z' :
17: printf("Option : %c\n", option);
18: break;
19:
20: case 'i' :
21: printf("Eingabe : %s\n", optarg);
22: break;
23:
24: case 'o' :
25: printf("Ausgabe : %s\n", optarg);
26: break;
27:
28: default :
29: exit(1);
30: }
31: }
32:
33: for ( ; optind < argc ; optind++)
34: printf ("Opt %2d : %s\n", optind, argv [optind]);
35:
36: return 0;
37: }

./list2004 -za -i eingabedatei -b -o ausgabedatei -z peter brad erik   
Option : z
Option : a
Eingabe : eingabedatei
Option : b
Ausgabe : ausgabedatei
Option : z
Opt 8 : peter
Opt 9 : brad
Opt 10 : erik

Dieses Listing beginnt mit der Einbindung der zwei benötigten Header-Dateien in den Zeilen 3 und 4. In der main()-Funktion wird nur die Variable option vom Typ int definiert, die als Rückgabewert für die getopt()-Funktion dienen soll. In Zeile 10 wird getopt() in dem bedingten Ausdruck der while-Anweisung aufgerufen. Beachten Sie, dass argc und argv[], die Argumente an die main()-Funktion, ohne irgendwelche weiteren Änderungen an getopt() übergeben werden. Das dritte Argument an getopt() ist der Optionsstring, der in diesem Fall angibt, dass getopt() nach den einbuchstabigen Optionen a, b und z sowie den Optionen i und o Ausschau halten soll. Letztere Optionen werden von Argumenten gefolgt, die im argv[]-Array direkt hinter den Optionen abgelegt sind.

Die getopt()-Funktion kehrt zurück und setzt die Variable option auf jedes der Optionenzeichen (a, b, i, o und z), nach denen sie Ausschau halten sollte. Mit Hilfe der switch-Anweisung in Zeile 12 können die einzelnen Optionen dann verarbeitet werden. Die Optionen i und o werden in dem Optionsstring, der getopt() übergeben wird, von einem Doppelpunkt gefolgt. Deshalb zeigt die Variable optarg nach dem Einlesen dieser Optionen auf das nächste Element im Array argv[]. Der obigen Ausgabe können Sie entnehmen, dass beispielsweise die Verarbeitung der Option -i eingabedatei dazu führte, dass optarg auf den String »eingabedatei« gesetzt wurde und getopt() das Zeichen i zurücklieferte.

Zum Schluss, nachdem die Funktion getopt() alle Fälle abgehandelt hat, liefert sie den Wert EOF zurück und beendet so die while-Schleife. In der for-Schleife in den Zeilen 33 und 34 werden die restlichen Argumente aus dem argv[]-Array verarbeitet.

So flexibel und nützlich die Funktion getopt() auch sein mag, es gibt noch zwei andere in getopt.h definierte Funktionen, getopt_long() und getopt_long_only(), die noch mehr Möglichkeiten bieten. Leider würde eine Besprechung dieser Funktionen den Rahmen dieses Buches sprengen, doch soll sie dies nicht davon abhalten, die Manpages zu diesen Funktionen (man 3 getopt) aufzurufen und sich selbst über deren Verwendung zu informieren.

Programmierung mit mehreren Quelltextdateien

Bis jetzt haben alle Ihre C-Programme aus nur einer einzigen Quelltextdatei bestanden (die Header-Dateien natürlich nicht mitgezählt). Häufig ist es auch nicht nötig, den Quelltext auf mehr als eine Quelltextdatei zu verteilen, besonders solange es sich nur um kleine Programme handelt. Sie können den Quelltext eines Programms aber auch ohne Weiteres auf zwei oder mehr Dateien verteilen. Man nennt diese Vorgehensweise auch modulare Programmierung. Worin liegt der Vorteil der modularen Programmierung? In den folgenden Abschnitten möchte ich Ihnen darauf eine Antwort geben.

Die Vorteile der modularen Programmierung

Der primäre Grund für den Einsatz von modularer Programmierung ist eng mit der strukturierten Programmierung und den Funktionen verbunden. Mit zunehmender Erfahrung als Programmierer werden Sie mehr und mehr dazu übergeben, allgemeine Funktionen zu schreiben, die nicht nur in dem Programm, für das sie geschrieben wurden, eingesetzt werden können, sondern auch in anderen Programmen. Sie könnten zum Beispiel eine Reihe von allgemeinen Funktionen zur Ausgabe von Informationen auf dem Bildschirm schreiben. Indem Sie diese Funktionen in einer eigenen Datei ablegen, können Sie sie in verschiedenen Programmen, die ebenfalls Informationen auf dem Bildschirm ausgeben, wiederverwenden. Wenn Sie ein Programm schreiben, das aus mehreren Quelltextdateien besteht, wird jede einzelne Quelltextdatei des Programms als Modul bezeichnet.

Modulare Programmiertechniken

Ein C-Programm kann nur eine main()-Funktion haben. Das Modul, in dem die main()-Funktion steht, wird auch main-Modul genannt. Die anderen Module nennt man sekundäre Module. Mit jedem sekundären Modul wird normalerweise eine eigene Header-Datei verbunden - warum, werden Sie gleich erfahren. Zuerst einmal wollen wir ein paar einfache Beispiele betrachten, die die Grundkonzepte der modularen Programmierung veranschaulichen sollen. Die Listings 20.5, 20.6 und 20.7 enthalten das main-Modul, das sekundäre Modul und die Header-Datei für ein Programm, das den Anwender zur Eingabe einer Zahl auffordert und das Quadrat der eingelesenen Zahl ausgibt.

Listing 20.5: list2005.c: Das main-Modul.

1: /* Liest eine Zahl ein und gibt das Quadrat aus. */
2:
3: #include <stdio.h>
4: #include "kalkul.h"
5:
6: int main(void)
7: {
8: int x;
9:
10: printf("Geben Sie einen Integer-Wert ein: ");
11: scanf("%d", &x);
12: printf("\nDas Quadrat von %d ist %ld.\n", x, sqr(x));
13: return 0;
14: }

Listing 20.6: kalkul.c: Das sekundäre Modul.

1: /* Das Modul mit den Rechenfunktionen. */
2:
3: #include "kalkul.h"
4:
5: long sqr(int x)
6: {
7: return ((long)x * x);
8: }

Listing 20.7: kalkul.h: Die Header-Datei für kalkul.c.

1: /* kalkul.h: Header-Datei für kalkul.c. */
2:
3: long sqr(int x);
4:
5: /* Ende von kalkul.h */

Geben Sie einen Integer-Wert ein: 100

Das Quadrat von 100 ist 10000.

Lassen Sie uns die Komponenten dieser drei Dateien etwas ausführlicher betrachten. Die Header-Datei, kalkul.h, enthält den Prototyp für die Funktion sqr() aus kalkul.c. Da jedes Modul, das die Funktion sqr() verwendet, den Prototyp von sqr() kennen muss, muss es auch kalkul.h einbinden.

Das sekundäre Modul kalkul.c enthält die Definition der Funktion sqr(). Mit der #include-Direktive wird die Header-Datei kalkul.h eingebunden. Beachten Sie, dass der Name der Header-Datei in doppelten Anführungszeichen und nicht in spitzen Klammern steht (den Grund dafür liefere ich Ihnen gleich nach).

Das main-Modul, list2005.c, enthält die main()-Funktion. Dieses Modul bindet ebenfalls die Header-Datei kalkul.h ein. Um die zwei C-Quelltextdateien zu einem Programm zusammenzubinden, müssen Sie gcc folgendermaßen aufrufen:

gcc -Wall -ggdb kalkul.c list2005.c -o list2005

Beachten Sie, dass dieser Kompilierbefehl zwei C-Quelltextdateien enthält. Sie können mit dem gcc-Compiler mehr als eine C-Quelltextdatei gleichzeitig kompilieren.

Statt beide C-Quelltextdateien gleichzeitig zu kompilieren, können Sie auch alternativ jede Quelltextdatei einzeln kompilieren und so Zwischendateien, so genannte Objektdateien, erzeugen, die die Extension .o erhalten. Diese Objektcode-Dateien weisen ein besonderes Format auf. Sie enthalten den vom Compiler erzeugten Maschinencode sowie Informationen, die nötig sind, um die Datei mit anderen Objektdateien zu einer ausführbaren Datei zusammenbinden (linken) zu können. Um aus der C-Quelltextdatei list2005.c eine Objektdatei zu erzeugen, müssen Sie folgenden Befehl eingeben:

gcc -Wall -ggdb -c list2005.c

Der Befehlzeilenoption -c teilt dem Compiler mit, die C-Quelltextdatei zu kompilieren, und - vorausgesetzt es gibt keine Fehler - eine Objektcode-Datei (in diesem Fall list2005.o) zu erzeugen und auf der Festplatte abzulegen. Entsprechend wird der Befehl

gcc -Wall -ggdb -c kalkul.c

eine Objektdatei namens kalkul.o erzeugen. Um diese beiden Dateien zu einer ausführbaren Datei zusammenzubinden, führen Sie den folgenden Befehl aus:

gcc -Wall -ggdb kalkul.o list2005.o -o list2005

Dieser Befehl teilt dem Compiler mit, die zwei Objektdateien kalkul.o und list2005.o zu linken und eine ausführbare Datei namens list2005 zu erzeugen.

.o-Dateien verwenden

Warum kompiliert man die C-Quelltextdateien getrennt in Objektdateien und linkt sie dann zusammen, anstatt alle C-Quelltextdateien direkt in eine ausführbare Datei zu kompilieren. Betrachten wir einen Fall, in dem ein Projekt aus mehreren C- Quelltextdateien besteht. Jede dieser Quelltextdateien kann Hunderte Zeilen Code enthalten. Wann immer Sie eine kleine Änderung an dem Programm vornehmen, müssten Sie alle Dateien des Programms neu kompilieren - was unter Umständen ziemlich viel Zeit in Anspruch nimmt. Wenn Sie hingegen jede Quelltextdatei einzeln in eine Objektdatei kompilieren und diese Objektdateien dann anschließend zusammenbinden, muss nur die geänderte Datei neu kompiliert werden. Die neue Objektdatei kann dann mit den restlichen, bereits bestehenden Objektdateien gelinkt werden, um die ausführbare Datei zu erzeugen. Mit anderen Worten, Sie müssen nur das gerade geänderte Modul neu kompilieren. Für die nicht bearbeiteten Quelltextdateien können Sie die alten Objektdateien wiederverwenden.

Angewendet auf unser obiges Beispiel heißt dies, dass man das Programm nach einer Änderung im Modul list2005.c mit folgendem Befehl neu erstellen könnte:

gcc -Wall list2005.c kalkul.o -o list2005

Dieser Befehl kompiliert list2005.c, linkt die Datei mit der Objektdatei kalkul.o, die bereits vorher erzeugt worden ist, und erzeugt dann die ausführbare Datei list2005.

Modulkomponenten

Wie Sie sehen, ist die Technik des Kompilierens und Linkens eines Programms aus mehreren Modulen ziemlich einfach. Haben Sie diese Technik erst einmal verstanden, bleibt nur noch die Frage zu klären, wie der Code auf die Datei verteilt werden soll. Dieser Abschnitt gibt Ihnen dazu einige Anhaltspunkte.

Das sekundäre Modul sollte allgemeine Dienstfunktionen enthalten - das sind Funktionen, die Sie auch in anderen Programmen verwenden wollen. Es ist allgemein üblich, für jede Kategorie von Funktionen ein eigenes sekundäres Modul anzulegen - zum Beispiel tastatur.c für die Tastaturfunktionen, bildschirm.c für die Funktionen zur Bildschirmausgabe und so weiter.

Normalerweise gibt es zu jedem sekundären Modul eine Header-Datei. Die Header- Dateien tragen üblicherweise den gleichen Namen wie das zugehörige Modul, allerdings mit der Extension .h. In die Header-Datei gehören:

Da die Header-Dateien in mehr als eine Quelltextdatei eingebunden werden können, werden Sie verhindern wollen, dass Teile der Header-Datei mehrfach kompiliert werden. Sie können das mit den Präprozessor-Direktiven für die bedingte Kompilierung verhindern (wurde weiter vorn in diesem Kapitel besprochen).

Externe Variablen und modulare Programmierung

In vielen Fällen findet die Datenkommunikation zwischen dem main-Modul und dem sekundären Modul nur über Argumente statt, die den Funktionen übergeben und aus den Funktionen wieder zurückgegeben werden. In diesem Fall brauchen Sie keine besonderen Vorkehrungen hinsichtlich der Sichtbarkeit irgendwelcher Variablen zu treffen. Wie aber steht es mit einer globalen Variable, die in beiden Modulen sichtbar sein muss?

Erinnern wir uns an Tag 11, »Gültigkeitsbereiche von Variablen«, dass globale Variablen außerhalb der Funktionen deklariert werden. Eine globale Variable ist in der ganzen Quelltextdatei, in der sie deklariert wurde, sichtbar. Sie ist jedoch nicht automatisch auch in anderen Modulen sichtbar. Um sie modulübergreifend sichtbar zu machen, müssen Sie die Variable in jedem Modul mit dem Schlüsselwort extern deklarieren. Wenn Sie zum Beispiel in dem main-Modul eine globale Variable wie folgt deklariert haben:

float zins_rate;

machen Sie zins_rate in einem sekundären Modul sichtbar, indem Sie die folgende Deklaration in das sekundäre Modul (außerhalb der Funktionen) mit aufnehmen

extern float zins_rate;

Das Schlüsselwort extern teilt dem Compiler mit, dass sich die ursprüngliche Deklaration von zins_rate (die den Speicherplatz für die Variable reserviert) irgendwo anders befindet, die Variable in diesem Modul aber ebenfalls sichtbar gemacht werden soll. Alle extern-Variablen sind statischer Natur und für alle Funktionen in dem Modul sichtbar. Abbildung 20.1 veranschaulicht die Verwendung des Schlüsselworts extern in einem Mehrdateienprogramm.

Abbildung 20.1:  Mit dem Schlüsselwort extern wird eine globale Variable modulübergreifend sichtbar.

In Abbildung 20.1 ist die Variable x über alle drei Module hinweg sichtbar, während y nur in dem main-Modul und dem sekundären Modul 1 sichtbar ist.

Was Sie tun sollten

Was nicht

Erzeugen Sie generische Funktionen in deren eigenen Quelltextdateien. Auf diese Weise kann man sie in alle anderen Programme linken, die sie benötigen.

Versuchen Sie nicht mehrere Quelltextdateien zusammen zu kompilieren, wenn mehr als ein Modul eine main()-Funktion enthält. Sie dürfen nur eine main()-Funktion haben.

Verwenden Sie nicht immer die C- Quelltextdateien, wenn Sie mehrere Dateien zusammen kompilieren. Wenn Sie eine Quelltextdatei in eine Objektdatei kompilieren, kompilieren Sie sie nur, wenn die Datei geändert wurde. So können Sie eine ganze Menge Zeit sparen.

Die make-Datei

Fast alle Systeme, die über einen C-Compiler verfügen, sind gleichzeitig mit einem make-Dienstprogramm ausgestattet, das Ihnen die Erstellung von Programmen aus mehreren Quelltextdateien erleichtern kann. Linux ist da keine Ausnahme. Es verfügt über GNU make - ein gutes Exemplar dieses Dienstprogramms. Die Unterschiede zwischen den make-Programmen der verschiedenen Plattformen sind geringfügig, so dass wir in diesem Buch nicht näher darauf eingehen werden.

Was genau macht make? Es erlaubt Ihnen die Arbeit mit Makefile-Dateien - so genannt, weil sie Ihnen helfen, Programme zu »machen« (im Englischen: make). Diese Hilfe besteht vor allem darin, dass Sie in den Makefile-Dateien (die Sie selbst aufsetzen müssen) die Abhängigkeiten zwischen den Quelldateien festhalten können und diese folglich nicht bei jeder Neukompilation rekonstruieren müssen.

Angenommen Sie haben ein Projekt, das über ein main-Modul namens programm.c und über ein sekundäres Modul namens sekund.c verfügt. Dazu gibt es zwei Header- Dateien, programm.h und sekund.h. Die Quelltextdatei programm.c bindet beide Header-Dateien ein, während sekund.c nur sekund.h einbindet. Der Code in programm.c ruft Funktionen aus sekund.c auf.

programm.c ist von den beiden Header-Dateien, die es eingebunden hat, abhängig. Wenn Sie eine Änderung an einer der beiden Header-Dateien vornehmen, müssen Sie programm.c neu kompilieren, damit die Änderungen auch in dem main-Modul wirksam werden. sekund.c hingegen hängt nur von sekund.h ab und nicht von programm.h. Wenn Sie also programm.h ändern, besteht kein Grund, sekund.c neu zu kompilieren - Sie können weiter die bestehende Objektdatei sekund.o, die bei der letzten Kompilierung von sekund.c erzeugt wurde, verwenden.

Eine make-Datei beschreibt die Abhängigkeiten, die in einem Projekt bestehen - wie zum Beispiel die oben angesprochen Abhängigkeiten. Immer wenn Sie eine oder mehrere Ihrer Quelltextdateien bearbeiten, rufen Sie anschließend das make- Dienstprogramm auf, um die Makefile-Datei »auszuführen«. Dieses Programm untersucht die Zeit- und Datumsstempel der Quelltextdatei und der Objektdateien und weist den Compiler an, auf der Basis der von Ihnen definierten Abhängigkeiten nur die Dateien neu zu kompilieren, die von der/den geänderten Datei(en) abhängen. Das hat zur Folge, dass keine unnötigen Kompilierungen ausgeführt werden und Sie mit höchster Effizienz arbeiten können. In Listing 20.8 sehen Sie eine minimale Makefile- Datei für das Programm aus Listing 20.5.

Listing 20.8: Makefile: eine Makefile-Datei für list2005.c.

1 : # Makefile-Datei für Listing 20.5
2 :
3 : CC = gcc
4 : CFLAGS = -Wall -ggdb # Alle Warnungen und Debuggen
5 :
6 : list2005 : list2005.o kalkul.o
7 : $(CC) list2005.o kalkul.o -o list2005
8 :
9 : list2005.o : list2005.c kalkul.h
10: $(CC) $(CFLAGS) -c list2005.c
11:
12: kalkul.o : kalkul.c kalkul.h
13: $(CC) $(CFLAGS) -c kalkul.c
14:
15: clean :
16: rm -f *.o

Wie im Falle der Quelltext-Listings in diesem Buch sind die Zahlen und der Doppelpunkt am linken Rand nicht Teil der Makefile-Datei. Genau genommen sollten die Zeilen 1, 3, 4, 6, 9 und 12 ganz links ausgerichtet werden. So beginnt zum Beispiel die Zeile 1 mit dem Hash-Zeichen (#). Außerdem möchte ich Sie darauf aufmerksam machen, dass die Zeilen 7, 10 und 13 nicht an der ersten Zeichenposition in der Zeile beginnen dürfen. Je nach den Einstellungen Ihres Editors können Sie diese Zeilen mit der Tabulatortaste um bis zu acht Zeichen (einschließlich) einrücken. Acht Zeichen sind die gängige Tabulatoreinstellung.

Lassen Sie uns diese Makefile-Datei einmal genau studieren und feststellen, was man damit überhaupt machen kann. Wie Sie vielleicht geraten haben, ist die erste Zeile ein Kommentar. In Makefile-Dateien beginnen die Kommentare mit dem Hash-Zeichen und erstrecken sich bis zum Ende der aktuellen Zeile. Die Zeilen 3 und 4 richten zwei Variablen (CC und CFLAGS) ein, die fast das gleiche Verhalten aufweisen wie C- Variablen. Die Werte dieser Variablen sind die Strings, die jeweils rechts vom Gleichheitszeichen stehen (ohne den Kommentar natürlich). Wenn das make- Programm die Makefile-Datei verarbeitet, sucht es nach Variablennamen in Klammern, denen ein Dollarzeichen vorangestellt wurde, und ersetzt diese durch die Werte der Variablen. Wo auch immer das make-Programm nach Zeile 3 auf $(CC) trifft, ersetzt es also den ganzen Ausdruck durch gcc, den Wert der Variablen CC.

Die Zeilen 6, 9 und 12 definieren auf der linken Seite des Doppelpunkts ein Ziel und auf der rechten Seite eine Abhängigkeitsliste. Ein Ziel ist etwas, das make erst erstellen soll, die Abhängigkeitsliste teilt make mit, welche Dateien benötigt werden, um das Ziel immer auf dem neuesten Stand zu halten. Zeile 6 der Makefile-Datei besagt, dass das Objekt list2005 (bei dem es sich um ein Programm handelt) von den zwei Objektdateien list2005.o und kalkul.o abhängt. Das ergibt einen Sinn, denn wenn eine dieser zwei Objektdateien neueren Datums als das Programm ist, muss das Programm neu erstellt werden. Entsprechend ist auch das Paar Ziel:Abhängigkeitsliste in Zeile 9 zu verstehen. Wenn eine der Dateien, list2005.c oder kalkul.h, ein neueres Datum aufweist als die Objektdatei list2005.o, muss die Objektdatei neu erstellt werden. Zeile 15 sieht aus wie ein Ziel ohne Abhängigkeitsliste, und genau so ist es. Doch dazu später mehr.

Zu jedem Ziel:Abhängigkeitsliste-Paar kann man eine beliebige Zahl von Zeilen angeben, die dem make-Programm mitteilen, wie das Ziel erstellt werden soll - in manchen Fällen werden keine Anweisungen benötigt. Für das Ziel in Zeile 6 - das Programm list2005 - stehen die Anweisungen zum Erstellen in Zeile 7. Denken Sie daran, dass das make-Programm $(CC) durch gcc ersetzt und dann die ganze Zeile als einen eigenen Programmaufruf ausführt. In diesem Fall bedeutet dies, dass, wenn eine der Dateien, list2005.o oder kalkul.o, ein neueres Datum hat als das Programm list2005, das make-Programm den folgenden Befehl ausführt:

gcc list2005.o kalkul.o -o list2005

Um die Makefile-Datei mit dem make-Programm zu verwenden, müssen Sie die Makefile-Datei in das gleiche Verzeichnis wie die C-Quelltextdateien list2005.c und kalkul.c ablegen und am Befehlsprompt make eingeben. Soweit der C-Quelltext keine Fehler enthält, sollten Sie daraufhin folgende Ausgabe sehen:

gcc -Wall -ggdb -c list2005.c
gcc -Wall -ggdb -c kalkul.c
gcc list2005.o kalkul.o -o list2005

Wenn alles nach Plan gelaufen ist, sollte jetzt im gleichen Verzeichnis das Programm list2005 zu finden sein. Beachten Sie, dass Sie bei erneuter Eingabe des Befehls make die Meldung erhalten, dass das Programm auf dem neuesten Stand ist:

make: 'list2005' is up to date

Dies bedeutet, dass make die Abhängigkeitsliste durchgegangen ist und festgestellt hat, dass das Hauptziel, list2005, aktueller ist als die Dateien, von denen das Ziel abhängig ist.

Um dies zu überprüfen, laden Sie die Datei kalkul.c in Ihren Editor, fügen Sie irgendwo in der Datei ein paar Leerzeichen ein und speichern Sie die Datei ab. Führen Sie jetzt das make-Programm erneut aus. Die Ausgabe sollte ungefähr wie folgt lauten:

gcc -Wall -ggdb -c kalkul.c
gcc list2005.o kalkul.o -o list2005

Das make-Programm hat festgestellt, dass die Datei kalkul.c aktueller war als die zugehörige Objektdatei kalkul.o. Aus diesem Grund führt es den Compiler-Befehl in der ersten Zeile aus, der aus der neuen C-Datei eine neue Objektdatei erzeugt. Danach überprüft make die Ziel:Abhängigkeitsliste-Paare und stellt fest, dass die neu erzeugte Objektdatei kalkul.o aktueller ist als das Programm list2005, das von kalkul.o abhängig ist. Folglich wird der zweite Compiler-Befehl ausgeführt. Beachten Sie, dass make ohne Probleme die Objektdatei list2005.o verwenden, und deshalb auf die Neukompilation von list2005.c verzichten konnte.

Wenn Sie am Befehlsprompt nur make eingeben, wird das erste Ziel aus der Makefile- Datei aktualisiert. Wenn Sie ein anderes Ziel erstellen wollen, geben Sie den Namen des Ziels beim Aufruf von make mit an. So bewirkt der Aufruf make kalkul.o, dass kalkul.o erstellt wird. Das letzte Ziel in der make-Datei heißt clean. clean hat keine Abhängigkeiten. Wenn Sie am Befehlsprompt make clean eingeben, wird die Anweisung zum Erstellen des Ziels clean ausgeführt. In unserem Beispiel werden mit dieser Anweisung alle Objektdateien im aktuellen Verzeichnis gelöscht.

Listing 20.9 enthält eine zweite, etwas kompliziertere Makefile-Datei namens Makefile2.

Listing 20.9: Makefile2: eine etwas kompliziertere Makefile-Datei.

1 : # Makefile-Datei für mehrere Ziele.
2 :
3 : CC = gcc
4 : CFLAGS = -Wall -ggdb
5 :
6 : all : list2004 list2005
7 :
8 : list2004 : list2004.c
9 : $(CC) $(CFLAGS) list2004.c -o list2004
10:
11: list2005 : list2005.o kalkul.o
12: $(CC) list2005.o kalkul.o -o list2005
13:
14: list2005.o : list2005.c kalkul.h
15: $(CC) $(CFLAGS) -c list2005.c
16:
17: kalkul.o : kalkul.c kalkul.h
18: $(CC) $(CFLAGS) -c kalkul.c
19:
20: clean :
21: rm -f $(TARGETS) *.o

Der Hauptunterschied zwischen dieser und der vorangehenden Makefile-Datei besteht darin, dass das erste Ziel in Zeile 6 all heißt. Dieses Ziel weist zwei Abhängigkeiten auf, die Programme list2004 und list2005, enthält aber keine Anweisungen zum Erstellen. Das geht in Ordnung, da jede dieser beiden Abhängigkeiten selbst wieder ein Ziel darstellt: list2004 aus Zeile 8 und list2005 aus Zeile 11. Ein weiterer Unterschied besteht darin, dass diese Datei Makefile2 heißt, das make-Programm aber bei seiner Ausführung in dem aktuellen Verzeichnis nach einer Datei namens Makefile oder makefile Ausschau hält. Sie können dieses Verhalten anpassen, indem Sie make den Namen der make-Datei übergeben, mit der es arbeiten soll:

make -f Makefile2

Die hier vorgestellten make-Dateien sind zwar noch sehr einfach, aber unleugbar doch sehr nützlich. Das make-Programm kann auch dazu verwendet werden, um Projekte zu erstellen, die in anderen Sprachen als C geschrieben sind. Fast jedes Programm für Linux, das mit Quelltext vertrieben wird, enthält zumindest eine make-Datei. Viele dieser Projekte enthalten C-Quelltext, der über mehrere Unterverzeichnisse verteilt ist. Die oberste Makefile-Datei, die im Hauptverzeichnis steht, verzweigt in jedes Unterverzeichnis und führt die dort stehenden untergeordneten Makefile-Dateien eine nach der anderen aus. In Projekten dieser Größe besteht die make-Datei oft aus mehreren hundert Zeilen und wird oft durch ein anderes Programm automatisch erzeugt. Die make-Dateien, die wir heute betrachtet haben, mögen zwar etwas einfach sein, aber sie können problemlos erweitert werden, so dass sie Ihnen auch dann von Nutzen sind, wenn Ihre Programme größer werden und Sie Ihren Quelltext auf mehrere Dateien verteilen.

Gemeinsam genutzte Bibliotheken

Wie die meisten anderen Mitglieder der Unix-Familie unterstützt auch Linux die Verwendung von Bibliotheken. So kann ein Programm Funktionen aufrufen, die nicht im Programmcode sondern in einer eigenständigen Bibliotheksdatei untergebracht sind. Diese Bibliotheken werden auch gemeinsam genutzte oder dynamisch gelinkte Bibliotheken genannt. Wenn ein Programm, das eine gemeinsam genutzte Bibliothek verwendet, aufgerufen wird, wird das Programm kurz vor dem Start dynamisch mit der gemeinsam genutzten Bibliothek verbunden. Die Bezeichnung gemeinsam genutzte Bibliothek resultiert daraus, dass mehrere Programme gleichzeitig mit einer gemeinsam genutzten Bibliothek verbunden (gelinkt) werden können (und nicht für jedes Programm eine eigene Kopie dieser Bibliothek angelegt werden muss). Diese Technik schont ganz offensichtlich den Arbeitsspeicher, da viele Programme sich eine Kopie einer Bibliothek teilen.

Die Programme in diesem Buch haben schon die ganze Zeit gemeinsam genutzte Bibliotheken verwendet, ohne dass Sie, der Programmierer, davon etwas gemerkt haben. Das liegt daran, dass der C-Compiler die C-Programme automatisch mit einer Bibliothek linkt, die alle grundlegenden Standard-C-Bibliotheksfunktionen enthält - beispielsweise printf(), puts(), scanf(), fgets() und so weiter.

Ein Programm namens ldd, das auf allen Linux-Systemen vorhanden ist, teilt Ihnen mit, welche gemeinsam genutzten Bibliotheken ein Programm verwendet. Rufen Sie ldd für einige der Programme in dieser Lektion auf. Wenn Sie ldd beispielsweise mit dem Programm aus Listings 20.5 aufrufen, werden Sie ungefähr folgende Ausgabe erhalten:

[erik@coltrane tag20]$ ldd list2005
libc.so.6 => /lib/libc.so.6 (0x40019000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

Wie man sieht, verwendet das Programm list2005 zwei gemeinsam genutzte Bibliotheken. Die erste heißt libc und ist die Standard-C-Bibliothek. Sie enthält alle Funktionen der Standard-C-Bibliothek. Die andere Bibliothek enthält die Funktionen, die vom dynamischen Linker benötigt werden und im Rahmen dieses Buches nicht besprochen werden können.

Am Tag 17, »Die Bibliothek der C-Funktionen«, haben wir den Einsatz der mathematischen Funktionen - wie exp(), sin() und so weiter - betrachtet. Um diese Funktionen verwenden zu können, mussten wir dem Compiler durch Anhängen des zusätzlichen Arguments -lm mitteilen, dass er die mathematische Bibliothek hinzulinkt. In den Verzeichnissen /lib, /usr/lib und vielleicht auch /usr/local/lib finden Sie eine große Zahl von Bibliotheken, deren Namen alle mit lib beginnen. Um eine Bibliothek namens libname zu Ihrem Programm hinzuzulinken, geben Sie -lname als Argument zum link-Befehl an.

Um die Funktionen einer gegebenen Bibliothek nutzen zu können, müssen Sie die entsprechende Header-Datei in die C-Quelltextdateien einbinden. Als wir am Tag 17 die mathematischen Funktionen verwendet haben, haben wir die Header-Datei math.h eingebunden, in der die mathematischen Funktionen deklariert sind, und an den Befehl compile das Argument -lm anhängt, um die mathematische Bibliothek automatisch mit dem Programm zu verbinden.

Unter Linux verwendet der gcc-Compiler eine Reihe von Standardverzeichnissen, in denen er nach Header-Dateien und Bibliotheken sucht. Für die Header-Dateien lauten die Verzeichnisse /usr/include und /usr/local/include/, während die Bibliotheken in den Verzeichnissen /lib, /usr/lib und /usr/local/lib gesucht werden. Darüber hinaus kann man den Suchpfad für die Header-Dateien und Bibliotheken um eigene Verzeichnisse erweitern. Um den gcc aufzufordern, das Verzeichnis /home/erik/ headers in den Suchpfad für Header-Dateien aufzunehmen, müssen Sie der Befehlszeile das Argument -I/home/erik/headers hinzufügen. Wenn Sie weitere Bibliotheken aus dem Verzeichnis /home/erik/lib verfügbar machen wollen, erweitern Sie den link-Befehl um -L/home/erik/lib.

Am Tag 21, »Einführung in die GUI-Programmierung mit GTK+«, werden wir von diesen Möglichkeiten Gebrauch machen. Die X-GUI-Programmierung erfordert die Einbindung von Bibliotheken und Header-Dateien, die nicht im Standardsuchpfad zu finden sind.

Zusammenfassung

Die heutige Lektion behandelte einige der fortgeschritteneren Programmier-Tools, die für C-Compiler verfügbar sind. Zuerst haben Sie gesehen, wie Sie Präprozessor- Direktiven für die Erzeugung von Funktionsmakros, für die bedingte Kompilierung und andere Aufgaben nutzen können. In diesem Zusammenhang haben Sie auch die vordefinierten Funktionsmakros kennen gelernt, die der Compiler für Sie bereitstellt. Des Weiteren habe ich Ihnen gezeigt, wie Sie Programme erstellen, deren Quelltext auf mehrere Dateien oder Module verteilt ist. Diese Technik, auch modulare Programmierung genannt, macht es sehr einfach, allgemeine Funktionen in mehr als einem Programm zu verwenden.

Fragen und Antworten

Frage:
Wurden in der heutigen Lektion alle vordefinierten Makros und Präprozessor- Direktiven vorgestellt?

Antwort:
Nein. Die hier vorgestellten Makros und Direktiven werden von fast allen Compilern unterstützt. Die meisten Compiler, einschließlich gcc, stellen darüber hinaus aber noch eigene Makros und Konstanten zur Verfügung.

Frage:
Ist der folgende Funktionskopf akzeptabel, wenn man Befehlszeilenargumente für main() übernehmen möchte?

main( int argc, char **argv);

Antwort:
Diese Frage können Sie wahrscheinlich schon selbst beantworten. Die Deklaration verwendet einen Zeiger auf einen Zeichenzeiger statt eines Zeigers auf ein Zeichenarray. Da ein Array ein Zeiger ist, ist obige Definition praktisch die gleiche, wie die, die in der heutigen Lektion vorgestellt wurde. Im Übrigen wird obige Form recht häufig verwendet. (Hintergrundinformationen zu diesen Konstruktionen finden Sie am Tag 7, »Numerische Arrays«, und am Tag 9, »Zeichen und Strings«).

Frage:
Woher weiß der Compiler, welchen Dateinamen die ausführbare Datei tragen soll, wenn diese aus mehreren Quelltextdateien erstellt wird?

Antwort:
Vielleicht denken Sie, dass der Compiler den Namen der Datei nimmt, in der die main()-Funktion steht. Dies ist jedoch nicht der Fall. Unter Linux und den meisten anderen Unix-ähnlichen Betriebssystemen heißt die Ausgabedatei standardmäßig a.out, es sei denn, dem Compiler wird explizit ein anderer Name mitgeteilt.

Frage:
Müssen Header-Dateien die Extension .h aufweisen?

Antwort:
Nein. Eine Header-Datei kann einen beliebigen Namen aufweisen. Es ist allerdings gängige Praxis, die Extension .h zu verwenden.

Frage:
Kann ich beim Einbinden von Header-Dateien explizit einen Pfad angeben?

Antwort:
Ja. Wenn Sie den Pfad zu der Header-Datei angeben wollen, setzen Sie in der include-Anweisung Pfad und Namen der Header-Datei in Anführungszeichen.

Workshop

Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, sowie Übungen, die Sie anregen sollen, das Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Die Lösungen zu den Fragen und den Übungen finden Sie in Anhang C.

Quiz

  1. Was bedeutet der Begriff modulare Programmierung?
  2. Was ist in der modularen Programmierung das »main-Modul«?
  3. Warum sollten Sie bei der Definition eines Makros alle Argumente in Klammern setzen?
  4. Was sind die Für und Wider bei der Verwendung von Makros im Vergleich zu normalen Funktionen?
  5. Was macht der Operator defined()?
  6. Welche Direktive muss immer zusammen mit #if verwendet werden?
  7. Was bewirkt die #include-Direktive?
  8. Worin liegt der Unterschied zwischen der Codezeile
    #include <meinedatei.h>
  9. und der folgenden Codezeile:
    #include "meinedatei.h"
  10. Wofür wird __DATE__ verwendet?
  11. Worauf zeigt argv[0]?

Übungen

Aufgrund der vielen möglichen Lösungen gibt es zu den folgenden Übungen keine Antworten.

  1. Verwenden Sie Ihren Compiler, um mehrere Quelltextdateien in eine einzige ausführbare Datei zu kompilieren (Sie können dazu die Listings 20.1, 20.2 und 20.3 oder Ihre eigenen Listings verwenden).
  2. Schreiben Sie eine Fehlerroutine, die als Argumente einen Fehlercode, eine Zeilennummer und den Modulnamen übernimmt. Die Routine soll eine formatierte Fehlermeldung ausgeben und dann das Programm abbrechen. Verwenden Sie vordefinierte Makros für die Zeilennummer und den Modulnamen. (Übergeben Sie die Zeilennummer und den Modulnamen von der Stelle, an der der Fehler aufgetreten ist.) Die Fehlermeldung könnte beispielsweise wie folgt aussehen:
    modul.c (Zeile ##): Fehlercode ##
  3. Überarbeiten Sie die Funktion aus Übung 2, und erzeugen Sie leichter verständliche Fehlermeldungen. Legen Sie in Ihrem Editor eine Textdatei an, in der Sie die Fehlercodes und die zugehörigen Meldungstexte abspeichern. Eine solche Datei könnte folgende Informationen enthalten.
    1    Fehler Nummer 1
    2 Fehler Nummer 2
    90 Fehler beim Öffnen der Datei
    100 Fehler beim Lesen der Datei
  4. Nennen Sie die Datei fehler.txt. Durchsuchen Sie die Datei mit Ihrer Fehlerroutine und geben Sie die Fehlermeldung aus, die zu dem übergebenen Fehlercode gehört.
  5. Wenn Sie ein modulares Programm schreiben, kann es passieren, dass einige Header-Dateien mehr als einmal eingebunden werden. Verwenden Sie die Präprozessor-Direktiven, um das Gerüst einer Header-Datei zu schreiben, die nur einmal kompiliert wird.
  6. Schreiben Sie ein Programm, das als Befehlszeilenparameter zwei Dateinamen übernimmt. Das Programm soll die erste Datei in die zweite Datei kopieren. (Siehe Tag 15, »Mit Dateien arbeiten«, wenn Sie Hilfe beim Umgang mit Dateien benötigen.)
  7. Dies ist die letzte Übung dieses Buches, und was hier programmiert werden soll, entscheiden Sie selbst. Wählen Sie eine Programmieraufgabe aus, die Sie interessiert und Ihnen gleichzeitig nützt. Sie könnten zum Beispiel ein Programm schreiben, mit dem Sie Ihre CD-Sammlung verwalten, oder ein Programm, mit dem Sie Ihr Scheckbuch kontrollieren, oder auch ein Programm, mit dem Sie die Finanzierung eines geplanten Hauskaufes durchrechnen können. Es gibt nichts Besseres als die Beschäftigung mit echten Programmierproblemen, um die eigenen Programmierfähigkeiten zu schulen und zu verbessern und sich all das in Erinnerung zu rufen, was Sie bisher in diesem Buch gelernt haben.


vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbackKapitelanfangnächstes Kapitel


© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH