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.


Informatik-Ecke Einstieg in C++ (C) Copyright 2005 Arnold Willemer