Aufteilung der Quelltexte
|
Willemers Informatik-Ecke
Wenn ein Programm mehrere Klassen enthält, ist es schon aus Übersichtsgründen
sinnvoll, den Quelltext auf mehrere Dateien zu verteilen.
Während die Sprache Java dem Programmierer vorschreibt, für jede Klasse eine
eigene Quelltextdatei zu verwenden, steht es dem C++-Programmierer frei, die
Unterteilung nach eigenen Vorstellungen vorzunehmen.
Mehrere gute Gründe sprechen dafür, ein
größeres Programm auf mehrere Dateien aufzuteilen:
- Es wird schwierig, in großen Texten bestimmte Funktionen oder Klassen
zu finden. Die Aufteilung sollte ein Auffinden erleichtern.
Dateien sollten darum sprechende Namen haben. Die Übersicht innerhalb der
Dateien muss gewahrt bleiben.
- Wenn im Team gearbeitet wird, sollte die Situation vermieden werden,
dass zwei Programmierer an der gleichen Datei arbeiten müssen und sich
dadurch gegenseitig ihre Änderungen zerstören. Je feiner das Programm in
Quelltextdateien zerlegt ist, desto geringer ist das Risiko, dass sich
zwei Programmierer gegenseitig stören.
- Dateien müssen nur dann übersetzt werden, wenn sie geändert werden.
Je mehr Dateien also unverändert bleiben können, desto schneller ist der
Arbeitswechsel zwischen Editieren, Übersetzen und Testen.
Dateikennungen
Quelltexte
Die Quelltextdateien von C-Programmen enden üblicherweise mit einem Punkt
und einem kleinen c. Bei C++-Programmen wurden verschiedene Endungen
vorgeschlagen. Der Vorschlag, ein großes C zu verwenden, scheiterte daran,
dass der Unterschied in den Dateisystemen von Microsoft nicht signifikant ist.
Häufig sieht man die Endung cc, aber inzwischen scheint sich die Endung cpp
durchgesetzt zu haben. Header-Dateien verwenden ein kleines h, und zwar sowohl
in C als auch in C++. Zeitweise wurde auch hpp verwandt. Bei den Header-Dateien
der Standardbibliotheken wurde die Endung nun ganz weggelassen.
Objektdatei
Der Compiler übersetzt jede Quelltextdatei einzeln und erzeugt daraus je
eine Objektdatei. Eine Objektdatei hat unter UNIX meist die Endung o, unter
MS-Windows ist obj häufiger anzutreffen.
Die Objektdateien werden bei fehlerfreier Übersetzung vom Linker zu einem
Programm zusammengesetzt.
Bibliotheken
Um aus den Objektdateien ein lauffähiges Programm zu machen, benötigt der
Linker aber noch Standardbibliotheken, die dem Entwicklungspaket beiliegen.
Eine solche Bibliotheksdatei hat unter Windows meist die Endung lib.
Unter UNIX beginnen die Dateien mit der Silbe lib und enden mit .a.
Deklaration und Definition
Klassen
Die Begriffe >>Definition<< und >>Deklaration<< werden bei der Klasse etwas
anders verwendet, als das bei Variablen und Funktionen der
Fall ist. Dort können Sie sich mit der Eselsbrücke helfen, dass eine
Deklaration nur eine Beschreibung ist und die Definition tatsächlich Speicher
belegt. So einfach ist das bei einer Klasse aber nicht. Eine Klassendeklaration
besteht aus dem Wort class und dem Namen der Klasse. Mit dieser
Deklaration kann ein Zeiger auf die Klasse angelegt werden, aber nicht eine
Variable, also ein Objekt der Klasse, weil der Compiler nicht weiß, wie groß
ein solches Objekt werden müsste.
Die Definition einer Klasse enthält die Klassendeklaration und sämtliche
Datenelemente und Elementfunktionen. Eine solche Definition muss jedem Modul
vorliegen, das von dieser Klasse mehr will, als eine Zeigervariable anzulegen.
Die Definition einer Klasse kann in einem Projekt beliebig oft wiederholt
werden, darf aber in einem Quelltext nur einmal auftauchen.
Das führt dazu, dass Klassendefinition, die für mehrere Module wichtig sind,
in der Header-Datei stehen, aber nicht in der Implementationsdatei.
Variablen und Funktionen
Einfacher ist die Lage bei globalen Variablen und Funktionen. Der Compiler
braucht lediglich deren Deklaration. Liegt eine Deklaration vor, können die
Funktionen und Variablen im Quelltext beliebig aufgerufen und verwendet werden.
Die Deklarationen können beliebig oft in jeder Quelltextdatei auftreten.
Bei Funktionen besteht die Deklaration aus dem Prototyp, also dem
Funktionskopf, der mit einem Semikolon abgeschlossen wird.
Bei globalen Variablen wird ein Prototyp durch Voranstellen des
Schlüsselwortes extern erstellt. Obwohl das Schlüsselwort es anders
vermuten lässt, kann die Variable in der gleichen Quelltextdatei definiert werden,
in der sie mit extern deklariert wird.
[Prototyp von Funktion und Variable]
int meineFunktion(int, char *); // Funktionsdeklaration
extern char *Pos; // Variablendeklaration
Ausnahme inline und Template
Funktionen dürfen wie Variablen nur einmal in einem Programm definiert
werden. Eine Ausnahme sind inline-Funktionen
und Templates.
Der Grund ist, dass der Compiler in beiden Fällen bereits beim
Aufruf der Funktionen die Funktionen selbst übersetzt. Bei der
inline-Funktion wird der Code an der Aufrufstelle erzeugt. Beim
Template muss in Abhängigkeit der Parametertypen eine passende Funktion
generiert werden. Eine Deklaration reicht dazu in beiden Fällen nicht aus.
Die Definition kann also mehrfach in einem Programm auftreten, in jeder
Übersetzungseinheit allerdings nur einmal.
Header-Dateien
Um die benötigten Deklarationen und Definitionen zwischen den verschiedenen
Quelltexten auszutauschen, gibt es Header-Dateien.
Die klassischen Header-Dateien enden mit der Endung .h. Sie werden
mit dem Befehl #include vom Präprozessor
in die Quelltextdatei eingebettet und so vom Compiler mit dem eigentlichen
Quelltext übersetzt.
#include
Alle Kommandos, die mit einem # beginnen, wenden sich an den Präprozessor
des Compilers.
Die Anweisung #include bewirkt, dass eine Datei an der
Stelle des Befehls in den Quelltext eingebunden wird.
Der Dateiname steht zwischen einem Größer- und einem Kleiner-Zeichen, also
quasi in spitzen Klammern.
In dem Fall handelt es sich um System- oder Compiler-Dateien. Der Pfad dieser
Dateien wird durch Konventionen oder Compiler-Optionen festgelegt.
Alternativ kann der Dateiname in Anführungszeichen stehen.
Dann gehören die Header-Dateien zum Projekt und stehen in den
Verzeichnissen, in denen sich auch die anderen Quelltexte befinden.
Header-Dateien enden traditionsgemäß mit >>.h<<.
Die Header-Dateien der C++-Standardbibliotheken haben allerdings keine
Endung.
Pfadtrenner
Als Pfadtrenner sind in jedem Fall Schrägstriche (/) zulässig. Unter Windows
kann auch ein Backslash (\) verwendet werden. Da ein Backslash in einer
Zeichenkette als Sonderzeichen interpretiert wird, muss er immer verdoppelt
werden. Da die doppelten Backslashes wenig übersichtlich sind, eine
Fehlerquelle darstellen und die Portabilität einschränken, sind Schrägstriche
die bessere Wahl.
Include-Pfade
Sollte sich eine Header-Datei in einem Verzeichnis befinden, das nicht
zu den Standardpfaden gehört, sollten Sie dieses Verzeichnis nicht direkt
in der include-Zeile kodieren, sondern die Pfadliste der
Include-Dateien erweitern. In IDEs gibt es dazu einen Eintrag in den
Projekt-Optionen. Bei Kommandozeilen-Compilern wird dies mit der Option -I
erreicht.
Falls Sie diesen Rat nicht beherzigen und eine Zeile wie die folgende in
Ihren Quellcode schreiben, wird das Programm nur noch unter
Windows und vermutlich nur noch auf Ihrem Arbeitsrechner übersetzbar sein:
#include "F:\mysrc\firma\incl\standards.h" // nicht gut
C-Header in C++
In manchen Fällen müssen ältere C-Bibliotheken zu C++-Programmen eingebunden
werden. Dann kann es sein, dass der C++-Funktionsaufruf nicht zu
der C-Nomenklatur passt, die die Bibliothek erwartet.
Da C++ auch die Parameter einer Funktion kodiert, sind sie nicht zu
Funktionen kompatibel, die ein C-Compiler generiert hat. Damit der
Aufruf von C-Funktionen möglich ist, werden die Funktionsdeklarationen
durch einen extern "C"-Block eingeschlossen.
Oft geschieht das bei den Einbindungen der einsprechenden Header-Dateien
wie im folgenden Beispiel:
extern "C" {
#include <cheader1.h>
#include "cheader2.h"
}
Schnittstelle
Die Header-Datei legt die Teile eines Moduls offen, die von den Programmierern
anderer Module benötigt werden. Sie bildet damit die Schnittstelle zwischen
dem Modul und den Programmteilen, die das Modul benutzen. Änderungen im
Modul betreffen damit auch andere Programmteile so lange nicht, wie die
Header-Datei unverändert bleibt. Wird aber die Header-Datei geändert, müssen
alle Module neu übersetzt werden, die diese Datein einbinden.
Wenn Sie das Tool make verwenden,
müssen Sie bei den Abhängigkeiten auch die Header-Dateien berücksichtigen.
Wenn Sie mit einer IDE arbeiten, wird diese eine solche Abhängigkeit
automatisch feststellen.
Änderungen an den Header-Dateien führen also dazu, dass alle abhängigen
Module neu kompiliert werden müssen. Sie sollten die Schnittstellen besonders
knapp gestalten und sorgfältig entwickeln, damit eine Änderung möglichst
selten erforderlich ist.
Doppelung
Solange nur Deklarationen in den Header-Dateien stehen, können die Dateien
beliebig oft eingebunden werden. Wenn aber auch Klassendefinitionen oder andere
Definitionen, die nur einmal je Übersetzungseinheit auftreten dürfen, in den
Header-Dateien stehen, muss darauf geachtet
werden, dass sie nicht zweimal in einen Quelltext eingebunden werden.
Das hört sich einfacher an, als es ist, da manchmal Header-Dateien selbst
wieder andere Header-Dateien einbinden.
Die Header-Datei schiff.h enthält die Klasse
tSchiff. Der Präprozessor kann verhindern, dass die Datei von einer
Quelltextdatei versehentlich zweimal eingebunden wird.
Mit ihm können Namen definiert werden, und er kann abfragen, ob Namen bereits
definiert wurden.
#ifndef
Die Grundidee ist, dass in der Header-Datei ein Name definiert wird. Ist dieser
Name definiert, ist die Header-Datei vom Compiler bereits einmal übersetzt
worden. Um zu verhindern, dass die Datei ein zweites Mal übersetzt wird, wird
an deren Anfang eine Präprozessor-Abfrage gestellt, ob der Name noch nicht
definiert wurde. Als Name bietet sich der Name der Header-Datei an. Da ein
Punkt in einem Namen nicht zulässig ist, verwendet man an seiner Stelle einen
Unterstrich. Aus traditionellen Gründen werden Präprozessor-Definitionen in
Großbuchstaben geschrieben. Also würde die Datei schiff.h so
aussehen:
[Vor Mehrfachübersetzung geschützte Header-Datei]
#ifndef SCHIFF_H
#define SCHIFF_H
class tSchiff
{
...
};
#endif
Wenn die Datei schiff.h so geschützt wird, kann sie in jeder
Quelltextdatei beliebig oft eingebunden werden. Der Compiler würde sie
trotzdem nur einmal übersetzen. Der Name SCHIFF_H ist beim ersten Mal
nicht definiert, daraufhin wird der Inhalt gelesen. Darin wird
der Name SCHIFF_H sofort definiert. So wird bei einem zweiten Einbinden der
Inhalt der Datei übersprungen.
Statische Funktionen
Statische Funktionen
Grundsätzlich
könnte jede Funktion in einem anderen Modul aufgerufen werden. Soll eine
Funktion ausschließlich für die eigene Quelltextdatei zur Verfügung stehen
und nicht dem Linker bekannt gemacht werden, so wird der Funktion das
Schlüsselwort static vorangestellt.
Öffentlichkeit
Aus historischer Sicht ist leider jede Funktion öffentlich, sofern sie nicht
mit static verborgen wird. An sich wäre es sinnvoller, eine
Funktion standardmäßig zu verbergen und nur auf den Wunsch des Programmierers
hin dem Linker zur Verfügung zu stellen. So würde die Tabelle, in der der
Compiler seine Namen verwaltet, kleiner,
und versehentliche oder absichtliche Indiskretionen würden unmöglich.
Namensraum
Sollen viele Funktionen als lokale Funktionen verwendet werden, empfiehlt sich
in C++ ein anonymer Namensraum,
der etwas einfacher zu handhaben ist, als
jeder Funktion das Schlüsselwort static voranzustellen.
Verborgene Implementation
Es ist etwas unglücklich, dass von einer Klasse auch die Privatsachen
derart in die Öffentlichkeit getragen werden. Wird eine Klasse in einem Header
abgelegt, der von anderen Modulen verwendet
wird, müssen auch private Elemente festgelegt werden, die sich vielleicht
erst später aus der Implementierung ergeben.
Um diese Schwäche der Sprache C++ zu umgehen, stellt dieser Abschnitt eine
denkbare Lösung vor.
Beispiel
Nehmen Sie als Beispiel ein Kartenspiel. Für die anderen Module des Programms
soll eine Schnittstelle geschaffen werden, um ein Kartenspiel zu mischen
und Karten ziehen zu können.
[Klasse Kartenspiel]
class KartenSpiel
{
public:
Karte *NaechsteKarte();
void NeuMischen();
private:
Karte karte[MAXKARTEN];
}
Hier wird im privaten Bereich ein Array von Karten definiert.
Zwar kann auf die Implementierung als Array nicht von außen zugegriffen werden,
und sie ist jederzeit änderbar, ohne dass fremde Module betroffen sind. Allerdings muss
die Schnittstelle der Klasse ausgetauscht werden, obwohl die Änderung andere
Module gar nicht betrifft.
Privatklasse
Das können Sie mit Hilfe einer Implementationsklasse umgehen. Für diese private
Klasse wird lediglich ein Zeiger im privaten Teil der öffentlichen Klasse
angelegt. Für diesen Zeiger wird nur eine Klassendeklaration benötigt,
die über den Inhalt der Klasse nichts aussagt.
[Kartenspiel mit geheimer Implementierung]
class lokalKartenSpiel;
class KartenSpiel {
public:
Karte *NaechsteKarte();
void NeuMischen();
KartenSpiel();
~KartenSpiel();
private:
lokalKartenSpiel *my;
};
Nach außen geheim
Alles, was im privaten Teil der Klasse abgelegt würde, aber nicht publik
werden soll, wird in die spezielle Implementierungsklasse
lokalKartenSpiel umgelagert. Diese Klasse wird zwar im Header
deklariert, aber nicht definiert. Damit ist ihr Inhalt im Header nicht bekannt.
Er muss auch nicht bekannt sein, da im privaten Teil der Klasse KartenSpiel
nur ein Zeiger angelegt wird. Da ein Zeiger so viel Speicherbedarf wie
jeder andere hat, braucht der Compiler keine Information über
die lokale Klasse.
Bei der Implementierung muss noch berücksichtigt werden, dass der Zeiger im
Konstruktor ein Objekt erhalten muss. Das muss natürlich im Destruktor wieder
gelöscht werden. In der Quelltextdatei würde dann folgender Code stehen:
[Die Implementierung]
class lokalKartenSpiel
{
public:
Karte karte[MAXKARTEN];
};
KartenSpiel::KartenSpiel()
{
my = new lokalKartenSpiel;
}
KartenSpiel::~KartenSpiel()
{
delete my;
}
Kosten der Intimsphäre
Ganz umsonst gibt es diese Diskretion nicht. Für die Zugriffe auf die
Privatklasse muss immer über den Zeiger my gegangen werden.
Aus diesem Grund ist es sinnvoll, einen kurzen Namen für den Zeiger zu
verwenden.
Bibliotheken
Bibliotheken enthalten Klassen, Funktionen und Variablen in Maschinencode.
Damit der
Compiler beim Übersetzen weiß, wie diese definiert werden, braucht er
die Deklarationen oder Prototypen der Bestandteile der Bibliothek.
Wenn Sie eine Bibliothek verwenden wollen, müssen Sie
immer die Header-Datei zuerst in die Quelltextdateien einbinden, die die
Bibliotheken benutzen.
Linker und Bibliotheken
Nachdem der Compiler durch die Header-Dateien zufrieden gestellt ist, geht
es nun daran, die eigentlichen Funktionen an das Programm zu binden.
Dazu könnten natürlich die Quelltexte der Bibliotheken mit in das Projekt
übersetzt werden. Es reicht aber aus, wenn die vorkompilierten
Objektdateien eingebunden werden, da sich an den Bibliotheken üblicherweise
nichts mehr
ändert. Da manche Bibliotheken aus sehr vielen Objektdateien bestehen, bindet
man diese kleinen Objektdateien zu großen Bibliotheksdateien zusammen.
So liegen die Standardbibliotheken als übersetzte Dateien vor.
Diese Bibliotheken werden nach dem Übersetzungsprozess vom Linker nach
Funktionen durchsucht, die von den anderen Modulen aufgerufen werden. Der
Linker bindet die benötigten Objekte zu dem Programm hinzu.
Kommandozeile
Den Compilern unter UNIX und auch vielen Kommandozeilen-Compilern unter Windows
muss mit Hilfe der Option -l (kleines L) der Name der
benötigten Bibliothek genannt werden.
Unter UNIX beginnen die Namen von Bibliotheken immer mit der Vorsilbe
lib und enden mit der Erweiterung
.a.
Beides wird bei der Option -l nicht angegeben. Die Bibliothek
libxml.a wird also mit -lxml eingebunden.
Bibliothekspfad
Unter UNIX befinden sich die Bibliotheken des C-Compilers standardmäßig im
Verzeichnis /usr/lib. Auf anderen Betriebssystemen gehören die
Bibliotheken zum Compiler und nicht zum Betriebssystem. Dort finden Sie die
Dateien meist in einem Unterverzeichnis lib in dem Verzeichnis,
in dem der Compiler installiert wurde.
Wenn Sie eine Bibliothek einbinden wollen, die sich nicht in dem
Standardpfad für Bibliotheken befinden, dann muss der Ort
mit der Option -L angegeben werden.
IDE
Bei einer IDE sind diese Informationen in den Dialogen zum Thema
>>Projekteinstellungen<< zu suchen. Dort finden Sie einen Eintrag, in dem
Sie Bibliotheken aufzählen und auch den Pfad ergänzen können.
Linker-Fehler
Wenn nach einer Übersetzung eine Fehlermeldung des Linkers erscheint, die
meist >>unresolved external<< enthält, dann fordert im Allgemeinen das Programm
eine Bibliothek an, die der Linker nicht hinzugebunden hat.
Für solche Fehler kann es verschiedene Ursachen geben.
- Hat es beim Durchlauf des Compilers bereits eine Warnung gegeben, dann
kann der Funktionsname falsch geschrieben sein oder der
#include-Befehl fehlen.
- Die Bibliothek wurde nicht eingebunden. Bei einer IDE muss die Bibliothek,
sofern sie keine Standardbibliothek ist, in den Projekt-Einstellungen
eingetragen werden. Bei Verwendung von
make
oder bei einem direkten Kommandozeilenaufruf
muss der Name der Bibliotheksdatei bei der Option -l (kleines L)
aufgeführt sein.
- Die Bibliothek liegt in einem Pfad, der vom Linker nicht nach Bibliotheken
durchsucht wird. Wenn es nicht angebracht ist, die Bibliothek in
den Standardpfad zu legen, dann muss das Verzeichnis in den
Projekteinstellungen aufgeführt werden oder bei einem
Kommandozeilen-Compiler mit der Option -L genannt werden.
- Einige Linker binden Bibliotheken nur hinzu, wenn eine offene Anforderung
vorliegt. Dadurch wird der Linker zwar schnell und die entstehenden
Programme werden klein, aber der Programmierer muss darauf achten, dass
voneinander abhängige Dateien in der richtigen Reihenfolge im Linkeraufruf
genannt werden.