ZurückZum Inhalt


Modularisierung

Vielleicht haben Sie sich einmal gefragt, warum ausgerechnet C zu einer Weltsprache der Computerbauer geworden ist. Neben C gibt es eine Vielzahl von Konkurrenten. Die traditionellen Sprachen sind Cobol und Fortran. Beide haben nach wie vor ihre großen Anwendungsgebiete. Warum sie nicht? Oder warum nicht Ada, die Sprache der Militärs?

Warum nicht Pascal ?

Und noch einen vielgerühmten Mitbewerber gibt es, Pascal. Aber gerade Pascal ist ein gutes Beispiel für die andersgeartete Leistungsfähigkeit von C. Pascal wurde für universitäre Zwecke entwickelt. Sie sollte Studenten einen guten Programmierstil anerziehen. (Und dazu ist Pascal sicher hervorragend geeignet.) Nur hat man dabei leider vergessen, daß ein lebendiges Produkt etwas anderes ist, wie eine Prüfungsaufgabe an einer Universität.

Um ein Softwareprodukt herzustellen, kommt es auf die Fähigkeit einer Sprache an, fertige Softwarebausteine herzustellen und zu einem Ganzen zu verknüpfen. Wie groß Softwareprodukte heute werden können, das merkt man an der eigenen Festplatte im Rechner. 30 Megabyte für OS/2, 40 Megabyte für TurboC++ 3.0 und noch einmal 10 Megabyte für ein Zeichenprogramm lassen viele Festplatten klein erscheinen. Aber sicherlich wird die Festplattenindustrie hier aufholen.

Große Projekte sind die Ursache

So große Projekte lassen sich wirtschaftlich nur dann erstellen, wenn viele Programmierer reibungslos zusammenarbeiten können und die Entwicklungsumgebung die Zusammenarbeit unterstützt. Ein (Original-)Pascal-Programm bestand immer aus einer einzigen Datei. Sollten mehrere Programmierer zusammen ein Produkt entwickeln, dann schrieb jeder seinen Quelltext, mischte ihn mit einem Editor zu den Quellen der anderen und dann wurde das Ganze wieder übersetzt. Ein untragbarer Zustand für Projekte. (Ein weiteres Problem ist die unvollständige Fallunterscheidung in Pascal.)

In C, und das ist das Thema dieses Kapitels, ist alles ganz anders. Gerade die Fähigkeit, große Projekte korrekt verwalten zu können, ist eine der ganz großen Stärken von C. Sehen wir uns die Modularisierung an einem Beispiel an und gehen dann über zur Diskussion der gesamten Programmerstellung.

Auf dem Weg zu Projekten - getrennte Übersetzung

Das Thema der Projekte haben wir bereits im allerersten Kapitel über die Strukturen angesprochen. Bauen wir auf den vorhandenen Erfahrungen und der Trennung zwischen Spezialist und Anwender auf und sehen uns den Erstellungsvorgang noch einmal im Detail an.

Bild 13-1: Kleinste Übersetzungseinheit für Code

Die entscheidende Fähigkeit von C ist die getrennte Übersetzung von Programmteilen, den Modulen. Das kleinste Stück Programmcode, das getrennt übersetzbar ist, ist ein Unterprogramm. Mit einer Übersetzung erhält man eine Objektdatei. Diese Objektdatei enthält unter anderem den übersetzten Quellcode. Er kann ohne neu übersetzt zu werden in anderen Quellen benutzt werden.

Diese Feststellung, daß keine neue Übersetzung notwendig ist, scheint einfach. Bedenkt man aber, daß Rechenzeit immer knapp ist, erkennt man den großen Zeitgewinn durch die Übersetzung von Teilen. Auch heute dauern große Übersetzung vieleicht noch Stunden und auf alle Fälle kostet jede neue Übersetzung Geld für den Arbeitsplatz und den Rechner.

Aus der Sicht der Daten ist ebenfalls eine getrennte Übersetzung möglich. Daten, die allein übersetzt werden, sind immer globale Daten, da sie in keinem Block stehen. Diese Möglichkeit erlaubt es, daß man in einem großen Projekt alle globalen Daten zentral in einer Datei hält und alle anderen Programmteile sich nur darauf beziehen. Dies erleichtert die Verwaltung erheblich.

Bild 13-2: Kleinste Übersetzungseinheit für Daten

Im Zusammenspiel der einzelnen Fähigkeiten schauen wir uns nun die Anwenderseite einmal an. Der Anwender will Funktionen oder Daten eines anderen benutzen. Dazu spricht er (normalerweise) die Programmelemente mit ihrem Namen an. Da der Compiler bei der Verwendung einer Variablen wissen muß, wie groß sie ist und welche Operationen man mir ihr ausführen darf, muß man dem Compiler die Bedeutung des Namens vor seiner Benutzung bekanntgeben.

Jede Übersetzung ist für sich abgeschlossen. Ein in der Übersetzungseinheit (sprich Quelldatei) benutzter Name, muß in der gleichen Übersetzungseinheit vor seiner Verwendung bekannt sein, sonst kann der Compiler keinen korrekten Maschinencode erzeugen.

Deklarationen

Die Lösung kennt man als Deklaration. Deklarieren (bekanntgeben) ist ein Begriff, den Urlauber an vielen Grenzen auch von Zöllner hören. Haben Sie etwas zu deklarieren? Gemeint ist, ob der Grenzgänger etwas angeben will über die Beschaffenheit von mitgeführten Waren. Diese Deklaration geschieht in C auf zwei unterschiedliche Arten: explizit oder durch eine eingebaute Automatik.

Bild 13-3: Deklarationen: implizit und explizit

Im Bild 3 wurde zum einen die Funktion mit dem Namen funktion() vollständig deklariert. ANSI-C nennt eine vollständige Deklaration einen Prototypen. Die vollständige Deklaration umfaßt die Angabe des Namens der Funktion, ihres Rückgabetyps und der Typen der übergebenen Parameter. Mit den Typen haben wir gleichzeitig die Anzahl und die Reihenfolge der Parameter bekanntgegeben.

C kennt daneben noch eine automatische Deklaration. Wird eine Funktion benutzt, die im Moment des Aufrufes nicht bekannt ist, dann erzeugt der Compiler eine eigene Deklaration. Die Deklaration besteht aus dem verwendeten Funktionsnamen, dem Rückgabetyp int und einer ungeprüften Parameterschnittstelle. In K&R-C war dies eine gängige und übliche Methode eigene Schreibarbeit zu sparen. In ANSI-C wird sie (noch) geduldet aber mit einer deutlichen Warnung versehen.

Im Beispiel paßte die automatisch erzeugte Deklaration gut zu der aufgerufenen Funktion printf(), die glücklicherweise genau diesen Aufbau hat. Sollte die implizite Deklaration nicht dem Aufbau der Funktion entsprechen, dann handeln wir uns erhebliche Probleme ein. Im einfachsten Fall findet der Compiler oder Linker den Fehler, im schlimmsten Fall der Kunde und Benutzer des Programmes.

Das Problem kann man auch dann sehen, wenn man eine Funktion (fehlerhaft) aufruft, die im gleichen Modul (Übersetzungseinheit) definiert wird. Die Definition soll hier ausnahmsweise erst hinter der Verwendiung im Aufruf stehen.

Bild 13-4: Probleme bei impliziter Deklaration

Der Compiler wird das Programm im Bild vier übersetzen und den Fehler melden, daß Deklaration und Definition der Funktion "funktion()" nicht zusammenpassen. Es ist daher in den allermeisten Fällen unbedingt wünschenswert, daß jede verwendete Funktion vor ihrer Verwendung bekanntgegeben wird. Steht die aufgerufene Funktion im gleichen Modul, muß sie vor dem Aufruf stehen. Wurde sie außerhalb des Moduls geschrieben, muß eine Deklaration vor dem Aufruf stehen.

In sehr seltenen Fällen kann es notwendig werden, eine sogenannte Vorwärtsdeklaration zu benutzen.

Eine Vorwärtsdeklaration gibt sozusagen im Vorgriff eine Funktion bekannt, die im gleichen Modul an einer späteren Stelle noch definiert werden wird. Allerdings steht dann die Verwenduung zwischen Deklaration und Definition, wie im Bild 5 gezeigt.

Diese Vorwärtsdeklaration ist bei Funktionen nur in ganz seltenen Fällen wirklich notwendig. Ein Grund könnten zwei Funktionen sein, die sich gegenseitig aufrufen. Aber dies würde wiederum den Spielregeln aller bekannten Designprinzipien, wie Strukturierter Programmierung oder OOP (Objekt orientierter Programmierung) widersprechen. Daher ist die Befgründung theoretisch.

Bild 13-5: Vorwärtsdeklaration

Der übliche Weg ist es, alle benutzten Funktionen am Programmbeginn zu deklarieren. Für den Fall, das ein Programm viele andere Funktionen aufruft, kommt auf den Programmierer eine ganze Menge Arbeit zu. Allerdings kostet eine Deklaration keinerlei Speicherplatz im Programm. Eine Deklaration ist nur eine Information für den Compiler, mit der er die korrekte Schreibweise der Aufrufe überprüft.

Zu viele Deklarationen schaden daher nie. Sie kosten höchstens etwas Zeit bei der Übersetzung. Um die Schreibarbeit für den Programmierer nicht zu groß werden zu lassen, haben die Entwickler von C den Präprozessor und die "#include"-Anweisung mit eingebaut. Mit "#include" wird eine andere datei an der Stelle der "#include"-Anweisung in den gerade übersetzten Quelltext eingelesen, als wäre es ein Bestandteil des übersetzten Moduls.

Alle üblichen Deklarationen schreibt man daher in Informationsdateien (engl. header files / Vorspanndateien) und liest sie bei Bedarf während der Übersetzung ein.

Informationsdateien für explizite Deklarationen

Ein Programmierer, der in der Rolle des Spezialisten, einen zusammengehörigen Satz von Funktionen für Anwender schreibt, wird für diesen Satz eine eigene Informationsdatei erstellen, die alle notwendigen Deklarationen umfaßt. Allen Anwendern, und damit auch ihm, wird damit eine ganze Menge Schreibarbeit abgenommen. Statt langer Deklarationen genügt nun eine einzige #include--Anweisung.

Wie die Deklarationen im einzelnen aussehen wird gleich besprochen. Wenden wir uns vorher noch einem unscheinbaren Detail zu, das aber wegen seiner grundsätzlichen Bedeutung sehr zu korrekten Programmen beitragen kann.

Die Namensräume für Deklarationen und Definitionen sind in C getrennt. Das bedeutet, daß man in einer einzelnen Datei ein und dieselbe Funktion sowohl deklarieren als auch definieren kann.

Bild 13-6: Informationsdatei des Spezialisten für ratio

Legen wir zuerst in der Rolle des Spezialisten eine Informationsdatei an. Darin definieren wir eine Struktur und einen Datentyp. Wie üblich folgen die Deklarationen der Operatorfunktionen.

Bild 13-7: Implementierung des Spezialisten für ratio

Beim Übersetzen wird nun diese Datei sowohl im Hauptprogramm als auch in der Implementierungsdatei verwendet.

In den beiden Fällen wird jedoch die Informationsdatei völlig unterschiedlich verwendet. Im Fall der Implementierung tritt der erwähnte Fall ein, daß in einer einzigen Übersetzung sowohl Deklarationen aus der Informationsdatei wie auch die Definitionen der Funktionen vorkommen.

Der große Vorteil liegt dabei in der Überprüfung durch den Compiler. Wenn der Spezialist seine eigene Informationsdatei beim Übersetzen der Implementierung mit einliest, dann erhält der spätere Anwender eine geprüfte Schnittstelle, auf die er sich verlassen kann.

Bild 13-8: Testrahmen des Anwenders für ratio

Diese Eigenschaft von C, die es ähnlich auch schon bei K&R gab, erlaubt die Erstellung sicherer Bibliotheken. Nicht alle Hochsprachen haben diese Eigenschaft und können Deklaration und Definition miteinander vergleichen.

C zeigt hier wieder einmal seine Stärke als unversielle Sprache, in der sich Programme professionell formulieren lassen. Dies gilt insbesondere in größeren Projekten.

Programmerstellung

In C werden eine ganze Anzahl von Programmen benutzt, um ein ablauffähiges Programm zu erzeugen. Die erste Stufe bildet dabei ein Editor, mit dem man den Quelltext erstellt und korrigiert. Je nach System wird dies ein Editor in einer integrierten Entwicklungsumgebung oder ein beliebiger anderer Editor sein. Wichtig ist dabei nur, daß eine Datei erstellt wird, die nur den reinen Quelltext beinhaltet. Manche Textprogramme legen zusammen mit dem Text noch Formatierungsinformationen ab. Der Compiler kann solche Zusatzinformationen nicht verstehen und benötigt sie auch nicht.

C ist eine formatfreie Sprache. Dies bedeutet, daß der Programmierer seinen Text so aufbauen kann, daß er für ihn leicht lesbar ist. In den Beispielen dieses Buches wurden zumeist die geschweiften Klanmmer so untereinandergesetzt, daß zusammengehörige Klammern in einer senkrechten Linie liegen. Man könnte ein Lineal an einen Ausdruck anlegen und sollte die passenden Klammern leicht finden. Hinter einer geschweiften Klammer steht auch normalerweise kein weiterer Text, sodaß der Beginn und das Ende eines Blockes optisch herausgehoben werden.

Solche Konventionen sind dem Compiler egal. Er wird eine beliebige Menge von white space, also von Leerzeichen, Tabulatoren und Zeilenschaltungen intern zu einem einzigen Trennzeichen zusammenfassen. Für den Programmierer sind solche Konventionen aber ein wichtiges Hilfsmittel, um den geschriebenen Text optisch zu gliedern und leichter zu verstehen.

Unter UNIX findet sich ein Programm mit dem schönen Namen cb für C- beautifier (C Verschönerer). Dieses Programm liest einen beliebigen C-Quelltext und liefert eine standardisierte Darstellung mit Einrücken der Zeilen. Da die Form aber von der in diesem Buch gewählten abweicht, soll er nur erwähnt, aber für eigene Quellen nicht empfohlen werden. Trotzdem kann er ein sinnvolles Werkzeug sein, wenn die Quelle zu unordentlich geschrieben wurde.

Nach dem Editieren folgt die Übersetzung. Grob betrachtet besteht die Übersetzung aus zwei getrennten Phasen. Die erste ist der Präprozessor und die zweite der Compiler.

Die Präprozessorphase

Die Ausgabe des Präprozessors wird normalerweise direkt in den weiteren Phasen des Compilers weiterverarbeitet. Die Ausgabedatei ist dann eine sogenannte Objektdatei. Sie enthält den Maschinencode, Bindeinformationen und Debuginformationen.

Eine der Aufgaben des Präprozessors kiegt in der bedingten Übersetzung. Damit kann man eine Informationsdatei so vorbereiten, daß sie unter verschiedenen Umgebungen benutzbar ist. Gerade für die Portierung von Quellen ist dies ein unschätzbarer Vorteil.

Bild 13-9: Bedingte Übersetzung

Die Arbeit des Präprozessors kann man auch in einer Datei ablegen lassen und sich das Ergebnis mit einem Editor ansehen. Dies kann besonders dann nützlich sein, wenn man mit dem Präprozessor eigene Makros geschrieben hat und nachprüfen will, ob sie auch wie gedacht expandiert werden.

Der Maschinencode besteht aus den Bitmustern, die die verwendete Maschine erkennen kann. Der Maschinencode ist aber noch nicht ablauffähig. Es fehlen ihm noch die richtigen Adressen. Ein Programm, daß die Funktion printf() aufruft, hat den Code von printf() nicht zur Verfügung. Er wurde ja von einem ganz anderen Programmierer geschrieben. An den Stellen im eigenen Programm, an denen printf() aufgerufen wird, hat der Compiler einfach die Adresse "0" eingetragen. Dies ist praktisch ein Loch im erzeugten Code. Es wird die Aufgabe des Linkers sein, mit Hilfe der Bindeinformationen im Objektmodul die richtigen Adressen nachzutragen.

Die Bindeinformationen in der Objektdatei sind letzlich Tabellen, in denen festgehalten wird, welche externen Namen für Funktionen und Variablen verwendet wurden. Zu jedem Namen ist dann festgehalten, wo er benutzt wurde, wo also die Löcher im Maschinencode sind.

Eine andere Art von Bindeinformationen gibt bekannt, welche eigenen Namen öffentlich zugänglich sein sollen und welche Adresse sie im Modul haben. Unter C werden alle globalen Namen von Funktionen und Variablen bekanntgegeben. Nicht bekanntgegeben werden die lokalen Namen, da sie sowie nur kurzfristig leben und alle Namen, die mit static definiert wurden. Diese Namen sind Modul lokal und können somit nicht vom Linker gefunden werden.

Ein solches Objektmodul bezeichnet man auch als verschiebbar (relocatable), da der Linker diesen Code an eine beliebige Stelle verschieben kann. Ein Code, der verschiebbar ist, verläßt sich nicht auf absolute Adressen. Assemblerprogrammierern wird vielleicht aufgefallen sein, daß alle bedingten Sprünge der 80X86-Prozessoren relative Sprünge sind. Dies wurde eingeführt, um den erzeugten Code leichter vom Linker bearbeiten lassen zu können.

Verbreitete Namen für Linker sind: LINK, TLINK, LINK86 oder, unter UNIX, ld (für linkage editor / Bindeeditor).

Beim Linken wird zuerst ein sogenanntes start-up-Modul gelesen. Dieses Modul stellt die benötigte Umgebung her, die ein C-Programm unabhängig von der verwendeten Maschine macht. Eines seiner wichtigsten Aufgaben ist der Funktionsaufruf von main() und die Weiterleitung der Rückgabe an die Statusvariable des Betriebsystems.

Unter DOS finden Sie mehrere Startmodule für verschiedene Übersetzungsmodelle des Compilers. Meist beginnen sie mit c0.. oder crt.. für C run time. Sehen Sie einmal in den Verzeichnissen /lib oder /tc/lib nach oder in dem Verzeichnis, das die Dokumentation Ihres Compilers angibt.

Beim Linken (oder deutsch: Binden) gibt man einfach eine Liste von Objektdateien an, die nacheinander gelesen werden und vom Linker zu einem fertigen Programm gebunden werden. Dabei sollte man auf einige Besonderheiten achten. Die Dateien sind in der Reihenfolge ihrer Wichtigkeit anzugeben. Damit steht das Starmodul immer ganz vorne, das Modul mit main() unmittelbar dahinter und am Schluß folgt die Standard-C-Bibliothek.

Neben den Objektdateien können auch Bibliotheksdateien angegeben werden, die einen ganzen Satz von Objektmodulen enthlaten. Bibliotheken sind sozusagen größere Behälter für viele kleine Objektdateien. Erstellen wir und daher zuerst einmal eine Bibliothek und benutzen diese dann zum Linken.

Aufbau von Bibliotheken

Bibliotheken sind Behälter für Objektdateien. Zumeist beinhalten Sie noch ein eigenes Verzeichnis der verwendbaren öffentlichen Namen (publics). Man kann mit Hilfe eines Bibliotheksverwalterprogramms Bibliotheken anlegen und pflegen. Namen für Bibliotheksverwalter sind z. B. Lib86, TLIB oder, unter UNIX, ar. Der Name des UNIX-Programmes leitet sich von einem anderen Namen für Bibliotheken ab. UNIX spricht von Archiven (archives). Und so heißt eben der Bibliotheksverwalter ar.

Um die Arbeitsweise des Bibliotheksverwalters zeigen zu können, zerlegen wir das Beispielprogramm ratio1.c in mehrere Module. Die Module sollen jeweils eine einzige Funktion beinhalten, mit Ausnahme eines Moduls ratio1ip.c, das die beiden Funktionen r_init() und r_print() hat.

Bild 13-10: Additionsmodul

Als Modul bezeichnet man das Ergebnis einer Übersetzung. Dabei wird natürlich vorausgesetzt, daß die Übersetzung korrekt verlaufen ist. Zumeist verwendet man den namen der Quelldatei als Modulnamen. Gerade bei Assemblern kann man aber eigene Modulnamen vergeben, die nicht mit dem Dateinamen übereinstimmen.

In jedem Modul wird die eigene Informationsdatei ratio.h eingelesen. Im Modul mit der r_print()-Funktion wird zusätzlich die stdio.h eingelesen. Als Module mit einer einzigen Funktion erhält man damit: ratio1a.c, ratio1s.c und ratio1z.c.

Das Modul mit zwei Funktionen hat den Namen ratio1ip.c. Jedes dieser Module wird nun einzeln übersetzt. Wir erhalten daher vier Objektdateien, die in einer Bibliothek zusammengefaßt werden.

Bild 13-11: Subtraktionsmodul

Der große Vorteil in einem wirklichen Programm ist es, daß jedes dieser Module eine überschaubare Größe besitzt und mit einer sauberen Schnittstelle versehen werden muß.

Bild 13-12: Zuweisungsmodul

Die Schnittstelle der Funktionen sind gerade beim Testen ein sehr wichtiger Punkt im Programm. Jede einzelne Funktion könnte mit Hilfe eines kleinen Testrahmens, einem speziell dafür geschriebenen Hauptprogramm, auf seine Funktionstüchtigkeit getestet werden.

Die Unterteilung in einzelne Module unterscheidet sich in unserem Fall dadurch, daß eine unterschiedliche Anzahl von Funktionen enthalten sind. Für den Linkvorgang und den Bibliotheksverwalter sind Module die kleinsten verwaltbaren Teile. Sieht daher der Linker die Notwendigkeit, ein bestimmtes Modul einzubinden, weil eine Funktion daraus benötigt wird, dann bindet er das Modul ein, in dem die Funktion enthalten ist. Ein Programm, das unser ratio-Beispiiel benutzt und dabei die Funktion r_init() benötigt, aber r_print() nicht aufruft, würde trotzdem nach dem Linken den Code von beiden enthalten.

Bild 13-13: Modul mit zwei Funktionen

Unter DOS mit der TurboC++-Umgebung würde eine neue Bibliothek mit dem folgenden Kommando angelegt:

TLIB ratio +ratio1a +ratio1s +ratio1z +ratio1ip

Dabei ruft man den Bibliotheksverwalter tlib.exe und teilt ihm mit, daß er eine neue Bibliothek mit dem Namen ratio.lib anlegen soll. Die Datei-Kennung wird dabei automatisch hinzugefügt.

Die vier Dateien sollen Objektdateien sein. Der Bibliotheksverwalter erwartet die Kennung .obj. Das "+"-Zeichen ist das Kommando "füge hinzu". Nach Beendigung sollte eine Datei mit dem Namen ratio.lib existieren, die alle vier Module enthält.

Sehen wir uns dazu das Inhaltsverzeichnis der Bibliothek an. Mit TurboC++ geht dies mit folgendem Aufruf:

TLIB ratio,rlist

Die Ausgabe läuft dabei in die Datei rlist.lst.

Bild 13-14:Inhaltsverzeichnis der Bibliothek

Das Inhaltsverzeichnis zeigt die öffentlich zugänglichen Namen (publics) pro Modul an. Die Modulnamen sind die Quelldateinamen und die öffentlichen Namen sind in unserem Fall nur Funktionsnamen. Im Falle globaler Variablen wären auch diese hier zu finden.

Sollten Sie unter UNIX arbeiten, sehen Sie sich einmal den Handbucheintrag zu ar an (mit: man ar). Das Inhaltsverzeichnis heißt dort: table of contents.

Durchsuchen von Bibliotheken

Beim Linken kann man neben den einzelnen Objektdateien auch Biobliotheken angeben. Es gibt einen wichtigen Unterschied zwischen beiden Angaben: Objektdateien werden immer eingebunden, Bibliotheken werden durchsucht. Alle angegebenen Objektdateien werden Teil des gebundenen Programms. Sie vergrößern immer den Codebedarf des Programmes, auch wenn die enthaltenen Funktionen im Programm gar nicht aufgerufen werden.

Bibliotheken können beliebig groß werden. Aus Bibliotheken wird der Linker nur die Module verwenden und zum Programm dazubinden, die tatsächlich im Programm aufgerufen werden. Man braucht daher nicht zu befürchten, daß die Angabe einer Bibliothek in Megabytegröße auch zu einem ebenso großen Programm führt.

Dynamische Bibliotheken

Viele Funktionen werden von unterschiedlichen Programmen benutzt. Um Plattenplatz zu sparen gibt es eine weitere Bibliotheksform, die sogenannten DLLs (dynamic link libraries / dynamische Binde-Bibliotheken).

Diese Bibliotheken werden beim Binden durchsucht, aber der enthaltene Code (oder die enthaltenen globalen Daten) werden nicht zum Programm dazugemischt. Die Bindung zwischen Programmaufruf und öffentlicher Funktion wird erst beim Laden vorgenommen. Damit braucht man die Funktionen nur einmal auf der Festplatte zu halten. Nur der Lader muß dafür sorgen, daß bei Bedarf nicht nur das Programm, sondern auch die notwendige Bibliothek in den Speicher geladen wird.

Die Löcher im Programm, wie hier die offenen Verweise auf externe Namen ziemlich salopp genannt wurden, werden mit DLLs beim Laden befriedigt. Der Linkr stellt nur sicher, daß es die gewünschten Namen in der Bibliothek auch gibt.

Unter Betriebssystemen, die Multitasking erlauben, kann man nicht nur auf der Festplatte Platz sparen, sondern auch im Speicher. Laufen mehrere Programme gleichzeitig, die alle die printf()-Funktion benötigen, dann braucht trotzdem die Funktion nur einmal vorhanden sein. Ein Grund mehr, solche Betriebssysteme zu bevorzugen.

Solche DLLs können sehr groß werden. Gerade Fensteroberflächen wie Windows oder X-Windows (UNIX) haben sehr umfangreiche Bibliotheken, die meist DLLs sind. Überigens: unter UNIX spricht man satt von DLL's von shared objects (gemeinsam benutzeten Objektdateien).

Aufbau von Projekten

Selbst das kleine Beispiel dieses Kapitels ist inzwischen auf 8 Dateien angewachsen. Davon sind vier Dateien, deren Objektdateien in einer Bibliothek zusammengafaßt werden und die Quelldatei des Hauptprogramms. Nach allen Übersetzungsschritten ergibt sich schließlich das ablaufffähige Programm. Die achte Datei, die noch aussteht, ist die Informationsdatei ratio.h.

Alle diese Dateien stehen jetzt in einem bestimmten Abhängigkeitsverhältnis zueinander. Je nachdem, welche Datei verändert wird, müssen unterschiedliche Aktionen folgen. Verändert der Programmierer im Hauptprogramm etwas, muß dieses Hauptprogramm neu übersetzt und mit der eigenen Bibliothek gebunden werden, um wieder das fertige Programm zu erhalten.

Anders sieht es aus, wenn eine Datei verändert wird, deren Objektmodul in der Bibliothek vorhanden ist. Hier muß nicht nur die Quelldatei neu übersetzt werden, sondern auch die Bibliothek wieder auf den neuesten Stand gebracht und mit dem Hauptprogramm zusammen gebunden werden.

Noch schlimmer wird es, wenn die Informationsdatei verändert wurde. Jetzt müssen alle Quelldateien, die die Informationsdatei mit #include einlesen, neu übersetzt, die Bibliothek aufgefrischt und das Hauptprogramm mit der Bibliothek wieder gebunden werden.

Diese Aufgaben zu überwachen ist mühsam und fehleranfällig. Es gibt daher immer Projektverwaltungen, die zumindest einen Teil der Arbeit abnehmen.

Eine ganz einfache Methode wäre es, einfach bei jeder Änderung alles neu zu übersetzen, die Bibliothek aufzubauen und das Hauptprogramm zu binden. Der Zeit- und Computerbedarf dazu übersteigt bei größeren Projekten sicher das Machbare um ein Vielfaches. Außerdem ist eine solche Methode nicht gerade fein.

Übersetzt und gebunden sollen immer nur die minimal notwendigen Dateien werden.

Projektverwaltung mit TurboC++

Die Projektverwaltung unter der integrierten Entwicklungsumgebung von TurboC++ ist für viele Anwendungsfälle eine einfache und elegante Möglichkeit, eigene Projekte zu erstellen. Solange nur wenige Dateien und Proogramme beteilgt sind, ist auch die Anwendung einfach.

Die Bilder sind einfache Bildschirmkopien und nur schwer in einem kleinen Bildformat wiederzugeben.

[Projektarbeit]

Bild 13-15: Auswahl der Projektdatei

[Projektauswahl]

Bild 13-16: Projektfenster

Mit Hilfe der Option Project wird eine Projektdatei angelegt (Open / Öffnen). Die Handhabung sieht man an den folgenden Bildschirmausdrucken.

Zuerst vergibt man einen neuen Namen für die Projektdatei oder wählt eine Datei mit der Kennung ".prj" aus der Liste der Dateien aus. Danach sieht man ein Projektfenster. Sollte es nicht automatisch angezeigt werden, genügt es, das Projektfenster über den Menüpunkt (Windows) auszuwählen.

Das Projektfenster zeigt eine untere Menüleiste. Mit der Maus oder mit der Taste "ins/Einfg" öffnet man erneut das Dateiauswahlfenster. Jetzt werden alle zugehörigen Quelldateien und Bibliotheken nacheinander ausgewählt.

In unserem Fall nehmen wir nur das Hauptprogramm ratiom.c und die fertige Bibliothek ratio.lib auf.

Die Kommandos, die ein ablauffähiges Programmvoraussetzen, wie Compile/Make oder Run/Run beziehen sich jetzt auf das Projekt. Bei Bedarf würden nun alle Quelldateien neu übersetzt werden, die sich verändert haben.

[Projektarbeit 4]

Bild 13-17: Auswahl der Dateien eines Projektes

Es werden keine Informationsdateien oder Objektdateien mit aufgenommen. Eine Ausnahme davon sind nur Objektdateien, die nicht als Quelldatei vorliegen.

[Projektarbeit 5]

Bild 13-18: Fertiges Projektfenster

Mit Hilfe von sogenannten Transfermakros könnte man auch die Erstellung der Bibliothek automatisieren. Dies ist aber speziell auf diesen Compiler zugeschnitten unsd soll daher hier nicht darsgestellt werden.

Neben Projektverwaltungen, die speziell auf einen Compiler passen, gibt es noch eine universelle Verwaltung, die mit Hilfe des Programmes make arbeitet.

Projekte verwalten mit make

make ist ein Programm, das Abhängigkeiten verwalten kann. Damit lassen sich Bücher erstellen, Softwareprojekte verwalten oder alles andere, was einem hierarchischen Aufbau von Abhängigkeiten folgt.

Zuerst müssen für make diese Abhängigkeiten beschrieben werden. Dazu genügt ein beliebiger Texteditor. Damit erstellt man eine Datei, die beschreibt, welche Datei von welchen anderen Dateien abhängt und was in dem Fall zu tun ist, wenn die Datei neu aufgebaut werden muß. Eine ".EXE"-Datei hängt von den enthaltenen Objektdateien und Bibliotheken ab. Und um die ".EXE"-Datei neu zu ertsellen, muß man den Linker aufrufen.

Die Datei, die die Abhängigkeiten beschreibt, heißt zumeist Makefile. Zwar kann man jeden beliebigen Namen verwenden, aber nach diesem Namen sucht das Make-Programm automatisch. Um die Verwaltung zu erleichtern, geht man oft auch davon aus, daß alle benötigten Dateien eines Projektes in einem eigenen Verzeichnis oder Teilbaum liegen.

Bild 13-19: Einfache make-Datei

Zuerst stehen wie immer Kommentare, um den Sinn der Datei anzugeben. Hier wird unterstellt, daß die Bibliothek fertig und unveränderlich ist. Dies ist bei gekauften Bibliotheken der Fall.

In der Zeile 4, die am Anfang beginnt, steht eine Abhängigkeit. Das Ziel ist ratio.exe. Diese Datei soll letztendlich erzeugt werden. Sie hängt von den Dateien ab, die hinter dem Doppelpunkt aufgeführt werden.

Bild 13-20: Aufbau der Steuerdatei für Tlink

Die nächste Zeile ist schon die Schlußzeile. Wenn make feststellt, daß ratio.exe neu aufgebaut werden muß, weil sie vielleicht noch gar nicht existiert, dann muß das angegebene Kommando ausgeführt werden. Die Kommandozeile ist eingrückt, um sie als Kommando kenntlich zu machen.

Hier wird das Kommando Tlink aufgerufen. Um Problemen mit der möglicherweise zu kurzen Länge der Kommandozeile aus dem Weg zu gehen, wurde eine Steuerdatei für Tlink angelegt, die alle notwendigen Parameter enthält. Solche Dateien nennt man auch response (Antwort)-Dateien.

Die Syntax des Aufrufes ist:

TLINK Objektdateien, Programmdatei, Mapdatei, Bibliotheken

Beachten Sie in diesem Zusammenhang besonders die erste Objektdatei, die das Startmodul beinhaltet, das die Aufgabe hat main() auftzurufen. Die Bibliotheken sind die eigene Bibliothek und drei Bibliotheken des Compilers. Dies sind die allgemeine C-Bibliothek, die mathematische Bibliothek und die Emulatorbibliothek für den vermutlich fehlenden 80X87-Koprozessor.

Der Aufruf geschieht einfach mit make. Falls die Projektdatei makefile hieß, wird sie automatisch gelesen und bearbeitet.

Als Ergebnis erhalten wir das ablauffähige Programm ratio.exe.

Aufruf von make

Allgemein ist die Aufrufsyntax von make:

make [Optione(en)] [Ziel(e)]

Die angegebenen Ziele werden erstellt. Wurde kein Ziel angegeben, wird das erste Ziel in der Make-Datei erstellt. Falls nicht mit der Option "-f" eine Make-Datei angegeben wird, sucht make im momentanen Verzeichnis nach Makefile oder makefile.

Die wichtigsten Optionen von make sind:

Arbeitsweise von make

Make liest die Projektdatei (makefile) ein und baut einen internen Abhängigkeitsbaum auf. Letzlich weiß dann make welche Datei von welchen anderen in welcher Reihenfolge abhängt. Die Datei, die am Anfang der Zeile steht und der ein Doppelpunkt folgt, nennen wir Zieldatei. Die Dateien, von denen die Zieldatei abhängt, nennen wir Basisdateien.

Der Auslöser für eine Aktion ist der Zeitvergleich. Ist eine Basisdatei jünger, wie die zugehörige Zieldatei, dann muß die zugehörige Aktion ausgeführt werden. Die gesamte Wirkung von make hängt damit von der richtigen Angabe der Zeiten in den Dateiverzeichnissen ab. Solange alle Dateien immer auf der gleichen Maschine liegen und die Uhr läuft, ergeben sich keine Probleme.

Schwierig kann es werden, wenn man Dateien von anderen Maschinen per Datenträger oder Netzwerk holt. Hier muß man sicherstellen, daß die Zeiten der Dateien korrekt sind.

Aufbau der Make-Datei

Mit einer Make-Datei verfolgt man zwei Ziele. Zum einen sollen die Abhängigkeiten beschrieben werden und zum anderen die notwendigen Schritte, um eine veraltete Zieldatei wieder auf den neuesten Stand zu bringen.

Beide Ziele werden mit sogenannten Regeln erreicht. Eine Regel beschreibt eine Abhängigkeit und die notwendigen Aktionen. Die Beschreibung des Ziels mit den abhängigen Dateien beginnt in der ersten Spalte des Textes, die Aktionen werrden eingerückt. Mehrere Aktionen stehen untereinander, bis wieder eine neue Regel in der ersten Spalte beginnt. Zum Einrücken können Lerrzeichen und Tabulatoren verwendet werden.

Viele Aktionen sind häufig wiederkehrende Standardaktionen. Wenn eine Objektdatei veraltet ist, dann wird immer der Compiler gerufen werden müssen, um die Objektdatei aus der geänderten Quelle zu erstellen.

Make-Dateien kennen daher explizite Regeln für Spezialfälle und implizite Regeln für Standardfälle.

Es gibt drei weitere Einträge in Make-Dateien, die den Umgang erleichtern sollen. Natürlich gibt es Kommentare und Kommentarzeilen, die mit einem "#" beginnen. Kommentare gelten bis zum Ende einer Zeile. Soll sich der Kommentar über mehrere Zeilen erstrecken, muß das "#"-Zeichen vor jeder Zeile wiederholt werden.

Es bleiben noch Makrozeilen und Steueranweisungen. Makros entsprechen in ihrem Aufbau den Umgebungsvariablen. Einem frei definierten Makronamen kann man einen beliebigen Ersetzungstext zuweisen. Damit lassen sich Make-Dateien leichter an verschiedene Umgebungen anpassen. Steueranweisungen kontrollieren den Ablauf des make-Programmes.

Einträge in einer Make-Datei:

Im Bild 19 wurde bereits eine einfache Make-Datei mit Kommentarzeilen und einer expliziten Regel vorgestellt.

Explizite Regeln

Eine explizite Regel setzt sich aus einer Abhängigkeitsbeschreibung und einem Befehlsrumpf zusammen.

# explizite Regel
zieldatei: Quelldatei(en)
 [Befehle]

Die explizite Regel gibt eine Zieldatei an und, nach einem Doppelpunkt, eine Liste von Basisdateien. Ist einer der Basisdateien jünger als die Zieldatei, werden die angegebenen Befehle ausgeführt.

Bild 13-21: Grundlegendes make-Beispiel

Vor der Ausführung der Befehle prüft Make jedoch erst, ob eine Basisdatei in einer weiteren Regel als Ziel beschrieben wird. Ist dies der Fall, wird zuerst die Regel für die Basisdatei ausgewertet. make arbeitet sich also rekursiv durch die Regeln.

Im ersten Beispiel (in der Datei make1) hängt die Datei ziel1.dat von der Datei quelle1.txt ab. Wird von make erkannt, daß die Basisdatei älter als die Zieldatei ist, dann werden die Befehle im Rumpf der Regel ausgeführt.

Zum Ausprobieren genügt es, eine Datei quelle1.txt mit einem beliebigen Inhalt anzulegen. Ruft man nun zweimal hintereinander make -f make1 auf, sieht man, daß beim ersten Mal kopiert wird, beim zweiten Mal jedoch keine Aktion mehr erfolgt. Manche make-Programm geben in diesem Fall eine Meldung "Ziel ist up to date" aus.

Bild 13-22: Make-Datei mit Befehlspräfix

Ein Befehl kann mit einem Präfix versehen werden, der die Bearbeitung dieses Befehls steuert.

Im zweiten Beispiel soll make den jeweiligen Befehl vor seinem Aufruf nicht anzeigen.

Das Ziel wird oft von mehreren Dateien abhängen. Eine Programmdatei hängt z. B. von Objektdateien, Informationsdateien und Bibliotheken ab. Das Beispiel "make3" zeigt die Verwendung bei mehreren Dateien, die einzeln erstellt werden, aber zu einem gemeinsamen Projekt gehören.

Die Zielprojekt besteht aus zwei Dateien. Es werden keine Befehle spezifiziert. In vielen Make-Dateien findet sich ein ähnlicher Eintrag mit dem Namen all. Ruft man make ohne spezielles Ziel auf, dann werden alle Teilziele erstellt, die hinter all angegeben sind.

Bild 13-23: Ziel mit mehreren Basisdateien

Da hinter dem Ziel "projekt1" zwei Basisdateien stehen, die ihrerseits wieder Ziele sind, wird make rekursiv die angebenen Namen prüfen. Sind zwei Dateien quelle1.txt und quelle2.txt vorhanden und jünger als die dazugehörigen Zieldateien, dann werden die Ziele durch die Kopierbefehle erstellt.

Diese Make-Datei könnte auch mit der folgenden Zeile aufgerufen werden.

make -f make3 ziel2.dat

Damit würde ein Teilziel erstellt. Je größer das Gesamtprojekt ist, desto wahrscheinlicher werden in den Make-Dateien sinnvolle Teilziele spezifiziert.

Beim Testen von Make-Dateien kann ein weiteres Kommando aus der UNIX-Welt gute Dienste leisten. Mit touch werden die Zeitstempel der angegebenen Dateien neu gesetzt. Wendet man touch auf Basisdateien an, werden die zugehörigen Ziele mit make neu erstellt.

touch Dateiliste

Implizite Regeln

Im nächsten Beispiel (make4) wird eine implizite Regel eingeführt. Die Dateien mit der Kennung ".dat" hängen in unserem Beispiel immer von ihren ansonsten gleichnamige Schwesterdateien mit der Kennung ".txt" ab. Mit einer impliziten Regel kann man angeben, wie dieser Übergang ablaufen soll.

Bild 13-24: Make-Datei mit impliziter Regel

Bei den Befehlen einer impliziten Regel, müssen diese mit den Dateinamen versorgt werden, die sie bearbeiten sollen. Dazu dienen vordefinierte Makros.

make wird bei der Bearbeitung die Makros automatisch zu den zugehörigen Dateinamen expandieren und damit die Befehle aufrufen. Bei der Verwendung von Makros stehen diese normalerweise in runden Klammern. Die Klammern dürfen nur dann entfallen, wenn der Makroname aus nur einem Zeichen besteht. Dies ist für viele vordefinierte Makronamen der Fall.

In der Datei make4 wird wieder ein Projekt definiert, daß aus zwei Zieldateien mit der Kennung ".dat" bestehen soll. Die Zieldateien können jeweils durch Kopieren der Quelldateien mit gleicher Kennung ".dat" erzeugt werden. Die eigentlichen Basisdateien haben aber die Kennung ".txt". Für die Wandlung von ".txt"-Dateien auf ".dat"-Dateien gibt es nun eine implizite Regel.

Implizite Zielangaben bestehen aus einem Punkt, der Basisdateikennung, einem weiteren Punkt und der Zieldateikennung. Danach kann wieder ein Befehlsrumpf folgen.

# Implizite Regel
.Quell-Kennung.Ziel-Kennung:
[Befehkl(e)]

Makros

Makros treten in einer Make-Datei in zwei unterschiedlichen Arten auf. Die einen bestehen nur aus einem vordefinierten Buchstaben und dienen zur Generierung von Dateinamen, die anderen werden vom Anwender selbst definiert und tragen einen frei gewählten Namen.

In den Befehlen der impliziten Regeln können u. a. folgende vordefinierte Makros benutzt werden:

$* Quellname (ohne Kennung) mit Pfad
$< Quellname mit Pfad
$. Quellname ohne Pfad
$@ Zielname mit Pfad
$? Quellname mit Pfad

Je nach Implementierung sind noch weitere Makros definiert. Die Makros können auch in den expliziten Regeln verwendet werden. Dabei ändert sich aber ihre Bedeutung. An Stelle vom Quelldateinamen geben sie in expliziten Regeln den Zieldateinamen an. Nur $@ gibt immer die Zieldatei an.

In expliziten Regeln gibt außerdem $? die Liste der neueren Quelldateien an.

Eigene Makros können ebenfals definiert werden. Ein häufig verwendetes Makro ist CFLAGS. Mit diesem Makro werden alle Optionen angegeben, die beim verwendeten Compiler benötigt werden. Makros werden wie eine globale Variable definiert. Unter UNIX gilt in beiden Fällen, daß keine Leerzeichen bei beim "="-Zeichen erlaubt sind.

Im Beispiel werden die CFLAGS nicht an den Compiler übergeben, sondern in eine Hilfsdatei geschrieben. Die Quelldatei wird dann zusammen mit der Hilfsdatei in die Zieldatei kopiert.

Makros können auch außerhalb der Make-Datei definiert werden. Wird in einer Make-Datei ein Makro verwendet, aber nicht definiert, dann sucht make nach einer möglichen Definition in der Kommandozeile.

Ist auch in der Kommandozeile kein Makro definiert worden, dann sucht make in der Programmumgebung nach einer Umgebungsvariable gleichen Namens.

Bild 13-25: Definition eigener Makros

Das Beispiel make5a könnte wie folgt aufgerufen werden:

make -f make5a -DCFLAGS="-O -S"

Alternativ könnte, wie erwähnt, die Makrodefinition im Aufruf entfallen, und durch eine Umgebungsvariable ersetzt werden.

Anweisungen an make

Neben den bisher besprochenen Regeln und Kommentaren, kann die Make-Datei auch Steueranweisungen für das make-Programm enthalten. Die Anweisungen beginnen in der ersten Spalte mit einem Punkt.

# Punktbefehle
.silent schaltet die Anzeige der ausgeführten Befehle aus
.precious schaltet das automatische Löschen von Zielen aus
.ignore schaltet den automatischen Abbruch bei Fehlern aus
.suffixes gibt Suchreihenfolge für implizite Regeln an

Steueranweisungen, wie silent oder ignore, können in ihr Gegenteil verkehrt werden, wenn man ein "no" nach dem Punkt einfügt.

Die Steueranweisung .suffixes gibt eine Liste der verwendeten kennungen an. Sollte es bei der Bearbeitung einer impliziten Regel zu Mehrdeutigkeiten kommen, dann sucht make die Liste der kennungen ab, die mit .suffixes definiert worden sind. Eine Mehrdeutigkeit ergibt sich z. B. beim Linekn von Objektdateien. Objektdateien können mit einem beliebigen Compiler oder Assembler erzeugt werden. Die Frage, welche Quelldatei übersetzt werden muß, entscheidet man mit .suffixes. Wird darin die Kennung ".c" vor der Kennung ".pas" gefunden, dann wird eben der C-Compiler gerufen.

Es ist bei der Angabe der Kennungen wichtig, daß mögliche Zielkennungen vor den möglichen Quelldateikennungen angegeben werden.

Globale Regel- und Makrodatei

Die meisten Implementierungen von make gestatten es, immer wiederkehrende Makros in speziellen Startdateien zu halten. Häufig wird die Datei builtins.mak im Verzeichnis des make-Programms verwendet. Andere Namen sind /usr/lib/makemacros und /usr/lib/makeactions.

Die jeweilige Dokumentation sagt, welcher Name paßt. Es ist auf alle Fälle interessant, sich diese Dateien einmal näher anzusehen. Als Anwender sollte man diese Dateien nur für wirklich häufige Standardfälle verändern.


Zum Inhalt