In dieser Lektion werden einige fortgeschrittene Möglichkeiten des C-Compilers besprochen. Heute lernen Sie:
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
hat zwei Einsatzbereiche: Sie erzeugt sowohl
symbolische Konstanten als auch Makros.
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.
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);
printf("%f", HAELFTEVON(x[1] + y[2]));
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");
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 );
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.
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.
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.
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.
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.
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 #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.
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.
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.
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.
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.
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.
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.
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
.
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:
#define
-Direktiven für alle symbolischen Konstanten und Makros, die im Modul
verwendet werden
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).
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.
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.
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.
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.
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.
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.
main
-Modul«?
defined()
?
#if
verwendet werden?
#include
-Direktive?
#include <meinedatei.h>
#include "meinedatei.h"
__DATE__
verwendet?
argv[0]
?
Aufgrund der vielen möglichen Lösungen gibt es zu den folgenden Übungen keine Antworten.
modul.c (Zeile ##): Fehlercode ##
1 Fehler Nummer 1
2 Fehler Nummer 2
90 Fehler beim Öffnen der Datei
100 Fehler beim Lesen der Datei
fehler.txt
. Durchsuchen Sie die Datei mit Ihrer
Fehlerroutine und geben Sie die Fehlermeldung aus, die zu dem übergebenen
Fehlercode gehört.