Getrennte Übersetzung in C
Teile einer Übersetzungseinheit, die gemeinsam eine bestimmte Aufgabe erledigen, können von dieser in eine weitere Übersetzungseinheit abgetrennt werden. Das ist so üblich und verbessert die Übersicht, wenn jede Übersetzungseinheit einen klar definierten Zuständigkeitsbereich hat. Es erlaubt außerdem, eine Übersetzungseinheit in verschiedenen Projekten (z.B. Programmen) zu verwenden und so einmal investierte Arbeit mehrfach zu nutzen.
Dieses Vorgehen soll zunächst an einem einfachen Beispiel gezeigt werden.
monolith.c
#include <stdio.h> // printf
void hallo(){ printf( "Hallo, " ); }
int main(){ hallo(); printf( "Peter!\n" ); }stdout
Hallo, Peter!
Es kann der Eindruck entstehen, daß die Funktion zur Ausgabe von "Hallo, " eine eigene Übersetzungseinheit verdient, weil sie in mehreren Projekten verwendet werden könnte. (In der Praxis wäre sie dafür meist zu klein, doch bleibt das Beispiel so zunächst überschaubar. In Anwendungsprojekten sind die Übersetzungseinheiten oft größer.)
Eine mögliche Zerlegung—noch nicht ganz perfekt—besteht aus der Übersetzungseinheit "hallo.c" und der Übersetzungseinheit "main.c".
hallo.c
#include <stdio.h> // printf
void hallo(){ printf( "Hallo, " ); }Konsole
Compile succeeded.
main.c
#include <stdio.h> // printf
int main(){ hallo(); printf( "Peter!\n" ); }Konsole
"main.c", line 2: error: identifier "hallo" is undefined
int main(){ hallo(); printf( "Peter!\n" ); }
^
1 error detected in the compilation of "main.c".
Die Übersetzungseinheit "main.c" kann nicht übersetzt werden, weil der Bezeichner "hallo" nicht bekannt ist. Das Problem kann durch eine Deklaration des Bezeichners behoben werden. Durch die Deklaration wird dem Übersetzer zugesichert, daß irgendwo ein Bezeichner "hallo" für eine Funktion (ohne Ergebnis und Parameter) definiert wird. Auch wenn der Übersetzer die Definition nicht kennt, ist ihm dann der Typ bekannt (nach dem Motto “trust the programmer ”) und das genügt.
main.c
#include <stdio.h> // printf
void hallo();
int main(){ hallo(); printf( "Peter!\n" ); }Konsole
Compile succeeded.
Das Programm läuft nun noch nicht, aber beide Übersetzungseinheiten konnten zunächst in Zwischendateien (die Objektdateien ) übersetzt werden.
Die erzeugten Zwischendateien müssen nun zu einem lauffähigen Programm verbunden werden, wobei der Bezug auf die Funktion "hallo" aufgelöst werden muß (Beim Aufruf der Funktion "hallo" muß die Definition aus einer anderen Objektdatei aufgerufen werden.). Zum Auflösen des Bezugs muß ein Verbinder aufgerufen werden. Wie genau dieser Verbinder bedient wird, hängt vom verwendeten Betriebssystem und Entwicklungssystem ab, es ist nicht durch die Programmiersprache C festgelegt. Da es hierfür viele verschiedene Möglichkeiten gibt, muß hierfür die Anleitung des verwendeten Entwicklungssystems herangezogen werden. Das Vorgehen zur Aktivierung dieser Verbindung kann daher nicht in dieser Lektion beschrieben werden. Nach dem Verbinden ist dann ein lauffähiges Programm entstanden, das die Zeile "Hallo, Peter!" ausgibt.
stdout
Hallo, Peter!
Es könnte nun noch eine andere Übersetzungseinheit "main1.c" mit einer Funktion "main" definiert werden, die mit der Übersetzungseinheit "hallo.c" verbunden werden kann, um ein weiteres ausführbares Programm zu erzeugen.
main1.c
#include <stdio.h> // printf
void hallo(); // Eine weitere Kopie der Deklaration
int main(){ hallo(); printf( "Susanne!\n" ); }Konsole
Compile succeeded.
stdout
Hallo, Susanne!
Das vorgestellte Verfahren hat aber noch Mängel: Wenn die Übersetzungseinheit "hallo.c" tatsächlich in mehreren Projekten verwendet werden sollte, dann muß die Deklaration der Funktion "hallo" in jeder Klienten-Übersetzungseinheit stehen. Es entstünden also viele Kopien. Bei den häufigen größeren Übersetzungseinheiten mit mehreren Funktionen müßten sogar mehrere Funktionsdeklarationen in jeden Klienten (Benutzer) kopiert werden. Außerdem kann der Übersetzer nicht prüfen, ob die Deklaration der Funktion "hallo" mit der Definition verträglich ist.
Um das unnötige Kopieren von Deklarationen zu verhindern, wird diese nun traditionell einmal in eine Datei geschrieben, die dann mit einer Vorverarbeiter-Direktive am „Kopf“ (Anfang) aller Klienten eingefügt wird und daher auch als „Kopf“ (header ) bezeichnet wird. Diese Datei enthält die Deklarationen der Bezeichner einer Übersetzungseinheit, die in anderen Übersetzungseinheiten verwendet werden können, und weiteres Material, das der Übersetzer sehen muß, damit eine Einheit verwendet werden kann. Solch ein Kopf hat oft die Dateinamenserweiterung ".h" und heißt sonst wie die zugehörige Übersetzungseinheit (ohne die Erweiterung ".c"). In dem hier verwendeten Beispiel enthält der Kopf "hallo.h" die Deklaration der Funktion "hallo".
hallo.h
#ifndef hallo_h_INCLUDED_20030319
#define hallo_h_INCLUDED_20030319
void hallo();
#endif
Alle Zeilen in einem Kopf werden Bestandteil jeder Datei, in die sie eingefügt wird, also ihres Klienten. Daher sollte der Autor eines Kopfes nur das unbedingt Nötige in diesen aufnehmen.
Allgemein gesagt, müssen alle Informationen, die der Compiler bei der Übersetzung sehen muß, in den Kopf geschrieben werden, alles andere gehört in die Implementationsdatei. Einem Anfänger fehlt aber oft noch die nötig Vorstellung von den Vorgängen bei der Herstellung, um dieses Kriterium immer richtig anzuwenden, daher können folgende Regelungen beachtet werden: Deklarationen kommen in den Kopf, Definitionen in die Implementationsdatei. Makrodefinitionen kommen dann in den Kopf, wenn sie von Klienten gesehen werden sollen. Laufzeitoptimierte Funktionsdefinitionen mit dem Schlüsselwort "inline" gehören auch in den Kopf.
Nun kann jeder Klient der Dienste (wie beispielsweise der Funktionen) der Übersetzungseinheit "hallo.c" alle benötigten Deklarationen mit einer Einfügedirektive (Kopfdirektive) einfügen (inkludieren).
main2.c
#include <stdio.h> // printf
#include "hallo.h"
int main(){ hallo(); printf( "Peter!\n" ); }Konsole
Compile succeeded.
stdout
Hallo, Peter!
Man kann sich vorstellen, daß die Standardkopfdirektiven, wie "#include <stdio.h>" auch auf das Einfügen solcher Köpfe zurückgehen. Bei den Standardkopfdirektiven werden die Namen jedoch in spitze Klammern "<>" statt in Anführungszeichen geschrieben. Man kann sich vorstellen, daß diese Standarddirektiven Köpfe mit Deklarationen für Standardbezeichner, wie den Bezeichner "stdout", einfügen, so daß diese dann verwendet werden können, auch wenn sie in einer Übersetzungseinheit nicht definiert werden.
Eine Übersetzungseinheit, wie die Übersetzungseinheit "hallo.c", zusammen mit ihrem Kopf, wie der Kopf "hallo.h" kann man insgesamt als ein Modul ansehen, das bestimmte Dienste anbietet. Perfekt wird solch ein Modul dann mit einer Dokumentationsdatei, die auch noch erklärt, wie diese Dienste benutzt werden können. Zur Herstellung solch einer Dokumentationsdatei können Dokumentationskommentare im Kopf verwendet werden. Man kann dann auch schön erkennen, wie der Kopf eine Schnittstelle beschreibt (Sicht von außen) und die cpp -Datei die Implementation (Sicht von innen), weswegen sie auch Implementationsdatei genannt wird.
In die Implementationsdatei des Moduls wird der Kopf auch noch einmal eingefügt, weil es dem Übersetzer dann auffallen würde, wenn die Deklarationen mit dem Typ der definierten Entitäten nicht verträglich wären und eine entsprechende Fehlermeldung erzeugt werden würde. Außerdem benötigt die Implementationsdatei oft selber auch bestimmten Informationen aus dem Kopf.
hallo3.c
#include <stdio.h> // printf
#include "hallo3.h"
void hallo3(){ printf( "Hallo, " ); }hallo3.h
#ifndef hallo3_h_INCLUDED_20030319
#define hallo3_h_INCLUDED_20030319
/** @file hallo3.h
@brief Unterstuetzung bei der Ausgabe von "Hallo, ". */
/// Ausgabe des Textes "Hallo, "
void hallo3();
#endifhallo3.txt [Dokumentation]
hallo3.h Dateireferenz
Unterstuetzung bei der Ausgabe von "Hallo, ".
Funktionen
void hallo3()
Ausgabe des Textes "Hallo, ".hallo3_ec.c
/** @file hallo3_ec.c
@brief Beispielklient des Moduls hallo3. */
#include <stdio.h> // printf
#include "hallo3.h"
int main(){ hallo3(); printf( "Peter!\n" ); }Konsole
Compile succeeded.
stdout
Hallo, Peter!
Man darf aber nicht glauben, die Programmiersprache C habe einen bestimmten Begriff von einem Modul. Ein Modul ist lediglich eine bestimmte etablierte Art der Verwendung von C, es wird aber nicht speziell durch C unterstützt, sondern vom Programmierer aus verschiedenen C -Sprachelementen zusammengebastelt. Es handelt sich als um eine bestimmte tradierte Art der Verwendung von C aber nicht um einen Begriff der Sprache selber. In diesem Sinne gilt „Es gibt in C keine Module.“ Es ist lediglich eine Entscheidung des Programmierers und dieses Textes eine Implementationsdatei (wie die Datei "hallo3.c") und einem Kopf (wie dem Kopf "hallo3.h") mit der dazugehörigen Dokumentationsdatei (wie der Datei "hallo3.txt") zusammen gedanklich als ein Modul "hallo3" aufzufassen.
Die Dokumentationsdatei und die Schnittstellendatei eines Moduls müssen so geschrieben werden, daß diesen beiden Dateien alle Informationen zur Benutzung des Moduls entnommen werden können, ohne daß es nötig ist, auf die Implementationsdatei zurückzugreifen. Im Allgemeinen sollten alle zur Verwendung eines Moduls nötigen Informationen der Dokumentation entnommen werden können.
Wenn ein Bezeichner von einem Modul definiert wird und mit dieser Definition anderen Übersetzungseinheiten zur Verfügung gestellt wird, sagt man auch, der Bezeichner werde exportiert oder er habe externe Verbindung (external linkage ). Soll eine Funktionsdefinition von anderen Übersetzungseinheiten aus nicht erreichbar sein, so kann sie als Schlüsselwortes "static" gekennzeichnet werden. Man sagt dann auch, diese Definition sei von anderen Übersetzungseinheiten aus nicht sichtbar und der definierte Bezeichner habe interne Verbindung (internal linkage ). Bezeichner mit interner Verbindung werden ihrer Sichtbarkeit entsprechend auch nicht im Kopf deklariert. Ein Beispiel dafür findet sich im Programm "hallo4.c".
- Getrennte Kompilierung
- Schreiben Sie ein Modul "englisch", das die Funktion "vielleicht" enthält. Diese Funktion soll das Englische Wort für „vielleicht“ (das Wort "perhaps") ausgeben. Schreiben Sie einen Klienten als weitere Übersetzungseinheit und benutzen sie darin das Modul "englisch", um das englische Wort für „vielleicht“ auszugeben. Im Klienten muß danach auch noch ein Zeilenende ausgegeben werden, bevor das Programm endet.
Makrodefinitionen
Makros muß der Übersetzer (genauer: der Vorverarbeiter) samt ihrer Definition kennen, damit er sie richtig verwenden kann. Deswegen müssen sie im Kopf eines Moduls definiert werden, wenn sie im Klienten verwendet werden sollen. Dadurch können Implementationsdetails sichtbar werden, die nicht sichtbar werden sollten. (Ein Klient könnte sich an diese binden, was ihre Änderbarkeit verringert.) Daher sollten Makros möglichst vermieden werden. Das folgende Beispiel zeigt wie ein Modul ein Makro exportiert.
hallo4.c
// keine Implementationsdatei noetig
hallo4.h
#ifndef hallo4_h_INCLUDED_20030319
#define hallo4_h_INCLUDED_20030319
/// Ausgabe des Textes "Hallo, " (Makroversion)
#define HALLO4 { printf( "Hallo, " ); }
#endifhallo4.txt [Dokumentation]
hallo4.h Dateireferenz
Makrodefinitionen
#define HALLO4 { printf( "Hallo, " ); }
Ausgabe des Textes "Hallo, " (Makroversion).main4.c
#include <stdio.h> // printf
#include "hallo4.h"
int main()
{ HALLO4 printf( "Michael!\n" ); }Konsole
Compile succeeded.
stdout
Hallo, Michael!
Definitionen laufzeitoptimierter Funktionen
Laufzeitoptimierte Funktionen (inline -Funktionen) muß der Übersetzer samt ihrer Definition kennen, damit er sie richtig verwenden kann. Deswegen müssen sie im Kopf zu einer Übersetzungseinheit nicht nur deklariert, sondern sogar vollständig definiert werden. Das weicht leider von der schönen Grundregel ab, die empfiehlt, daß der Kopf nur die Schnittstelle eines Moduls enthält und die Implementationsdatei die Definitionen und ist daher auch ein Nachteil laufzeitoptimierter Funktionen. Dadurch, daß der Klient aus dem Kopf Interna der Definition erfahren könnte, kann er sich an diese Binden und es dadurch erschweren, diese bei Bedarf zu ändern.
Das folgende Beispiel zeigt, wie ein Modul eine laufzeitoptimierte Funktion exportiert.
hallo5.c
// keine Implementationsdatei noetig
hallo5.h
#ifndef hallo5_h_INCLUDED_20030319
#define hallo5_h_INCLUDED_20030319
/** @file hallo5.h
@brief Unterstuetzung bei der Ausgabe von "Hallo, ". */
/// Ausgabe des Textes "Hallo, " (laufzeitoptimierte Funktion)
inline void hallo5(){ printf( "Hallo, " ); }
#endifhallo5.txt [Dokumentation]
hallo5.h Dateireferenz
Funktionen
void hallo5() [inline]
Ausgabe des Textes "Hallo, " (laufzeitoptimierte Funktion).main5.c
#include <stdio.h> // printf
#include "hallo5.h"
int main(){ hallo5(); printf( "Susanne!\n" ); }Konsole
Compile succeeded.
stdout
Hallo, Susanne!
Definitionen mit interner Bindung
Eine Funktionsdefinition kann mit dem Deklarationsspezifizierer "static" gekennzeichnet werden. Der definierte Bezeichner hat dann interne Bindung. Das bedeutet, daß er nur innerhalb der die Definition enthaltenden Übersetzungseinheit sichtbar ist. Aus einer anderen Übersetzungseinheit ist dieser Bezeichner also nicht mit der definierten Bedeutung sichtbar. (Er könnte in anderen Übersetzungseinheiten allerdings ebenfalls definiert werden.)
Der Gültigkeitsbereich jedes Bezeichners soll so klein wie nur möglich sein. Daher sollte jede Funktion mit dem Deklarationsspezifizierer "static" gekennzeichnet werden, es sei denn sie soll in anderen Übersetzungseinheiten angewendet werden können.
hallo7.h
#ifndef hallo7_h_INCLUDED_20030319
#define hallo7_h_INCLUDED_20030319
/** @file hallo7.h
@brief Eine Funktion zur Ausgabe von "Hallo, " */
/// Ausgabe des Textes "Hallo, ".
void hallo4(); // Deklaration einer Funktion mit externe Verbindung
#endifhallo7.c
#include <stdio.h> // printf
#include "hallo7.h"
static void hallo4_impl() // interne Verbindung
{ printf( "Hallo, " ); }
void hallo4() // externe Verbindung
{ hallo4_impl(); }hallo7.txt [Dokumentation]
hallo7.h Dateireferenz
Funktionen
void hallo7()
Ausgabe des Textes "Hallo, ".main7.c
#include <stdio.h> // printf
#include "hallo7.h"
int main()
{ // hallo4_impl(); // nicht moeglich
hallo4(); printf( "Peter!\n" ); }stdout
Hallo, Peter!