zurück zum News-Archiv
> Anfang zurück vorwärts Ende ## ANSI C - Ein Programmierkurs - Teil IX ### Stack Jedes Programm benutzt einen Speicher als Stack. Ein Stack ist ein Stapel (last in, first out), d. h. das, was zuletzt auf den Stack gelegt wird, wird auch zuerst wieder herausgenommen. Er wird vom Programm dazu benutzt, die Parameter für einen Funktionsaufruf und auch lokale Variablen aufzunehmen. Der Stack wächst von großen Adressen zu kleinen Adressen. Ein C-Programm legt bei einem Funktionsaufruf üblicherweise die Parameter beginnend von rechts der Reihe nach auf den Stack. Hat sich die Funktion beendet, entfernt der Aufrufer die Parameter vom Stack. TC und PC benutzen in der Grundeinstellung allerdings Register für die Parameterübergabe. Die Übergabe über den Stack kann auch mit dem Schlüsselwort `cdecl` erzwungen werden. Ein Pascal-Programm legt die Parameter von links nach rechts auf den Stack und die aufgerufenen Funktion entfernt die Parameter vom Stack. Diese Art der Parameterübergabe kann mit dem Schlüsselwort `pdecl` erzwungen werden. `cdecl` bzw. `pdecl` werden zwischen Returnwert und Funktionsnamen angegeben:
>     int cdecl Test(int i)
>     {
>        return i;
>     }
> Mit diesem Wissen kann nun auch die erforderliche Stackgröße abgeschätzt werden. Für jede Funktion, die in der Aufrufhierarchie eine Ebene tiefer geht, muss der Platz, den die Parameter und die lokalen auto-Variablen dieser Funktion belegen, addiert werden. Bei einer Rekursion wäre dies also jeder Aufruf von sich selbst. Für Funktionen, die nacheinander aus der gleichen Funktion aufgerufen werden, darf der Speicherverbrauch natürlich nicht einfach aufsummiert werden, sondern beide Aufrufe werden getrennt betrachtet; für eine Abschätzung ist das Maximum relevant. ## Preprozessor Was bis jetzt noch nicht erklärt wurde, sind die Zeilen mit dem Lattenkreuz bzw. Hash '#' als ersten Buchstaben. Es handelt sich hierbei um Anweisungen für den Preprozessor. Der Preprozessor ist in der Regel ein eigenständiges Programm, das vor dem eigentlichen Compiler den Quelltext bearbeitet. Eine Anweisung für den Preprozessor kann an jeder Beliebigen Stelle im Quelltext stehen, muss aber eine eigene Zeile belegen. Natürlich muss das Ergebnis nach dem Preprozessorauf wieder ein korrektes C-Programm ergeben. Im Folgenden werden die Möglichkeiten des Preprozesors der Reihe nach besprochen.
>     #include <stdio.h>
> Die `Include`-Anweisung fügt den Inhalt der angegebenen Datei in den Quellcode ein. Ist der Name wie im obigen Beispiel in spitze Klammern gesetzt, wird nur das Verzeichnis nach dieser Datei durchsucht, das bei der Installation des Compilers als Include-Verzeichnis angegeben wurde. Dies wird für die mit dem Compiler mitgelieferten Include-Dateien benötigt. Wird der Name in doppelten Hochkommata angegeben, wird zuerst das aktuelle Verzeichnis und danach das Systemverzeichnis durchsucht. Dies wird ausgenutzt, um ein Programm modular zu compilieren und dazu entsprechende Include-Dateien anlegen zu können. Dazu finden sich Beispiele im Kapitel über modulares Kompilieren.
>     #define PI 3.141592
> Mit der `define`-Anweisung wird eine Regel für Textersetzungen definiert. Überall, wo im Quellcode der erste Text auftaucht, wird er durch den zweiten ersetzt. Als Konvention werden Konstanten, die durch `define` definiert werden, üblichereise in Großbuchstaben geschrieben. Dies ist aber kein Muss. Die Möglichkeiten dieser Textersetzung gehen so weit, dass der Preprozessor auch in der Lage ist, Parameter zu erkennen und damit auch Makros definiert werden können. Als Beispiel lässt sich das Minimum zweier Zahlen auch als Makro über den Preprozessor lösen und ist damit unabhängig von dem tatsächlich verwendeten Datentyp:
>     #define max(x,y) ((x)>(y)?(x):(y))
> Der Preprozessor erkennt `x` und `y` als Parameter. Im Ersatztext sollten diese Parameter geklammert werden, da es sich um Ausdrücke handeln und unter Umständen durch die Rangfolge der Operatoren ein ganz anderer Ausdruck als der beabsichtigte entstehen kann. Zur Verdeutlichung zeigen die folgenden Beispiele einmal den Quelltext vor und nach dem Preprozessorlauf.
>     #define max(x,y) ((x)>(y)?(x):(y))

>     int main(void)
>        int x,y;

>        y = 2;
>        x = max(3,y+3);
> Nach dem Preprozessorlauf sieht das wie folgt aus:
>     #define max(x,y) ((x)>(y)?(x):(y))

>     int main(void)
>        int x,y;

>        y = 2;
>        x = ((3)>(y+3)?(3):(y+3));
> `#ifdef #else #endif` Mit `#ifdef` kann abgefragt werden, ob ein bestimmter Name per `#define` definiert worden ist. Wenn ja, wird der dahinter stehende Code bis zum `#else` oder `#endif`, wenn kein `#else` vorhanden ist, für den Compiler im Quelltext belassen. Der nicht zutreffende Teil ist für den Compiler nicht sichtbar. Er wird nicht in den Code übernommen, den der Compiler letztendlich auswertet. Es gibt auch die Abfrage, ob ein Name per `#define` nicht definiert ist. Hiermit können bestimmte Teile des Codes eingeschaltet werden, solange noch am Programm entwickelt wird und zusätzliche Ausgaben erwünscht sind. Dadurch, dass der Name nicht definiert wird, erzeugt man die Version, die ausgeliefert wird. Bei vielen Compilern ist es außerdem möglich, für solche Zwecke einen Namen beim Aufruf des Compilers zu definieren. Zur Verdeutlichung ein Beispiel:
>     int main(void)

>        int x;

>        x = 3;
>     #ifdef TEST
>        x += 4;
>     #endif
>        printf("x = %d", x);
> und nun das gleiche Beispiel mit der Definition von TEST.
>     #define TEST

>     int main(void)

>        int x;

>        x = 3;
>     #ifdef TEST
>        x += 4;
>     #endif
>        printf("x = %d", x);
> Es ist hier nicht notwendig, einen Wert für eine Textersetzung anzugeben, da nur die Existenz der Definition von `TEST` abgefragt wird. Es gibt keine Stelle, an der der Text `TEST` durch einen anderen Wert ersetzt werden soll. Die Ausgabe des Programms sollte die Wirkungsweise verdeutlichen. `#if #elif #else #endif` Soll der Preprozessor auch einen Ausdruck auswerten, kann statt der `ifdef`-Anweisung die `if`-Anweisung verwendet werden. Hiermit kann z.B. auch der Wert eines mit `define` definierten Namen abgefragt werden. Um mehrere Bedingungen der Reihe nach abzufragen, gibt es die `elif`-Anweisung.
>     #if SYSTEM == SYSV
>        #define HDR "sysv.h"
>     #elif SYSTEM == BSD
>         #define HDR "bsd.h"
>     #elif SYSTEM == TOS
>         #define HDR "tos.h"
>     #else
>         #define HDR "default.h"
>     #endif
> Und als ein weiteres Beispiel folgt die obige Funktion mit dem Flag `TEST`:
>     #define TEST 1

>     int main(void)

>        int x;

>        x = 3;
>     #if TEST == 1
>        x += 4;
>     #endif
>        printf("x = %d", x);
> Das gleiche Beispiel wie oben prüft hier nicht die Existenz, sondern den Wert von `TEST`. `#pragma` Mit der `Pragma`-Anweisung können compilerspezifische Anweisungen ausgeführt werden. Dies könnten z.B. Einstellungen des Compilers sein. ### `defines` für den Cookie-Jar Mit dem Preprozessor lassen sich auch geeignete Konstanten für das Programm zum Auslesen des Cookie-Jars definieren. Weiterhin kann mit Preprozessor-Anweisungen dafür gesorgt werden, dass sich das Programm sowohl mit TC/PC, Sozobon als auch unter GCC compilieren lässt. Für das Auslesen der Adresse des Cookie-Jars muss mit der GEMDOS-Funktion `Super` der Prozessor in den Supervisor-Mode geschaltet werden. Damit der Compiler den Prototypen kennt, muss die Includedatei für OS-Aufrufe auf dem Atari eingebunden werden. Da rechnerspezifische Dinge nicht genormt sind, hat diese Datei in jeder der 3 Entwicklungsumgebungen einen anderen Namen. Da aber die Compiler ihren eigenen Namen als Konstanten definieren, der auch mit `ifdef` abgefragt werden kann, lässt sich je nach Compiler eine andere Includedatei einbinden. Wenn `__TURBOC__` definiert ist, wird TC als Compiler benutzt und der Name der Includedatei ist `tos.h`. Andernfalls wird geprüft, ob vielleicht `__GNUC__` definiert ist. Wenn ja, ist der Compiler der `gcc` und die benötigte Datei heißt `osbind.h`. Wenn nicht, wird von Sozobon ausgegangen und als Includedatei `tosbind.h` benutzt. PC definiert sowohl `__TURBOC__` zur Kompatibilität mit TC als auch `__PUREC__`.
>     #ifdef __TURBOC__
>     #include <tos.h>
>     #else
>     #ifdef __GNUC__
>     #include <osbind.h>
>     #else
>     #include <tosbind.h>
>     #endif
>     #endif
>     #include <stdio.h>

>     #define _p_cookies (void *)0x5a0l
>     #define NULL_COOKIE 0l

>     typedef struct cookie_entry {
>        union {
>           unsigned long name_long;
>           char name_array[4];
>        } name;
>        unsigned long value;
>     } CookieEntry;

>     CookieEntry *GetCookieJar(void)
>     {  long OldStack;
>        CookieEntry *CookieJar;

>        OldStack = Super(0L);
>        CookieJar = *((CookieEntry**)_p_cookies);
>        Super((void *)OldStack);
>        return CookieJar;
>     }

>     void PrintCookie(CookieEntry *Cookie)
>     {
>        printf("Name des Cookies: %d\n", Cookie->name.name_long);
>        printf("Wert des Cookies: %d\n", Cookie->value);
>     }

>     int IsNullCookie(CookieEntry *Cookie)
>     {
>        return (Cookie->name.name_long == NULL_COOKIE);
>     }

>     void TraverseCookieJar(CookieEntry *Cookie)
>     {
>        while (!IsNullCookie(Cookie))
>        {
>           PrintCookie(Cookie);
>           Cookie++;
>        }
>     }

>     int main(void)
>     {  CookieEntry *CookieJar;

>        CookieJar = GetCookieJar();
>        if (CookieJar == (CookieEntry *)0)
>           printf("Dieses System hat keinen Cookie Jar\n");
>        else
>           TraverseCookieJar(CookieJar);
>        return 0;
>     }
> ## Modulares Kompilieren Bisher wurde nach dem Compilieren das Programm schon mit Bibliotheken, also weiteren Funktionen, zu einem ausführbaren Programm gelinkt. Genauso lässt sich der Code des eigenen Programms auch auf mehrere Dateien aufteilen, die zusammengelinkt werden. Das Aufteilen auf mehrere Dateien bietet folgende Vorteile: Die einzelnen Dateien sind kleiner und damit übersichtlicher. Ein Programm lässt sich in Funktionsgruppen zerlegen und ist damit leichter verständlich. Einzelne Teile lassen sich wiederverwenden. Damit verbunden ist allerdings der höhere Verwaltungsaufwand, da mehrere Dateien übersetzt und gelinkt werden müssen. Diese Arbeit kann aber dem Computer überlassen werden, wenn das Tool make benutzt wird. TC bzw. PC benutzt Projektdateien. Dies ist eine einfachere Variante, die die Einbindung anderer Programme als die der Entwicklungsumgebung nicht erlaubt. Wir werden hier beide Möglichkeiten besprechen, die Aufteilung des Programms ist davon unberührt. Was wäre eine mögliche Aufteilung des Cookie-Programms auf einzelne Dateien? Eine gute Möglichkeit ist es, wie bei der Suche nach Kandidaten für Objekte vorzugehen. Dazu sucht man nach Substantiven, die unsere Objekte bzw. Strukturen liefern. Die zu diesen Substantiven passenden Verben liefern die Methoden bzw. Funktionen. Bei der Vorstellung möglicher Module sollten möglichst viele Ideen über den Aufbau solcher Module vermittelt werden. Deshalb sind die Module nicht komplett nach den gleichen Ideen entworfen worden. Zum einen haben wir den Cookie. Einen Cookie kann man auf einen Wert setzen, auf dem Monitor ausgeben, in eine Datei schreiben, einlesen, aus einer Datei einlesen oder vergleichen. Damit haben wir zwar mehr Funktionen, als hier benötigt. Wir haben aber auch schon genug Ideen für das nächste Kapitel über die ANSI-Bibliotheken. Das Modul enthält auch Funktionen bzw. Makros für das Setzen und Lesen der Komponenten der Struktur. Es ist auch möglich, direkt die Strukturkomponenten zu benutzen. Die Verwendung von entsprechenden Methoden erlaubt es, den Aufbau der Struktur (z.B. für eine Verbesserung) zu ändern, ohne dass sich der Zugriff ändert. Werden Objekte deklariert, kann sogar der Zugriff auf diese Komponenten verhindert werden. Das nennt sich data hiding: der Anwender bekommt nur Zugriff über eine definierte Schnittstelle. Eine Initialisierung des Cookie-Entrys ist nicht erforderlich, weil es keine sinnvolle Vorbesetzung gibt oder der Cookie sowieso aus dem Cookie-Jar stammt. Neue Cookies müssen sowieso komplett auf ihren Wert gesetzt werden. Dann haben wir den Cookie-Jar. Zuerst muss der Cookie-Jar ermittelt werden, wir können nach bestimmten Cookies suchen und den Cookie-Jar durchwandern. Der Linker kann nur gesamte Objektdateien zu einem Programm zu linken und keine einzelnen Funktionen aus einem Modul herausholen. Deshalb wird üblicherweise jede Funktion in eine Einzelne Datei geschrieben und die daraus resultierenden Objektdateien zu einer Bibliothek gelinkt. Wir begnügen uns hier allerdings mit einer Objektdatei. Die einzelnen Module werden nun der Reihe nach vorgestellt. Funktionen, für die noch zusätzliches Wissen erforderlich ist, werden zunächst ohne Funktionalität implementiert. Damit die für ein Modul definierten Datentypen auch in anderen Modulen verwendet werden können und der Compiler die Parameter eines Funktionsaufrufs auf Korrektheit prüfen kann, schreiben wir eine Headerdatei. Die Headerdatei fragt zuerst ab, ob wir einen bestimmten Namen definiert haben. Wenn ja, ist unsere Includedatei schon bekannt und wir müssen unsere Definitionen verstecken, um Fehlermeldungen oder Warnungen es Compilers zu vermeiden. Wenn nein, definieren wir zuerst den Namen und anschließend alles, was unser Modul anderen zur Verfügung stellt. ### `cookie.h` Diese Headerdatei zeigt, wie eine Mehrfachincludierung zustande kommen könnte. In den Funktionsprototypen wird die Struktur `FILE` benötigt. Damit ein Benutzer unseres Moduls nicht wissen muss, was er noch includieren muss, tun wir dies selbst. Deshalb wird die `stdio.h` includiert. Da wir in `cookie.c` aber Ein- und Ausgabe machen wollen, benötigen wir die Definitionen aus `stdio.h` dort auch. Um nicht wissen zu müssen, ob diese Includedatei indirekt benutzt wird, wird sie auch dort includiert. Dadurch würden die Definitionen zweimal gemacht. Dies wird von der Abfrage, ob `COOKIE_H` definiert ist und der anschließenden Definition verhindert.
>     #ifndef COOKIE_H

>     #define COOKIE_H

>     #include <stdio.h>

>     #define NULL_COOKIE 0l

>     typedef struct cookie_entry {
>        union {
>           unsigned long name_long;
>           char name_array[4];
>        } name;
>        unsigned long value;
>     } CookieEntry;

>     #define CookieGetValue(x) (x)->value
>     #define CookieGetNameL(x) (x)->name.name_long
>     #define CookieGetNameS(x) (x)->name.name_array

>     void CookieSetL(CookieEntry *Cookie,long Name,long Value);
>     void CookieSetS(CookieEntry *Cookie,char *Name,long Value);
>     void CookiePrint(CookieEntry *Cookie);
>     void CookieInput(CookieEntry *Cookie);
>     int CookieIsNullCookie(CookieEntry *Cookie);
>     int CookieIsCookie(CookieEntry *Cookie,long Name);
>     int CookieRead(CookieEntry *Cookie,FILE *stream);
>     int CookieWrite(CookieEntry *Cookie,FILE *stream);

>     #endif
> ### `cookie.c` Und nun noch die dazugehörige C-Datei. Damit der Compiler prüfen kann, ob die Funktionen auch den in der Includedatei angegebenen Prototypen entsprechen, wird die Includedatei auch hier includiert. Da dieses Modul allein kein ablauffähiges Programm ergeben soll, sondern nur Funktionalität zur Verfügung stellt, fehlt hier die Funktion `main`. Einige Funktionen sind noch leer bzw. enthalten nur ein `return` mit einem entsprechenden Returnwert. Diese Funktionen werden später bei der Besprechung der ANSI-Libs ausgefüllt, wenn das nötige Wissen zur Verfügung steht.
>     #include <string.h>
>     #include <stdio.h>
>     #include "cookie.h"

>     void CookieSetL(CookieEntry *Cookie,long Name,long Value)
>     {
>        Cookie->name.name_long = Name;
>        Cookie->value = Value;
>     }

>     void CookieSetS(CookieEntry *Cookie,char *Name,long Value)
>     {
>        memcpy(Cookie->name.name_array,Name,4);
>        Cookie->value = Value;
>     }

>     void CookiePrint(CookieEntry *Cookie)
>     {
>        printf("Name des Cookies: %d\n", Cookie->name.name_long);
>        printf("Wert des Cookies: %d\n", Cookie->value);
>     }

>     void CookieInput(CookieEntry *Cookie)
>     {
>     }

>     int CookieIsNullCookie(CookieEntry *Cookie)
>     {
>        return Cookie->name.name_long == NULL_COOKIE;
>     }

>     int CookieIsCookie(CookieEntry *Cookie,long Name)
>     {
>        return Cookie->name.name_long == Name;
>     }

>     int CookieRead(CookieEntry *Cookie,FILE *stream)
>     {
>        return 0;
>     }

>     int CookieWrite(CookieEntry *Cookie,FILE *stream)
>     {
>        return 0;
>     }
> ### `cjar.h` Dieses Modul enthält eine Funktion zur Initialisierung, die den Cookie-Jar ermittelt. Auch hier wird das Prinzip des data hiding angewandt und der Cookie-Jar nicht zurückgeliefert. Stattdessen wird nur ein Wert von 0 geliefert, wenn kein Cookie-Jar existiert und ein Wert ungleich 0, wenn der Cookie-Jar ermittelt wurde. Achtung: Das Modul geht davon aus, dass der Cookie-Jar während der Laufzeit des Programms nicht mehr verändert wird. Dies ist, insbesondere unter einem Multitasking-OS, nicht garantiert. Für diesen Kurs ist diese Vereinfachung allerdings zulässig, da in der Regel der Cookie-Jar von Programmen nicht verändert wird.
>     #ifndef CJAR_H

>     #define CJAR_H

>     #include cookie.h

>     typedef void (*CookieAction)(CookieEntry *Cookie);

>     int CjarInit(void);
>     CookieEntry *CjarSearchL(long Name);
>     CookieEntry *CjarSearchS(char *Name);
>     void CjarTraverse(CookieAction ActionFkt);

>     #endif
> ### `cjar.c` Die Implementierung zeigt auch, wie ein Modul automatisch initialisiert werden kann. Dazu wird einfach der Zeiger auf den Cookie Jar mit einem entsprechenden Wert initialisiert. Dies ist `NULL` für einen ungültigen Zeiger. Bei jedem Zugriff kann die Initialisierung aufgerufen und geprüft werden, ob dieser Zeiger noch ein ungültiger Zeiger ist. Weiterhin nutzt diese Modul auch Funktionspointer für das Durchwandern des Cookie-Jars. Damit kann eine Funktion, die etwas mit einem Cookie tut, an anderer Stelle programmiert werden. Dieses Modul weiß, wie man den Cookie-Jar durchläuft und ruft für jeden Cookie die Funktion auf, die etwas mit einem einzelnen Cookie tut.
>     #ifdef __TURBOC__
>     #include <tos.h>
>     #else
>     #ifdef __GNUC__
>     #include <osbind.h>
>     #else
>     #include <tosbind.h>
>     #endif
>     #endif

>     #include <stddef.h>
>     #include cookie.h
>     #include cjar.h

>     #define _p_cookies (void *)0x5a0l

>     CookieEntry *CookieJar = NULL;

>     int CjarInit(void)
>     {  long OldStack;

>        OldStack = Super(0L);
>        CookieJar = *((CookieEntry**)_p_cookies);
>        Super((void *)OldStack);
>        return (CookieJar != NULL);
>     }

>     CookieEntry *CjarSearchL(long Name)
>     {
>        return NULL;
>     }

>     CookieEntry *CjarSearchS(char *Name)
>     {
>        return NULL;
>     }

>     void CjarTraverse(CookieAction ActionFkt)
>     {  CookieEntry *AktCookie;

>        if (CookieJar != NULL)
>           CjarInit();
>        if (CookieJar != NULL)
>        {
>           AktCookie = CookieJar;
>           while (!CookieIsNullCookie(AktCookie))
>           {
>              (*ActionFkt)(AktCookie);
>              AktCookie++;
>           }
>        }
>     }
> ### `cmain.c` Die `main`-Funktion kann mit solchen umfangreichen Modulen sehr einfach gehalten werden. Es muss lediglich `cjar` initialisiert werden. Anschließend kann mit `CjarTraverse` der Cookie-Jar durchwandert werden. Die übergebene Funktion ist einfach die Ausgabefunktion aus dem Modul `cookie`.
>     #include <stdio.h>
>     #include "cookie.h"
>     #include "cjar.h"

>     int main(void)
>     {
>        if (CjarInit())
>           CjarTraverse(CookiePrint);
>        else
>           printf("Dieses System hat keinen Cookie Jar\n");
>        return 0;
>     }
> ### `cookie.prj` TC/PC bietet mit den Projektdateien eine Möglichkeit, automatisch von der Entwicklungsumgebung entscheiden zu lassen, welche Dateien übersetzt werden müssen. Die Projektdatei gibt außerdem an, welche Dateien zusammen das Programm bilden. Das Beispiel unten ist die Projektdatei für TC, für PC muss nur in den Angaben für `*.LIB` und `*.O` das TC gegen PC getauscht werden. In der ersten Zeile steht der Name des Programms, das das Ergebnis bildet. Nach dem Gleichheitszeichen folgen sämtliche Dateien, die das Projekt bilden. Die erste dieser Dateien ist immer der Startup-Code. Dies ist der erste Code, der nach dem Start des Programms ausgeführt wird. Dieser Code ruft die Funktion `main` auf. Anschließend folgen unsere C-Dateien und zum Schluss die benötigten Bibliotheken. Wenn TC/PC sieht, dass der angegebene Quellcode neuer ist als die daraus übersetzte Objektdatei, wird diese Datei neu compiliert und das Projekt gelinkt. Es ist zusätzlich möglich, in Klammern Dateien anzugeben, von denen eine Datei abhängt. In `cmain.c` werden `cjar.h` und `cookie.h` includiert, also werden diese Dateien noch mit angegeben. Wenn sich `cjar.h` oder `cookie.h` ändern, also damit neuer sind als `cmain.c`, so wird `cmain.c` neu compiliert. Auf diese Weise kann der Compiler prüfen, ob die Definitionen aus den Includedateien noch richtig benutzt werden. Eine Abhängigkeit von den C-Dateien (`cjar.c` und `cookie.c`) ist nicht vorhanden. Wenn nur die C-Dateien ohne die Includedateien geändert werden, dann hat sich die Schnittstelle, also die Datentypen und die Funktionen, nicht verändert. Wie eine Funktion intern arbeitet, ist aber für den Aufruf der Funktion nicht wichtig.
>     COOKIE.TOS
>     =
>     TCSTART.O
>     CMAIN.C (CJAR.H, COOKIE.H)
>     CJAR.C (CJAR.H, COOKIE.H)
>     COOKIE.C (COOKIE.H)
>     TCSTDLIB.LIB
>     TCTOSLIB.LIB
> ### `makefile` Für Sozobon und GCC kann das Tool make benutzt werden. Da make per default nach der Datei `makefile` sucht, bekommt unser Makefile auch diesen Namen. In den ersten zwei Zeilen wird ein Makro definiert. Die Zeile mit dem Definition, also wird die erste Zeile auskommentiert. Für Sozobon ist die erste Definition zuständig. Dieses Makro legt fest, welches Programm für das Übersetzen aufzurufen ist. Auch für das Linken des Programms wird cc bzw. gcc aufgerufen. Dieses Programm erkennt anhand der Parameter selbständig, ob der eigentliche Compiler oder der Linker zu starten ist. In einem Makefile gibt man eine Datei an, gefolgt von einem Doppelpunkt und den Dateien, von dem diese Datei abhängt. Darunter steht die Aktion, die ausgeführt werden soll. Achtung: jede Aktion beginnt mit einem Tabulator! Wenn der Editor stattdessen Leerzeichen einfügt, funktioniert die Makedatei nicht! Make sucht nach der ersten Regel und versucht, sie auszuführen. Das wäre das komplette Programm, das natürlich von den Objektdateien aus unseren Quellen abhängt. Sind die Objekte neuer, wird die Aktion ausgeführt und das Programm neu gelinkt. Da für die Objekte auch wieder entsprechende Regeln definiert sind, werden auch diese überprüft. Eine Objektdatei ist natürlich von der Quelldatei und auch von den benutzen Includedateien abhängig. Immer dann, wenn sich die Schnittstelle zu dem Modul, also die Datentypen und Funktionen ändern, ändert sich die Includedatei. Damit ist diese neuer als die Quelle und die Quelle wird neu übersetzt. So wird garantiert, dass unsere veränderten Definitionen benutzt werden. Da in einem Makefile die Aktion explizit angegeben wird, bietet es noch mehr Möglichkeiten als die Projektdatei von TC/PC. Es lassen sich beliebige Abhängigkeiten und dazu beliebige Aktionen definieren.
>     CC=	gcc

>     cookie.tos:	cmain.o cjar.o cookie.o
>     	 cmain.o cjar.o cookie.o -o cookie.tos

>     cmain.o: cmain.c
>     	 -c cmain.c

>     cjar.o: cjar.c
>     	 -c cjar.c

>     cookie.o: cookie.c
>     	 -c cookie.c
Der nächste Teil gibt einen Einblick in die ANSI-Bibliotheken Michael Bernstein

Anfang zurück vorwärts Ende Seitenanfang

Copyright und alle Rechte beim ATOS-Magazin. Nachdruck und Veröffentlichung von Inhalten nur mit schriftlicher Zustimmung der Redaktion.
Impressum - Rückmeldung via Mail oder Formular - Nachricht an webmaster