Programmieren in C

| ### Programmieren in C von Prof. Jürgen Plate

# 9 Programmierstil, Fallstricke, Module

## 10.1 Programmierstil in C

Dies ist eine kleine Stilkunde, die das Schreiben klarer Programme fördern soll. Diese Hinweise und Erfahrungen sollten an Bedeutung gewinnen, so wie Ihre Erfahrung mit C wächst.

### Allgemeines

- Benutzen Sie `#define` für symbolische Konstanten. Wenn ihr Wert verändert werden muss, braucht nur eine Zeile des Programms (die mit dem `#define`) geändert zu werden. Noch besser ist der Einsatz von `const`, weil dabei auch noch der Datentyp festgelegt wird.
- Benutzen Sie ausschließlich Großbuchstaben für Konstanten und gemischte Schreibweise oder das Unterstreichungszeichen ( _ ) für Variable und Funktionsnamen. Namen wie GetData oder get_data sind leichter zu lesen als kleingeschriebene Namen ohne Unterstreichungszeichen (wie `getdata`).
- Verwenden Sie "sprechende" Variablennamen, auch wenn dadurch die Schreibarbeit um 5% zunimmt. Also nicht (wie in den folgenden Beispiel-Schnipseln praktiziert) f, t, ms und v, sondern frequenz, zeit, motor_status und geschwindigkeit.
- Passen Sie besonders beim doppelten Gleichbeitszeichen (= =) auf, das für die Abfrage auf Gleichheit benutzt wird. Wenn wir in einer if-Anweisung prüfen wollen, ob zwei Werte, A und B, gleich sind, tun wir das, wie unten links dargestellt. Rechts zeigen wir einen häufigen Fehler, die Benutzung eines einfachen Gleichheitszeichens, was eine völlig andere Bedeutung hat.

      if (A == B)                if (A = B)
        anweisung1;                 anweisung1;
      else                       else
        anweisung2;                 anweisung2;

  Was ist der Unterschied? Im linken Fall werden A und B verglichen und als wahr oder falsch bewertet, je nachdem, ob sie gleich sind oder nicht. Im rechten Fall handelt es sich um eine Zuweisung, die als Nebeneffekt ebenfalls einen Wert als Ergebnis liefert. Der Wert von B wird an A zugewiesen, und der neue Wert von A ist der Wert der Zuweisung. Er wird immer als wahr (ungleich null) bewertet, es sei denn, B ist gleich null.
- Benutzen Sie Funktionen, um Ihre Programme kurz und modular zu halten. Funktionen verkürzen das Hauptprogramm und machen dadurch den Gesamtablauf klarer. Ein Funktionsname sollte, wie alle anderen Namen auch, den Zweck erkennen lassen. Funktionen sollten verhältnismäßig kurz (50 bis 200 Zeilen) sein und großzügig kommentiert werden.
- C sieht nur minimale Fehlerprüfung vor, um dem Programmierer vollen Zugang zu der tatsächlich eingesetzten Maschine zu ermöglichen. Es wird unterstellt, daß Programmierer wissen, was sie tun. Es liegt in der Verantwortung des Programmierers, Arraygrenzen und Parametertypen zu überprüfen. Einige wenige sorgfältige Prüfungen können lange Testzeiten ersparen.

### Über das Schreiben von Ausdrücken

Wegen der großen Zahl von Operatoren, des nicht immer einleuchtenden Vorrangs und der Regeln der automatischen Typangleichung liegt es in der Verantwortung des Programmierers sicherzustellen, daß ein Ausdruck in der erwarteten Art und Weise arbeitet. Es folgen hier einige Anregungen, die es Ihnen erleichtern sollen, leicht lesbare, effiziente (und fehlerfreie) Programme zu schreiben.

- Einige der Vorrangregeln in C sind nicht einleuchtend. Falls Sie unsicher sind, benutzen Sie Klammern, um die Reihenfolge und Priorität der Bewertung anzugeben. Denken Sie jedoch daran, daß der Compiler die Bewertungsreihenfolge bestimmter Ausdrücke (solche, die nur die Operatoren &, /, ^, + oder * enthalten), selbst bei Anwesenheit von Klammern, frei wählen kann.
- Typumwandlungen können im allgemeinen benutzt werden, um die Angleichungen an den erforderlichen Typ zu erzwingen. Da Arithmetik mit unterschiedlichen Typen automatische Typangleichung mit sich bringt, die manchmal anders als erwartet ausfällt, benutzen Sie eine Umwandlung, um das Gewünschte zu erhalten.
- Es gibt keine Leistungseinbuße durch die Bewertung von Konstantenausdrücken, da sie zur Compilationszeit bewertet werden. Zum Beispiel wandelt der Compiler den Ausdruck `a = 17.3 / 14.7 * PI / vals[i];` (vorausgesetzt, PI ist als 3.14159 definiert worden) in `a = 3.697245371 / vals[i];` um.
  Auch die bei hardwarenaher Programmierung geübte Praxis, einzelne Steuerbits nicht direkt durch Angabe eines Hexadezimalwerts, sondern durch einen Ausdruck zu definieren, führt nicht zu einer Leistungseinbuße (z. B. ist `X = Y & (1 << 4)` gleichwertig mit `X = Y & 0x10`).
- Einige C-Operationen können geschickt in vielfältiger Weise eingesetzt werden. Zum Beispiel kann das logische UND zweier Bedingungen manchmal durch das bitweise UND ersetzt werden, was möglicherweise schneller zu bewerten ist. Das ist jedoch nur angebracht, wenn Sie alle Bedingungen bewertet haben wollen.
- Seien Sie vorsichtig bei der Benutzung der additiven Operatoren `++` und `--`. Im allgemeinen sollten Sie nicht mehr als einen vor- oder nachgestellten Operator in einem einzelnen Ausdruck benutzen. Welches Ergebnis haben beispielsweise die folgenden Ausdrücke?

  `a[i++] = a[--i];`

  `a[--i] = b[++i];`

  `a+++++b;`

- Es ist leicht, unklare Ausdrücke zu schreiben. Selbst wenn diese Ausdrücke vom Compiler eindeutig interpretiert werden können, werden sie doch die meisten Programmierer verwirren. Wenn Sie sich nicht sicher sind, vereinfachen Sie es.
- Verlassen Sie sich nicht auf Eigenschaften von Daten oder Codes. Legt man ASCII zugrunde, ist ein Ausdruck wie z. B. `('A' + ' ') == 'a'` wahr. Wird irgendwann einmal zu einem anderen Code gewechselt, geht das schief.
- Extrem gefährlich können implizite Typumwandlungen sein. Betrachten Sie dazu das folgende Codefragmente:

      ...

      int a = -1;
      unsigned int b = 4;

      ...

      if (a < b)
        { printf("Gotcha!\n");

      ...

  Wird der Text "Gotcha!" ausgeben? Nein! Warum? Wenn in einem Vergleich einer der Werte vom Typ "unsigned" ist, wird der andere Wert per impliziten Typecast automatisch zum unsigned-Typ. Aber eine Integervariable (z. B. von 16 Bit Länge) mit einem Wert von -1 wird im Zweierkomplement (die Art und Weise, wie negative Werte von den meisten Prozessoren behandelt werden) als 0xffff gespeichert. Dieser Wert entspricht bei einer Unsigned-Integer-Variablen 65.535. Da 65.535 größer als 1 ist, ist die Bedingung immer falsch und die Ausgabeanweisung wird nie ausgeführt. Fazit: Verwenden Sie immer explizite Typumwandlung (typ casting). Der fragliche Code lautet dann:

      ...

      if (a < (int)b)
        { printf("Gotcha!\n");

      ...

  Das gilt natürlich erst recht bei der Umwandlung von Integer nach Float oder umgekehrt.

### Programmformat

Obwohl die äußere Erscheinung des Quelltextes keine Rolle spielt, hat das Programmformat erheblichen Einfluß auf seine Verständlichkeit. Das Layout von Quellcode ist nicht nur eine Frage des persönlichen Geschmacks oder des Programmierstils. Ein vereinheitlichtes Code-Layout in einem Softwareprojekt ist eine wichtige Maßnahme zur besseren Interaktion von Teammitgliedern.

- Kommentare, die sich über mindestens eine Zeile erstrecken, sind linksbündig am Code ausgerichtet. Sie sollten beschreiben, was in den folgenden Programmzeilen bis zur nächsten Leerzeile geschieht.
  Ist ein Programmblock länger als 20 - 30 Zeilen, sollte ein Kommentar am Blockende auf den Blokanfang verweisen (z. B. `/* Ende: while (x > 0) ... */` Funktionsbeschreibungen enthalten alle Parameter, Fehlerfälle und Rückgabewert. Ggf.muss auch angegeben werden, ob ein Parameter Eingabewwert, Ausgabewert oder beides ist. Die Beschreibung erfolgt direkt nach der Definition des Funktionskopfes noch vor den Block der Funktion.
- Formatieren Sie die Anweisungen durch Einrücken so, daß man die Anweisungs- und Blockstruktur erkennen kann. Achten Sie auf Lesbarkeit. Klammern oder Schlüsselworte zur Blockdefinition sollten in einer separaten Zeile stehen. Programmblöcke müssen durch Einrückung erkennbar (2 - 3 Leerschritte, keine Tabulatorzeichen) und linksbündig ausgerichtet sein. Beispiel:

      if (wert > 10)
        {
        /* mach
             etwas
               Tolles */
        }

  Code im globalen Kontext (z. B. die Definition einer global sichtbaren Funktion) darf nicht eingerückt sein.
  Eine Sprungmarke (Label) sollte eine einfache negative Einrückung zum vorhergehenden Code aufweisen.
- Vermeiden Sie `goto`. Gerade für Schleifenkonstruktionen gibt es bessere Möglichkeiten. Statt

      LOOP:
        /* mach
             etwas
               Tolles */
      goto LOOP;

  schreiben Sie besser

      while(1)    // oder: for(;;)
        {
        /* mach
             etwas
               Tolles */
        }

  Aber: In einigen Fällen trägt `goto` zur Klarheit und Übersichtlichkeit bei. Dann nämlich, wenn sich tief in einer Bedingungs- und Schleifestruktur zeigt, dass eine Weiterbearbeitung sinnlos ist (Nenner == 0, unerwartetes Dateiende, etc.) kann man mittels `goto` an eine sinnvolle Stelle springen.
- Trennen Sie Vereinbarungen und Anweisungen durch eine Leerzeile, auch wenn das nicht vom Compiler gefordert wird. Scheuen Sie sich nicht, weitere Leerzeilen einzufügen. Funktionen sollten vom vorhergehenden Code durch 2 - 3 Leerzeilen getrennt werden. Einzelne zusammengehörende Codezeilen können durch eine Leerzeile vom vorhergehenden Code getrennt werden.
  Die maximale Zeilenbreite des Quellcodes sollte maximal 70 - 75 Anschläge umfassen. Längere Zeilen müssen umbrochen werden. Der Umbruch eines Ausdrucks sollte an der Stelle eines Operators erfolgen, der keine höhere Priorität hat als der vorhergehende oder nachfolgende. Die umbrochene Zeile kann weiter eingerückt werden.
  Die Zeilentrennung bei Auflistungen (z. B. Initialisierung eier Array-Variablen oder Parameterliste beim Funktionsaufruf) sollte nach dem Trennungszeichen erfolgen. Der Eintrag in der Folgezeile wird linksbündig nach dem ersten Eintrag ausgerichtet.
- Vermeiden Sie es, zusammengesetzte Anweisungen in eine Zeile zu schreiben. Ausdrucksanweisungen, die einen Bezug aufeinander haben, können unter Benutzung des Kommaoperaters verknüpft werden. Ausdrucksanweisungen, die keinen Bezug zueinander haben, haben Anspruch auf eigene Zeilen.
- Vermeiden Sie es, den Anweisungsteil einer Schleifen- oder Entscheidungsanweisung in dieselbe Zeile zu schreiben wie den Schleifensteuerungsausdruck. Die zwar kürzere Form ist schlechter lesbar und anfälliger gegen Fehler wie das irrtümliche Einfügen einer leeren Anweisung.
- Vergessen Sie nicht, bedingte Ausdrücke in Klammern einzuschließen. Denken Sie auch daran, daß dem `while` kein "`do`" folgt und dem `if` kein "`then`"; das Einfügen dieser Schlüsselwörter verursacht Syntaxfehler.
- Verwenden Sie bei bedingten Anweisungen nicht unreflektiert einen `else`-Zweig. Dazu ein Beispiel: Hat die Variable X den Wert 0, soll Y = 2 gesetzt werden, hat X den Wert 1, soll Y = 5 gesetzt werden. Eine erste Realisierung könnte lauten:

      if (X == 0)
        Y = 2;
      else
        Y = 5;

  Der Denkfehler in dem obigen Programmstück mag vielleicht nie auffallen, da möglicherweise X immer nur die Werte 0 und 1 annimmt. Das Programmstück ist aber nicht in der Lage, zu erkennen, ob X immer korrekt ist. Daher ist die folgende Realisierung sicherer:

      if (X == 0)
        Y = 2;
      else if (X == 1)
        Y = 5;
      else
        CallError(...);

  Wnn es sich um skalare Datentypen handelt, würde sich auch noch folgende Lösung anbieten:

      switch(X)
        {
        case 0:
          Y = 2; break;
        case 1:
          Y = 5; break;
        default:
          CallError(...);
        }

- Präprozessoranweisungen:
  Pfadangaben bei #include-Anweisungen erfolgen relativ zum Projektverzeichnis. Absolute Pfade sind verboten. Projektspezifische Include-Dateinamen stehen zwischen "...". Spitze Klammern sind für Standard-Headerfiles und projektübergreifende Dateien reserviert. Der Name einer Headerdatei darf sich nur in der Dateiendung von der Implementierungsdatei unterscheiden (z. B.: "foo.h" gehört zu "foo.c").
  Namen von Makros dürfen nur Grossbuchstaben und das Unterstreichungszeichen enthalten. Alle Makros zur bedingten Compilierung müssen im Dateikopf und der Projektdokumentation beschrieben werden.
  In einer Header-Datei müssen Deklarationen in folgender Reihenfolge stehen: Makros, Typen, globale Variable, lokale Variable (static), Prototypen lokaler Funktionen, globale Funktionen, lokale Funktionen.
- Schließlich, unterliegen Sie nicht der Versuchung vieler C-Programmierer, herauszufinden, wie man ein Programm in der knappsten möglichen Form schreiben kann. Wenn Sie unbedingt das Bedürfnis haben, tun Sie es, aber werfen Sie das Programm anschließend weg, denn es ist nicht mehr lesbar - oder nehmen Sie am Obfuscated C Contest teil.

### Ein paar Regeln zur Schreibweise

- Die Zeilen sind auf eine Länge von 120 Zeichen beschränkt.
- Alle Einrückungen werden durch mindestens zwei Leerzeichen vorgenommen.
- Vermeinden Sie Tabulatoren (die bei jeder Software anders eingestellt sind) beim Einrücken und verwenden Sie stattdessen Leerzeichen.
- Delimiter und Ausdrücke
  Delimiter von Statements (der Strichpunkt) oder Variablen (das Komma) dürfen nicht unmittelbar einem Leerzeichen folgen, müssen aber von einem Zeilenvorschub, einem Leerzeichen oder weiteren Delimitern gefolgt werden.
- Trennzeichen in zusammengesetzten Datentypen
  Trennzeichen für zusammengesetzte Datentypen (also z.B. der Punkt beim Spezifizieren der Komponente einer struct oder union in C) dürfen weder direkt vor einem Leerzeichen stehen noch direkt von einem Leerzeichen gefolgt werden.
- Leerzeichen und Zuweisungsoperator
  Der Zuweisungsoperator muss zwischen zwei Leerzeichen oder zwischen einem Leerzeichen und einem Zeilenvorschub stehen.
- Leerzeichen und unäre Operatoren
  Unäre Operatoren (z.B. der Adressoperator & oder der Inhaltsoperator *) dürfen von ihrem Operanden nicht durch ein Leerzeichen getrennt werden.
- Unäre Operatoren als Teil von Datendeklarationen
  Ein unärer Operator, der in der Bezeichnung einer Variablen vorkommen kann, darf vom Variablenbezeichner nicht getrennt werden.
- Leerzeichen und biäre Operatoren
  Biären Booleschen Operatoren (z. B. &&, ||) muss ein Leerzeichen direkt vorangehen und eines direkt nachfolgen. Die Leerzeichen um biäre Operatoren darf visuell nicht den Vorrangregeln arithmetischer Ausdrücke wiedersprechen . Ein Leerzeichen muss entweder auf beiden Seiten des Operators stehen oder auf keiner Seite.
- Empfehlung zur Klammersetzung
  Eine öffnende eckige oder runde Klammer sollte einem Leerzeichen, Zeilenvorschub, einer anderen Klammer oder einem Funktions-/Variablennamen folgen und nicht direkt von einem Leerzeichen gefolgt werden. Eine schliessende eckige oder runde Klammer sollte nicht direkt hinter einem Leerzeichen stehen und sollte direkt von einem Trennzeichen, Delimiter oder einer weiteren Klammer gefolgt werden. Vor öffnenden Klammern, welche die Parameter vom Namen einer Funktion trennen, sollte kein Leerzeichen stehen. Zwischen einem Schlüsselwort (if, while, do, etc.) und einer öffnenden Klammer sollte ein Leerzeichen stehen.
- Jede Anweisung steht in einer separaten Zeile (siehe oben)
- Die Blöcke der Anweisungen `if, else, while, do-while` oder `for` werden immer in geschweifte Klammern gesetzt, auch wenn es sich nur um eine Anweisung handelt oder wenn die Klammern einen leeren Block bilden.
- Geschweifte Klammern ({ ... }), die einen Block einschließen, werden in der gleichen Spalte in getrennten Zeilen unmittelbar vor und nach dem Block gesetzt.

### Über das Schreiben leicht lesbarer Programme

Wir haben wenig Zeit darauf verwandt, Kriterien dafür zu finden, was eine gute Funktion ausmacht. Hier folgen einige Merkmale, die eine gut geschriebene Funktion besitzen sollte:

- Zusammenhalt: Eine Funktion sollte nur eine Aufgabe ausführen und alle Anweisungen in der Funktion sollten auf diese Aufgabe bezogen sein. Wenn die Wirkungsweise der Funktion nicht in einem einzigen Satz beschrieben werden kann, versucht die Funktion, zu viel zu tun.
- Allgemeingültigkeit Eine Funktion sollte ihre eine Aufgabe gut erfüllen. Eine Sortierroutine sollte z.B. für alle Eingabemengen gut arbeiten und Fehlerfälle (wie wenn keine zu sortierenden Elemente vorhanden sind) angemessen behandeln.
- Einfachheit: Eine Funktion sollte ihre Aufgabe auf die einfachst mögliche Weise erledigen; versuchen Sie nicht, durch übertriebenes Ausfeilen des Codes noch ein oder zwei Operationen einzusparen. Im allgemeinen wird ein Wechsel des Algorithmus mehr zur Effizienz beitragen als irgendwelche Manipulationen am Code.
- Kürze: Funktionen, die eine einzelne Aufgabe in einfacher Weise erledigen, sind im allgemeinen nicht lang. Es bewährt sich, Funktionen auf ca. 25 bis 200 Zeilen) zu beschränken. Natürlich können auch zu viele kleine Funktionen ein Programm zu sehr zerreißen und damit die Lesbarkeit und die Effizienz des Programms ruinieren. Die meisten Programmierer schreiben jedoch eher zu lange als zu kurze Funktionen.
- Dokumentation: Jede Funktion hat Anspruch auf einen Kommentar, der ihm Aufgabe beschreibt. Die Parameter und lokalen Variablen der Funktion sollten ebenfalls kommentiert werden. Häufig genügt ein einziger Satz zur Beschreibung der Funktion oder Variablen. Schreiben Sie die Kommentare immer gleichzeitig mit dem Code, und warten Sie nicht, bis die Funktion fertig geschrieben ist.

### Schreiben von Makros

Makroersetzung ist die am häufigsten benutzte Fähigkeit des Preprozessors. Wir geben hier einige Richtlinien als Entscheidungshilfe, wann Makros angebracht sind, und einige stilistische Hinweise, die die Benutzung vereinfachen:

- Ersetzen Sie einfache Funktionen aus Effizienzgründen durch Makros. Achten Sie jedoch darauf, daß Sie nicht versuchen, zu viel in einem Makro zu tun. Es lohnt sich nicht, eine Funktion in ein Makro umzuschreiben, wenn der Verwaltungsaufwand des Funktionsaufrufs gering im Vergleich zu dem ist, was die Funktion ausführt.
- Ersetzen Sie, um den Code zu vereinfachen, komplizierte oder verwirrende Ausdrücke durch Makros. Durch das Makro erhält der Ausdruck einen Namen, was der Lesbarkeit dient. Dies ist besonders wichtig, wenn der Ausdruck an mehreren Stellen vorkommt.
- Schreiben Sie Makros, die in Ausdrücke und nicht in Anweisungen umgewandelt werden. Dadurch erreichen Sie, daß der Makroaufruf überall dort verwendet werden kann, wo auch ein Funktionsaufruf stehen kann. Wenn das nicht möglich ist, schließen Sie den Text des Makros in geschweifte Klammern ein, Sie dürfen dann allerdings das Makro nicht mit einem Semikolon abschließen.
- Vermeiden Sie Seiteneffekte in Makroaufrufen, unterstellen Sie nie, daß Argumente nur einmal bewertet werden, und stellen Sie keine Vermutungen darüber an, in welcher Reihenfolge Argumente bewertet werden. Diese Regeln gelten auch für Funktionen, da es manchmal nicht möglich ist, zu entscheiden, ob ein Aufruf einem Makro oder einer Funktion gilt.
- Klammern Sie Ausdrücke innerhalb eines Makroersatztextes, um sich gegen unerwartete Ergebnisse bei der Makroumwandlung zu schützen. Makrotexte, die in Ausdrücke umgewandelt werden, sollten ebenfalls geklammert werden.

### Zeiger und Arrays

Die Komplexität der Arrays, Zeiger, Arrays aus Zeigern, Zeigern auf Zeilen eines Arrays, Zeiger auf Spalten eines Arrays und Zeiger auf Zeiger können überwältigend sein. Wir wollen hier einige Anregungen geben, wie Sie den "Zeiger" in die richtige Richtung finden.

- Benutzen Sie Arrays mit Zeigern auf Zeichenketten anstatt zweidimensionaler Arrays, wenn die Zeichenketten unterschiedlich lang sind Zur Speicherzuweisung für die Zeichenketten sollten Sie `malloc` benutzen, um einiges von dem sonst bei jeder Zeichenkette vergeudeten Speicherplatz einzusparen. Merken Sie sich jedoch, daß bei der Vereinbarung eines Arrays aus Zeigern nicht automatisch Speicherplatz für das, worauf die Zeiger zeigen, zugewiesen wird.
- Fragen Sie den Rückgabewert von `malloc` immer ab. Unterlassen Sie das, können Sie sich ernsthafte Schwierigkeiten bereiten, die in den meisten Fällen zu einem Abbruch Ihres Programms durch das Betriebssystem führen, insbesondere, wenn Sie versuchen, mit einem Zeiger auf NULL Speicherplatz zuzuweisen. Es kann sein, daß Sie nichts Sinnvolles mehr tun können, wenn der verbliebene dynamische Speicher nicht mehr aus- reicht; die Entscheidung, das Programm dann abzubrechen, sollte jedoch bei Ihnen liegen. Die allgemeine Regel besagt, daß maschinennahe Routinen wie solche, die `malloc` aufrufen, dem Aufrufenden mitteilen sollten, daß es Schwierigkeiten gibt. Der aufrufende Teil sollte sich dann um den Fehler kümmern.
- Benutzen Sie `typedef`, um Vereinbarungen und Umwandlungen lesbar zu gestalten. Selbst wenn Sie in der Lage sind, komplexe Vereinbarungen zu verstehen, wird es unter den Lesern Ihres Programms sicher welche geben, die es nicht können.

### Benutzung selbstdefinierter Typen

- Benutzen Sie Aufzählungstypen, wenn eine Variable einen Wert aus einer Menge von Werten annehmen kann. Sie bieten eine bequeme Möglichkeit, Konstanten zu definieren, fördern die Lesbarkeit und ermöglichen zusätzliche Typprüfung durch den Compiler. Beachten Sie jedoch, daß Aufzählungstypen keine Ganzzahlen sind und erst in geeigneter Weise umgewandelt werden müssen, wenn sie als Ganzzahlen benutzt werden sollen.
- Benutzen Sie Strukturen, wenn Sie Daten, die in einem engen Zusammenhang stehen, speichern wollen. Strukturen bieten außerdem eine weitere Möglichkeit, Informationen zu verbergen, da sie im Ganzen verarbeitet werden können, ohne daß man sich um den Inhalt ihrer Felder kümmern muß. Benutzen Sie Varianten, wenn ein Speicherplatz verschiedene Typen aufnehmen soll.
- Beachten Sie den Unterschied zwischen Strukturen und Varianten, der darin besteht, daß jedem Glied einer Struktur Speicherplatz zugewiesen wird, bei Varianten jedoch nur ein Platz entsprechend dem längsten Feld zugewiesen wird. Sie können nur jeweils einen Wert in einem Feld einer Variante speichern.
- Unterstellen Sie nie, daß die Felder einer Struktur fortlaufend gespeichert sind. Strukturen werden oft aufgefüllt, um Ausrichtungsnotwendigkeiten zu erfüllen. Die Annahme, daß nicht aufgefüllt wird, führt zu nicht portierbarem Code.

### Dynamische Datenstrukturen

- Dynamische Datenstrukturen ersparen dem Programmierer viel Zeit. Sie haben außerdem die angenehme Eigenschaft, daß sie Speicher für beliebige Wertezusammenstellungen bereitstellen, wobei nur Speicherplatz für die tatsächlich gespeicherten Werte verbraucht wird. Schließlich sind sehr viele Algorithmen am einfachsten zu implementieren, wenn dynamisch zugewiesener Speicher zur Verfügung steht.
- Dynamische Datenstrukturen sind jedoch zur Laufzeit nicht notwendigerweise effizienter. Normalerweise müssen das Betriebssystem und die Laufzeitumgebung von C den frei zur Verfügung stehenden Speicherplatz so verwalten, daß Knoten jeder Größe in beliebiger Reihenfolge angefordert und freigegeben werden können. Bei Arrays fester Größe entlasten wir das System zur Laufzeit, müssen jedoch als Ausgleich dafür den Speicherbedarf im voraus genau angeben.
- Speicherzuweisung für dynamische Datenstrukturen kann versagen; daher müssen wir den Rückgabewert von `malloc` prüfen. Diese Funktion versagt (gibt NULL zurück), wenn im System kein Speicher mehr vorhanden ist.

### Auswahl der geeignetsten Bibliotheksfunktion

Die Standard-E/A-Bibliothek enthält verschiedene nützliche Funktionen. Unglücklicherweise sind es so viele, daß es manchmal schwierig ist zu entscheiden, welche Funktion man benutzen soll.

- Benutzen Sie nach Möglichkeit immer zeichenweise Ein- und -ausgabe. Die Funktionen `getc`, `putc`, `getchar` und`putchar` sind effizienter und einfacher zu benutzen als die anderen Bibliotheksfunktionen.
- Benutzen Sie bei Anwendungen, bei denen eine zeilenweise Verarbeitung ihrer Eingabe nahe liegt, die zeilenorientierten Funktionen `fgets`, `fputs`, `gets` und`puts`. Sie sind nicht so effizient wie die Zeichen-E/A-Funktionen, sind aber effizienter als die formatierten E/A-Funktionen `scanf, printf`. Passen Sie jedoch bei Zeilen auf, die länger als erwartet sind.
- Sparen Sie sich die formatierten Funktionen für die Fälle auf, in denen sie wirklich benötigt werden: Lesen und Schreiben formatierter Ein- und Ausgabe. Wegen des zusätzlichen Aufwandes beim Analysieren der Formatzeichenkette sind sie erheblich langsamer und erzeugen wesentlich mehr Code als alle anderen E/A-Funktionen.
- Benutzereingaben sind grundsätzlich als fehlerbehaftet und unkorrekt zu betrachten.

## 10.2 Fallen in C

### Feldgrenzen

In der Lernphase mit der Sprache C erlebt man immer wieder nicht nachvollziehbare AbstürzezurLaufzeit(memory fault - core dumped) bzw. das Verschwinden von Strings, obwohl nicht darauf zugegriffen wurde. Gibt es Hinweise, um solche Katastrophen zu vermeiden?
In der Sprache C muß der Programmierer das Überprüfen der Feldgrenzen selbst in die Hand nehmen und bei Strings auf das abschließende NULL-Byte achten. Der C-Programmierer muß selbst dafür sorgen, daß solche Kontrollen, falls notwendig, vorgenommen werden. Ein Feld `char string[100];` belegt genau 100 Zeichen (Byte)-Speicherplätze. Der String, der darin abgelegt werden kann, darf also höchstens 99 Zeichen lang sein, da per Konvention als Ende des Strings ein NULL-Byte ('\0') eingefügt werden muß. Ist der String zu lang, wird über den nachfolgenden Speicherplatz hinaus geschrieben. Das hat zur Folge, daß andere Variablen, die dort stehen, überschrieben werden. Im schlimmsten Fall ist der Speicherplatz dann nicht mehr für den Prozeß definiert, was zu obiger Fehlermeldung (memory fault - core dumped) führt. Übrigens läuft der Index in C von `0` bis `Dimension-1`.

Dumme Fehler können auch entstehen, wenn im Speicher mehrere Strings hintereinander stehen und man mit der Länge nicht aufpaßt. Schreibt man nämlich die abschließende ' \ 0 , über den ersten String hinaus, steht sie unter Umständen als erstes Zeichen im zweiten String, sodaß dieser als Leerstring erscheint, obwohl er explizit nicht belegt wurde:

    char s1[10], s2[12];

            | a | b | c | d | e | f | g | h | i | j |\0 | x | y | z |\0 |  |  |
             ^s1                                     ^s2

    printf("%s\n",s1);     -->   "abcdefghij"
    printf("%s\n",s2);     -->   ""

Natürlich hängt es sehr von der Speicherorganisation ab, wo die Variablen im einzelnen abgelegt sind (oft sind sie auf eine Wortgrenze ausgerichtet, besitzen also etwas Spielraum!). Gibt man einen solchen String an eine Funktion weiter, so ist innerhalb der Funktion nur die Startadresse bekannt (Feldname ist Anfangsadresse). Man kann dann nur mit einem weiteren Argument überprüfen, ob der Speicherbereich überschritten wird:

    int getline(char buf[], int len);    /* Deklaration (Prototyp) von getline */

    main ()
      {
      int ll;
      char string[80];

      ll = getline(string, sizeof string); /* Aufruf von getline */
      return 0;
      }

    int getline(char s[], int lim);
      {
      int i = 0;
      for(i=0; i<lim-1; i++)
        s[i] = ....;
      s[i] = '\0';
      return i;
      }

Folgende Regeln lassen sich daraus ableiten:

- Felder immer groß genug wählen
- Feldgrenzen immer abfragen
- Bei Strings auf die abschließende'\0'achten

Werden diese Hinweise beachtet, können viele Speicherabstürze vermieden werden.

### Falsch gesetzte Semikolons

- Einer der häufigen Fehler das Vergessen des Semikolons am Ende der Anweisung. Dummerweise merkt der C-Compiler das oft erst in der nächsten Zeile und weist erst dann darauf hin. Aber immerhin handelt es sich um einen Compile-Fehler.
- Auch das Vergessen des Semikolons am Ende einer Strukturdeklaration kann merkwürdige Auswirkungen haben. Wird gleich anschließend das Hauptprogramm ohne Returntyp definiert, interpretiert der Compiler die Struktur als Returntyp des Hauptprogramms.
- `for( ... ; ... ; ...) ;
  { ... }`
  beendet Schleifenrumpf, analog bei `while`
- Ein ähnliches Problem ist das Vergessen der Blockklammer in Kontrollstrukturen:

      while (i > O)
        tuwas();
        i++;

  Hier besteht die Schleife nur aus dem Kopf mit der Bedingung und dem Aufruf der Funktion `tuwas()` Der Zähler `i++` ist bereits außerhalb der Schleife - also wieder eine Endlos-Schleife!

### Falsche Einrückung

Bei der if-Kontrollstruktur ist oft nicht klar, zu welchem if ein else gehört:

    if (i > j)
      if (k < 100)
        tuwas();
    else
      tuwasanderes();

Es hat hier den Anschein, als ob das else zum ersten if gehört - das ist falsch. Ein else gehört immer zum nächst höheren if, das noch kein else hat - also zum zweiten if! Korrektur durch eine Block-Klammer:

    if (i > j)
      {
      if (k < 100)
        tuwas();
      }
    else
      tuwasanderes();

### Die switch-Falle

Eine weitere Kontrollstruktur kann Kummer machen: switch. Wird hier ein break vergessen, so arbeitet C sequentiell weiter, auch wenn andere Marken anstehen. Jede Auswahl sollte also normalerweise mit break abgeschlossen werden.

    switch (c)
      {
      case '1': tuwas(); break;
      case '2': tudies(); break;
      case '3': tujenes(); break;
      default:  printf("error\n"); break;
      }

Vielleicht kann hier der Präprozessor helfen:

    #define CASE break;case
    #define DEFAULT break;default

Dann kann die Auswahl so aussehen:

    switch (c)
      {
      CASE '1': tuwas();
      CASE '2': tudies();
      CASE '3': tujenes();
      DEFAULT:  printf("error\n");
      }

### Float-Ausdrücke

Eine weitere Falle ist folgende Rechnung:
`double d = 3/4;` Hier wird zunächst 3 durch 4 geteilt - was für Integerwerte natürlich 0 ist. Dann wird dieses Ergebnis d zugewiesen als 0.000000 (über eine implizite Typumwandlung) und bleibt somit natürlich 0. Abhilfe schafft die Verwendung der richtigen Konstanten 3.0 und 4.0 als double (zumindest eine):
`double d - 3.0/4.0;`

### Vergleichsoperator vs. Zuweisungsoperator

In Vergleichen vergißt man leicht, daß der Vergleichsoperator = = und der Zuweisungsoperator = unterschiedliche Funktionen haben. Eine (scheinbare) Bedingung `while (i = 20)` weist der Variablen i den Wert 20 zu, was logisch gesehen wahr (ungleich 0) ist, also wieder eine Endlosschleife ergibt. Natürlich kann dies (z. B. bei Strings) auch positiv genutzt werden:

    char s1(100), s2[100];

    /* s1 nach s2 kopieren (bis Stringende '\0') */
    while (s2[i] = s1[i])
      i++;

### Reihenfolge der Auswertung

- `i++ * i++` undefiniert
- Reihenfolge nur definiert bei `&&`, `||`, Komma und `?:`, dort sogar bedingte Auswertung
- Alle Argumente einer Funktion werden in unbestimmerter Reihenfolge vor dem Aufruf ausgewertet

### Eingabe-Probleme

Desweiteren gibt es oft bei der Bibliotheksfunktion `scanf` Schwierigkeiten. Vergißt man doch allzuleicht das Adreß-Zeichen & bei den Argumenten. Weniger durchschaubar ist aber, daß scanf das abschließende \n im Tastaturpuffer läßt, so daß ein nachfolgender `getchar()`-Aufruf dies als erstes geliefert bekommt. Möchte man `scanf` und `getchar` mischen, empfiehlt es sich, den Tastaturpuffer zu löschen:

    #include

    int i, c, ret;
    ret = scanf("%d",&i);         /* gepufferte Eingabe   */
    if(ret != 1) error();         /* Fehlerbehandlung     */
    while (getchar() != '\n';     /* Eingabepuffer leeren */
    getchar();                    /* Einzelzeichen lesen  */

Das Überprüfen des Return-Codes ist außerdem fast ein Muß, sofern man nicht sowieso die ganze Zeile einliest und untersucht:

    fgets(buf, MAX, stdin);
    sscanf(buf, .... );

Dann kann auch auf versehentliche Leerzeilen reagiert werden.

### Preprozessor

- Ein mit `#if` ausgeklammertem Text muß aus gültigen Preprozessortokens bestehen
- Makros können ebenfalls Probleme bereiten: `#define sqr(x) x = x*x` Hier wird ein Square-Makro definiert, der das Quadrat einer Zahl ermitteln soll.
  Dies funktioniert auch für Variablen ganz gut: `int i = 10; ... sqr(i); ...`
  Nun steht in i das Quadrat, also 100. Benutzt man dieses Makro aber für einen Wert (`sqr(10);`), so macht die Zuweisung Probleme. Also sollte man die Zuweisung im Makro unterlassen: `#define sqr(x) x*x`
- `i = sqr(10);` funktioniert nun. Ruft man dieses Makro nun aber mit einem Ausdruck auf, gibt es wieder Probleme: `i = sqr(2 + 8);` wird zu `2 + 8 * 2 + 8` ersetzt und da Punktrechnung vor Strichrechnung geht, ist das Ergebnis `2 + 16 + 8 = 26`, also falsch. Die zweite Regel sollte also lauten, Klammern zu verwenden:

      #define sqr(x) ((x)*(x))

  Doch damit ist beispielsweise `sqr(++i);` noch nicht gelöst, da nun ++i zweimal ausgeführt wird, und zwar ++i * ++i. Dieses Problem ist mit Makros nicht zu lösen. Also keine Zuweisungen oder andere Seiteneffekte in Makroaufrufen!
- In den Wahnsinn können den Programmierer Fehler bei Makrodefinitionen treiben, denn diese werden an der Stelle im Code gemeldet, wo das Makro expandiert wird und nicht etwa in der `#define`-Zeile. Denken Sie immer daran, dass eine Makrodefinition eigentlich nur eine Art Textersetztungsregel ist. `#define MAX 42` heisst ja für den Compiler: Ersetze überall die Zeichenfolge "MAX" durch die Zeichenfolge"42". Wenn Sie nun im Zustand geistiger Umnachtung bei der Definition `#define MAX 42;` (gewohnheitsmäßig Strichpunkt am Ende) schreiben, führt die Anweisung `Data[MAX] = 3.14;` zu einer scheinbar unverständlichen Fehlermeldung (Makroexpansion ergibt: `Data[42;] = 3.14;`).

### Konstante und Variable

- Bei `const char *a` und `char *b` ist `a=b` ok (Erlaubnis zum Ändern wird nicht übernommen), `b=a` ist dagegen falsch (Erlaubnis zum Ändern wird fälschlicherweise gegeben)
- Bei `const int a` und `int b` ist `a=b` falsch (Unerlaubte Änderung), `b=a` ist erlaubt (Verarbeitung nur einer Kopie)

### (Null-)Pointer

- `NULL` ist nur `#define NULL 0` - Automatische Typumwandlung
- Pointer sind keine Integer-Werte.
- Ein Zeiger muß immer mit einer gültigen Speicherplatzadresse initialisiert sein.

### Arrays und Pointer

- In einer Datei `char a[5]`, in der anderen `extern char *a` ist falsch, `extern char a[]` ist richtig
- Äquivalenz der Deklarationen `char a[]` und `char *a` nur bei formalen Argumenten von Funktionen
- `char b[][]` und `char **b` nicht äquivalent, letzteres verwendet real im Speicher vorhandene Pointer
- Aufzeichnen: Realer Speicher bei `char **a`, `char *a[]`, `char (*a)[]`, `char a[][]`
- Dynamische Allozierung mehrdimensionaler Felder: Am besten Allozieren eines Feldes mit Pointern und Allozieren jeder einzelnen Zeile

### Dynamische Speicherverwaltung

- `char *s; gets(s);` ist falsch, da kein Speicher bereitsteht
- `char *s="Hal",*t="lo!",*u=strcat(*s,*t)` ist genauso falsch
- Mit `free()` freigegebener Speicher darf nicht mehr angesprochen werden

### Fehlermeldungen beim Compilieren

Sie starten den Compiler und es erscheint eine Unmenge von Fehlermeldungen. Nicht verzweifeln, denn oft benötigt man für die Behebung der Fehler nur wenige Handgriffe. Achten Sie auf:

- fehlende Include-Dateien (wenn vielfach die Meldung kommt, dass Funktionen vor ihrer Verwendung nicht definiert wurden, legt es vermutlich daran),
- fehlende Strichpunkte,
- fehlende Kommas und
- fehlende öffnende oder schliessende geschweifte Klammern.

Oft liegt der eigentliche Fehler auch einige Zeilen vor derjenigen, die in der Fehlermeldung steht. 90% aller Syntaxfehler beruhen auf schludriger Eingabe des Codes (nicht umsonst sind die Tools so beliebt, bei denen man sich fertigen Code zusammenklicken kann).

## 10.3 Modularisierung und Makefiles

### Compiler und Linker

Wie von Anfang an bekannt, kann unser C-Compiler nicht nur den Quellcode übersetzen, sondern auch einzelne Binärobjekte zu einen aausführbaren Programm zusammenbinden (linken). Mittels `gcc -c foo.c` erzeugt man ein Object-File `foo.o` und mittels `gcc -o foo foo.o` kann daraus ein ausführbares Programm erzeugt werden. Das Ganze funktioniert auch mit mehreren Quell- und Object-Dateien, z. B.:

    gcc -c foo.c            Erzeugen der Object-Dateien (*.o)
    gcc -c bar.c
    gcc -c test.c
    gcc -o go *.o           Erzeugen des Executables (go)

Die Option "-c" weist den gcc an, nur zu kompilieren und die Option "-o" sorgt für das Linken. Der gcc hat noch ein paar wichtige Optionen zu bieten:

    -c         nur übersetzen, nicht linken
    -Idir      Include-Dateien in dir suchen
    -Wall      alle Warnungen aktivieren
    -g         Debugging-Symbole erzeugen
    -o file    fertiges Programm in file schreiben
    -Olevel    Optimierungen einschalten, z. B. -O2
    -foption   generelle Compiler-Optionen, z. B. -ffast-math
    -llib      Bibliothek linken, z. B. bindet "-lm" die libm.o ein
    -Ldir      Bibliotheken in dir suchen

Daneben gibt es noch zahlreiche andere Optionen. Typische Kommandozeilen wären:

    gcc -Wall -o binary quelle.c            # nur eine Quelldatei, erzeugt ausführbares "binary"
    gcc -Wall -o binary main.c quelle.c     # dito mit zwei Quelldateien (Prototypdefinition
                                            # per Headerdatei quelle.h in main.c
    gcc -Wall -o binary -lm quelle.c        # dito mit Einbinden der (vocompilierten) Mathe-Library

Wenn es komplexer wird, hilft das Make-Paket weiter:

### Make

Unter den C-Quellen (Dateien mit den Endungen ".C2 bzw. ".h") ergibt sich bei einem Projekt ein System von Abhängigkeiten. Wenn bestimmte Quell-Dateien modifiziert wurden, müssen einige, aber in der Regel nicht alle, Module neu erstellt werden. Bei mehreren tausend Dateien (wie z. B. beim Linux-Kernel) wäre es aber unsinnig bei jeder kleinen Korrektur alle Dateien neu zu übersetzen. Ebenso mühsam ist, manuell diejenigen Dateien herauszufinden, die sich geändert haben. Man braucht also ein Werkzeug, das diese Abhängigkeiten erkennt und jeweils nur die nötigen Module neu übersetzt.

Das Make-Utility leistet das Gewünschte. Dem Make-Utility muss ein sogenanntes Makefile (Datei-Name: "Makefile", diese Schreibweise ist wichtig) bereit gestellt werden, das die Abhängigkeiten eines Projekts beschreibt: Module, Versionen (Debug-Version, Release-Version) sowie andere Informationen. Make erkennt anhand der Modifikations-Zeit der Dateien was sich geändert hat. Das Makefile beschreibt die Abhängigkeiten mittels "Targets" (engl. Ziele). Das Makefile bestehen aus einer Menge von Regeln zur Steuerung der Übersetzung. Sein Aufbau ist:

    Ziel: Quelle Quelle ...
          Shellkommando
          Shellkommando
          ...

Das Ziel gibt meist an, welche Datei erzeugt wird. Falls eine der Quellen neuer ist als das Ziel, wird das Ziel aktualisiert. Aktualität und Vorhandensein der Quellen werden vorher rekursiv sichergestellt. Die Shellkommandos (durch Tabulatorzeichen eingeleitet!) erzeugen das Ziel aus den Quellen. Dazu ein Beispiel:

    go: bar.o foo.o test.o
        gcc bar.o foo.o test.o -lm -lglib -o go
    bar.o: bar.c
        gcc -Wall -O2 -c bar.c
    foo.o: foo.c
        gcc -Wall -O2 -c foo.c
    test.o: test.c
        gcc -Wall -O2 -c test.c
    clean:
        rm -rf *.o

Ein Eintrag eines Makefiles sieht formal folgendermaßen aus:

    Target_Name: <Dateien oder Sub-Targets von den dieses abhängt>
       <Tab-Zeichen>   <Regel zum erstellen dieses Targets>

Eine gemeine Falle für Anfänger ist, daß die zweite Zeile mit einem `<tab>` anfangen muß, und nicht mit Leerzeichen.

Beim Aufruf von make kann angegeben werden, welches Target man erstellen möchte. Make erzeugt dann alle hierfür nötigen Subtargets. Wird make ohne Parameter gestartet, wird das erste (oberste) Target aus dem Makefile erstellt. Im Beispiel oben werden dann `foo.o, bar.o, test.o` und `go` erzeugt.

Es gibt auch Targets, die keine Abhängigkeiten haben. Im obigen Makefile haben wir ein zusätzliches Target "clean", das dazu da ist, alle o-Files wieder sauber zu löschen. Somit kann man mit dem Kommando `make clean` wieder aufräumen.

Viele targets können nicht (wie oben) durch einen einzigen Befehl erzeugt werden, sondern benötigen mehrere Kommandos. In diesem Fall folgen auf die Zeile mit den Abhängigkeiten einfach mehrere Zeilen, die alle mit `<tab>` beginnen. Auch die Abhängigkeiten für ein target dürfen auf mehrere Zeilen verteilt sein.

Im Beispiel oben ist "`go`" gleichzeitig ein Target-Name und ein Datei-Name. make sieht darin keinen Unterschied. Auch die beiden Dateien `foo.c` und `bar.c` sind für make nichts anderes als Targets. Diese hängen von keinen anderen Targets ab, sind also immer aktuell, und es gibt auch keine Regel, um sie zu erzeugen. Würde nun beispielsweise die Datei `bar.c` nicht existieren, würde make feststellen, daß es das target `bar.c`, welches für `bar.o` benötigt wird, nicht erzeugen kann. Die Fehlermeldung lautet dementsprechend:

       make: *** No rule to make target `bar.c'. Stop.

make kennt noch viele weitere Möglichkeiten, von denen hier nur einige besprochen werden.

Es ist möglich, in Makefiles Variablen (eigentlich sind es Makros) zu definieren und zu benutzen. Normalerweise verwendet man Großbuchstaben. Gebräuchlich sind beispielsweise folgende Variablen:

    CC        der Compiler
    CFLAGS    Compiler-Optionen
    LDFLAGS   Linker-Optionen

Auf den Inhalt dieser Variablen greift man dann mit `$(CC)`, `$(CFLAGS)` bzw. `$(LDFLAGS)` zu. Ein einfaches Makefile eines Programmes namens `go`, welches aus einer Reihe von Objekt-Dateien zusammengelinkt werden soll, könnte also so aussehen:

    VERSION = 3.02
    CC      = /usr/bin/gcc
    CFLAGS  = -Wall -g -DVERSION=\"$(VERSION)\"
    LDFLAGS = -lm -lglib

    OBJ = datei1.o datei2.o datei3.o datei4.o datei5.o

    all: $(OBJ)
            $(CC) $(CFLAGS) -o go $(OBJ) $(LDFLAGS)

    %.o: %.c
            $(CC) $(CFLAGS) -c $<

Das %-Zeichen ist hier der Platzhalter für Regelmengen. Das Defaulttarget ist hier `all`, das erzeugte ausführbare Programm `go`. Dieses hängt von allen Objekt-Dateien ab. Beim Linken werden zwei Libraries dazugelinkt. An diesem Beispiel sieht man, dass eine Shell die Befehle ausführt: Ohne die Backslashes in `\"$(VERSION)\"` würde diese nämlich die Anführungzeichen entfernen. Die Versionsnummer soll dem C-Präprozessor aber als konstante Zeichenkette übergeben werden. Interessant ist die letzte Zeile, wo der Compiler angewiesen wird, eine Quelle namens `$<` zu übersetzen. Bei `$<` handelt es sich um eines der sogenannten automatischen Makros, deren es unter anderem folgende gibt:

    $@     Ziel
    $<     erste Quelle
    $ˆ     alle Quellen
    $?     Quellen, die neuer sind als das Ziel

Dazu ein Beispiel:

    go:   $(OBJECTS)
    $(CC) $ˆ -lm -lglib -o $@
    %.o: %.c
    $(CC) $(CFLAGS) -c $<

### Module

Module sind die einzige höhere Abstraktion, die C bietet, man sollte also reichlich Gebrauch davon machen. Wie zerlegt man nun einm Programm in Module? Betrachten Sie dazu das folgende einfache Beispiel:

    #include <stdio.h>

    /* Funktions-Prototypen */
    int foo(int a, int b);
    void bar(int a, int *result);

    int main(void)
      {
      int X = 2,Y = 3, res;
      res = foo(X, Y);
      printf("%d\n", res);
      bar(X, &res);
      printf("%d\n", res);
      return(0);
      }

    int foo(int a, int b)
      {
      return a + b;
      }

    void bar(int a, int *result)
      {
      *result = a;
      }

Es sollen nun die Funktionen `foo()` und `bar()` in ein Modul ausgelagert werden. Es ergibt sich erstens ein Programm-Testmodul `test.c`:

    /* Testmodul */
    #include <stdio.h>

    /* Funktions-Prototypen */
    int foo(int a, int b);
    void bar(int a, int *result);

    int main(void)
      {
      int X = 2,Y = 3, res;
      res = foo(X, Y);
      printf("%d\n", res);
      bar(X, &res);
      printf("%d\n", res);
      return(0);
      }

Das Zweite ist ein Funktionsmodul `modul.c`:

    int foo(int a, int b)
      {
      printf("I am foo\n");
      return a + b;
      }

    void bar(int a, int *result)
      {
      printf("I am bar\n");
      *result = a;
      }

Das ist schon ganz nett und mittels `make` kann man `modul.c` und `test.c` übersetzen (ergibt `modul.o` und `test.o`) und zusammenlinken. Das Modul `test.c` muss auf jeden Fall den Prototyp von `foo()` und `bar()` kennen. Man könnte nun versucht sein, diese manuell vor das `main()` zu schreiben. Dies ist aber nicht ratsam, denn man müsste alle Prototyp-Definitionen überall eintragen bzw. ändern, wenn irgendwo `modul` verwendet respektive geändert würde.

Besser ist es, die Prototypen in eine separate Datei zu schreiben, die man `modul.h` nennt (das "h" steht für "Header-Datei"). Für das Beispiel oben sieht die Header-Datei `mdul.h` so aus:

    /* Funktions-Prototypen */
    int foo(int a, int b);
    void bar(int a, int *result);

Bei jeder Verwendung von `module` muss man jetzt nur noch dafür sorgen, dass der Inhalt des Header-Files am Anfang hinein kopiert wird, was sich mittels `#include "modul.h"` erledigen lässt. Beachten Sie die Gänsefüßchen! Für das `#include`-Makro gilt: Dateien zwischen < > werden in den vordefinierten Header-Verzeichnissen gesucht, Dateien zwischen " " werden im aktuellen Verzeichnis (realtiv zur C-Quelle) gesucht. Unser Testmodul sieht nun so aus:

    /* Testmodul */
    #include <stdio.h>
    #include "modul.h"

    int main(void)
      {
      int X = 2,Y = 3, res;
      res = foo(X, Y);
      printf("%d\n", res);
      bar(X, &res);
      printf("%d\n", res);
      return(0);
      }

Es ist übrigens sinnvoll, auch in der Datei `modul.c` zu Beginn das Header-File `modul.h` per `#include` einzubinden (schon, damit der Compiler warnt, falls die Prototypen und die Funktionen sich auseinander entwickeln).

Bei größeren Systemen kann es durchaus vorkommen, dass einzelne Module die Funktionen anderer Module verwenden und daher deren Header-Dateien includieren. So kann es recht schnell zum mehrfachen Einbinden der Header-Dateien (mit entsprechen seltsamen Verhaltensweisen von Programm und Compiler) kommen. Mit den Präprozessor-Konstrukten `#define` und `#ifndef` kann man gewährleisten, dass eine Header-Datei jeweils nur beim ersten Mal effektiv includiert wird. Damit sieht unser Beispiel-Headerfile folgendermaßen aus:

    #ifndef _MODUL_H_
    #define _MODUL_H_
    /* Funktions-Prototypen */
    int foo(int a, int b);
    void bar(int a, int *result);
    #endif
    /* _MODUL_H_ */

Nur wenn `_MODUL_H_` noch nicht definiert worden ist, wird der Quellcode zwischen `#ifndef` und `#endif` eingefügt. Zugleich wird auch `_MODUL_H_` definiert. Hiermit wird sichergestellt, dass die `#ifndef`-Bedingung bei zukünftigen Includes nicht mehr erfüllt ist.

Wir folgen der C/C++-Konvention, indem wir für die jeweiligen Symbole den Datei-Namen der Header-Datei verwenden und jeweils ein Unterline-Zeichen vorne und hinten anfügen. Ebenso wird der Punkt im Dateinamen durch das Underline-Zeichen ersetzt. Wenn Sie sich für jede Header-Datei an diese Konvention halten, kann nichts mehr schief gehen. Tun Sie es auch, wenn Sie der festen Überzeugung sind, dass keine Mehrfach-Inkludierung vorkommt. Beim `#endif` geben Sie als Kommentar die Bezeichnung zum zugehörigen Symbol der `#ifndef`-Direktive an. So wissen wir immer, zu welchem `#ifdef` ein `#endif` gehört.

Das Makefile zum Beispiel muss natürlich nun auch das Header-File berücksichtigen:

    go: modul.o test.o
        gcc modul.o test.o -lm -lglib -o go
    modul.o: modul.c modul.h
        gcc -Wall -O2 -c modul.c
    test.o: test.c
        gcc -Wall -O2 -c test.c
    clean:
        rm -rf *.o

Zum Inhaltsverzeichnis | Zum nächsten Abschnitt

###### Copyright © FH München, FB 04, Prof. Jürgen Plate

###### Letzte Aktualisierung: 30. Mär 2013