Programmieren in C

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

# 2 Programmstrukturen

Die Sprache C war ursprünglich ausschließlich gedacht zum Implementieren und Erweitern des Betriebssystems UNIX, also eher weniger als Standard-Programmiersprache mit großer Verbreitung. Ursprünglich, wohlgemerkt! Davon zeugen immer noch deren Eigenschaften wie

- Reichtum an Operatoren,
- relative Maschinennähe,
- mögliche hohe Portabilität,
- kleiner Sprachumfang (nur 32 Schlüsselwörter),
- viele syntaktische Möglichkeiten in Kombination mit vereinfachten Schreibweisen.

Speziell der letzte Punkt führt zu einer hohen Toleranz des Compilers gegenüber Leichtsinnsfehlern. Diesbezüglich ist C auch ideal, wenn es darum geht, kryptische Programme zu schreiben (und sich selbst damit ein Bein zu stellen).
Spätestens seit Festsetzung des ANSI-Standards hat sich die praktische Verwendbarkeit wesentlich verbessert; durch Einsatz von Prototypen ist die Typsicherheit deutlich erhöht. Was schließlich die kryptischen Programme angeht, sagen die Entwickler selbst:

"C retains the basic philosophy that programmers know what they are doing".

Genereller Charakter: Der sehr kleine Sprachumfang ist fast immer ungenügend (z.B. keine I/O-Möglichkeit), deshalb finden Routinen aus definierten Standard-Bibliotheken Anwendung.

In einigen Fällen werden zur Beschreibung der Syntax von C sogenannte Syntaxdiagramme verwendet. Dies sind grafische Darstellungen der Abfolge von Elementen der Sprache C. Sie können mit den Diagrammen sozusagen "mit dem Zeigefinger" nachprüfen, ob Ihre Programmsyntax mit der Sprachdefinition übereinstimmt. Jedes Diagramm stellt eine Produktionsregel dar, was nichts weiter bedeutet als eine erlaubte Folge von Symbolen. Dabei gibt es zwei Formen von Symbolen:

- Metasprachliche Variable, d. h. ein Verweis auf eine andere Produktionsregel. Solche Symbole werden als Rechteck dargestellt.
- Terminalsymbole. Diese Symbole stehen für sich selbst.

Ein Programm ist dann syntaktisch richtig, wenn es durch eine Folge von Terminalsymbolen dargestellt werden kann (wobei die Meta-Variablen nach und nach durch Terminalsymbole aufgelöst werden).

#### Übersetzen und Linken eines C-Programms

## 2.1 Grundelemente eines C-Programms

In diesem Kapitel werden einige syntaktische Grundelemente der Programmiersprache C behandelt. Wie schon im Vorwort gesagt, wird schon sehr bald auf viele Sprachelemente von C zurückgegriffen, ohne daß diese in der linearen Folge des Skripts bereits behandelt wurden. In der Vorlesungen werden die Sprachkonstrukte dann jeweils angesprochen, bei häuslichen Studium wird Ihnen aber ein Blick an die entsprechenden Referenzstellen nicht erspart bleiben. Wenden wir uns nun einigen Grundlagen zu:

#### Zeichensatz von C-Programmen

Jedes C-Programm besteht aus sichtbaren und aus weiteren Zeichen eines bestimmten Zeichensatzes. Die folgenden sichtbaren Zeichen sind in C zulässig:

Form | Zeichen
--- | ---
Buchstaben | A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k 1 m n o p q r s t u v w x y z
Ziffern | 0 1 2 3 4 5 6 7 8 9
Unterstrich | _
Sonderzeichen | ! " # % & ' ( ) * + , - . / : ; < = > ? [ \ ] ^ { | } ~

Neben den sichtbaren Zeichen verfügt C noch über weitere Zeichen:

Zeichen | Bedeutung
--- | ---
Leertaste | Space, Leerzeichen
Warnung | BEL, Klingel, Signalton
Backspace | BS, Rückschritt
Formfeed | FF, Sprung zum nächsten Seitenanfang
Newline | NL, Zeilenende, "Line Feed", Zeilenvorschub
Return | CR, "Carriage Return", Sprung zum Anfang der aktuellen Zeile
Tab | HT, Horizontaler Tabulator

Sie sehen, daß die nationalen Sonderzeichen, wie z. B. Umlaute, nicht zum Zeichensatz von C gehören. Diese können jedoch in Literalen (Strings) auftauchen. Die oben vorgestellten weiteren Zeichen des Zeichensatzes in C werden durch Escape-Sequenzen realisiert. Diese Technik wird weiter unten vorgestellt.

Namen bestehen in C aus Buchstaben, Ziffern und dem Unterstrich. Sie dürfen nicht mit einer Ziffer beginnen.

#### Trennzeichen

Das Leerzeichen, Zeilenende, die Tabulatoren, Seitenvorschub und Kommentare sind Trennzeichen. Sie trennen die Grundelemente der Sprache. Mehrere dieser Trennzeichen hintereinander werden als ein Trennzeichen angesehen. Folgt einem Backslash (\) ein Zeilenendezeichen, dann wird eine logische Zeile gebildet:

    Das \
    ist eine logische Zeile

#### Formatfreiheit

Der Programmtext wird in C ohne Zeilennummern und formatfrei eingegeben. Trotz dieser Formatfreiheit sollten Sie Ihre Programme so gestalten, daß sie von Menschen gelesen und verstanden werden können. Aus den vorgestellten Programmen ist eine gewisse, vom Autor frei gewählte, Konvention erkennbar. Andere Autoren haben vergleichbare Gewohnheiten, entscheiden Sie sich für eine.

#### Kommentare

Die Informationen zur Programmdokumentation werden in einem Kommentar untergebracht. Dieser wird mit /* eingeleitet und mit */ beendet. Ein Kommentar kann über mehrere Zeilen gehen.
Diese Möglichkeit ist nicht nur zum Kommentieren wichtig, sondern auch zum Einkreisen von Fehlern. Verdächtiger Kode mit /* und */ eingekreist und der Fehler möglicherweise umzingelt.
Kommentare dürfen nicht geschachtelt werden. Diese Einschränkung ist dann unangenehm, wenn Sie Programmtext "auskommentieren", d. h. außer Kraft setzen wollen. Sie müssen dann nämlich darauf achten, daß Sie nicht zufällig Kommentare mit einschließen. Daher lassen manche Compilerhersteller auch verschachtelte Kommentare zu.
Es gibt Compiler, die // als Kommentarbeginn zulassen, Kommentarende ist dann das Ende der Zeile.

#### Compilerdirektive `#include`

Durch diese Anweisung an den Compiler werden weitere Dateien in den Quelltext eingebunden. Es handelt sich in der Regel um sogenannte "Headerdateien" (siehe unten). Beispiel:

    #include <stdio.h>

Durch diese Compilerdirektive wird der Inhalt der Datei `stdio.h` dem Quelltext hinzugefügt. Die Lage der Headerdateien im Dateisystem des Rechners ist über Compiler-Voreinstellungen festgelegt. Meist heißt das Verzeichnis auch "include". Eine Weitere Form der include-Anweisung ist:

    #include "myfile.h"

Hier wird normalerweise in aktuellen Verzeichnis gesucht. Man kann bei der Headerdatei auch einen Dateipfad angeben.

#### Headerdateien

Die Datei stdio.h ist durch die Endung ".h" als Headerdatei erkennbar. Headerdateien werden am Kopf (englisch head = Kopf) des Quelltextes eingefügt. Die Headerdatei mit dem Namen stdio.h wird somit vom Compiler eingelesen. Hier stehen wichtige Informationen über den Aufbau von Funktionen, die im Programmlauf aufgerufen werden. Diese Headerdateien sind reine Textdateien, Sie können den Inhalt mit einem Editor untersuchen.
In den Headerdateien findet der Compiler Prototypen, Präprozessor-Makros und symbolische Konstanten. Prototypen sind Muster für die Verwendung von Funktionen. Der Präprozessor wird vor dem eigentlichen Compilationsvorgang gestartet, er ersetzt Makros im Text durch einen anderen Text. Weiterhin werden häufig verwendete Konstanten, wie z. B. EOF, definiert. Die Kodierung für die angesprochenen Funktionen befindet sich in Bibliotheken, die beim Linken dem compilierten Programm hinzugebunden werden. Durch die Headerdateien wird der Compiler beruhigt: Es fehlt nichts, die weiteren Informationen folgen beim Linken (= Zusammenbinden mehrerer binärer Objektdateien zu einem Programm).

#### Das Semikolon

In C muß jede Anweisung mit einem Semikolon (;) abgeschlossen werden. In Pascal, zum Vergleich, trennt das Semikolon zwei Anweisungen.

#### Funktionen

Es gibt in C kein besonderes Hauptprogramm (wie in Pascal oder anderen Sprachen). Für jedes C-Programm ist `main()` die Hauptfunktion. Hier beginnt der prozedurale Teil. Da `main()` eine Funktion ist, können Argumente übergeben werden. Weiterhin kann dem aufrufenden Programm ein Ergebnis geliefert werden. In diesem Fall wird nichts übergeben und nichts zurückgeliefert. Das Nichts kann in C auch void genannt werden. Innerhalb der Funktion `main()` befindet sich der Anweisungsblock, der mit "{" eingeleitet und mit "}" beendet wird. Das ist ein allgemeines Prinzip:

Anweisungsblöcke werden durch geschweifte Klammern zusammengefaßt.

`main()` wird auch im Programm nur definiert, aber nicht aufgerufen. Sie werden niemals einen Befehl `main();` in einem C-Programm finden. `main()` hat eine Sonderstellung: Es handelt sich um die Funktion, die beim Programmstart auf jeden Fall ausgeführt wird.

Die Funktion `main()` liefert bei einem erfolgreichen Programmlauf den Wert Null als Ergebnis. Manche Compiler verlangen, daß am Ende von `main()` ausdrücklich ein Rückgabewert angegeben wird. Ergänzen Sie dann das Programm nach der letzten Anweisung, also vor der letzten geschweiften Klammer, um die Anweisung:

`return (0)`

oder

`return 0`

Mit dieser Angabe kann das Betriebssystem, das das Programm gestartet hat, überprüfen, ob das aufgerufene Programm ordnungsgemäß beendet worden ist. Im Fehlerfall wird ein Wert ungleich Null zurückgegeben.

#### Standardbibliotheken

Die Standardfunktionen werden von Standardbibliotheken zur Verfügung gestellt. Die folgenden Headerdateien stellen die Verbindung zu den Standardbibliotheken her, wobei die folgende Tabelle nur ein Auszug aus allen verfügbaren Headerdateien ist:

Bibliothek | Aufgabenbereiche
--- | ---
assert.h | Überprüfung von Bedingungen
ctype.h | Typkonvertierungen und Typtests
errno.h | Behandlung von Systemfehlern
float.h | Fließkomma-Bibliothek
limits.h | Grenzen für Datentypen
locale.h | Verwaltung der lokalen Struktur
math.h | mathematische Funktionen
signal.h | Prozeßsteuerung
stddef.h | Standardkonstanten
stdio.h | Standardeingabe und -ausgabe
stdlib.h | Standardbibliotheksfunktionen
string.h | Funktionen zur Stringverarbeitung
time.h | Zeitmanagement

Beim Programmieren in C verwenden Sie entweder die oben angebotenen Standardfunktionen oder selbstdefinierte Funktionen oder Sie beziehen vollständige Funktionsbibliotheken für alle nur denkbaren Anwendungsgebiete. Damit ist C so leistungsfähig, daß sowohl Betriebssysteme als auch beliebige Anwendungen programmiert werden können. Eine Beschreibung der Bibliotheken befindet sich im Anhang.

#### Schlüsselwörter

Die Schlüsselwörter einer Programmiersprache haben eine vordefinierte Bedeutung, diese kann nicht geändert werden. Hier eine _bersicht:

auto | double | int | struct
--- | --- | --- | ---
break | else | long | switch
case | enum | register | typedef
char | extern | return | union
const | float | short | unsigned
continue | for | signed | void
default | goto | sizeof | volatile
do | if | static | while

Für C ist der Unterschied zwischen Groß- und Kleinschreibung wichtig.

Die Schlüsselwörter müssen also genauso geschrieben werden, wie sie vorgestellt worden sind. Diese Schlüsselwörter bilden den Kern der Programmiersprache C.

#### Bezeichner und Namen

In C (nicht in C++) ist ein Objekt ein Speicherbereich, der aus einer zusammenhängenden Folge von einem oder mehreren Bytes bestehen muß. Mit Bezeichnern werden Objekte identifiziert. Dabei gelten die folgenden Regeln:

- Ein Bezeichner besteht aus einer Folge von Buchstaben, Ziffern oder dem Unterstrich (_), das erste Zeichen darf keine Ziffer sein.
- Ein Bezeichner darf beliebig lang sein, es ist jedoch nur eine bestimmte Länge signifikant.
- Schlüsselwörter dürfen nicht als Bezeichner genommen werden.

Auch hier gilt, daß zwischen Groß- und Kleinschreibung unterschieden wird. Namen sind Bezeichner von Variablen, Funktionen und Marken.

#### Escape-Sequenzen

In der Zeile

`printf("Hello World!\n");`

finden Sie die Escape-Sequenz \n, die den Cursor zum Anfang der nächsten Zeile bewegt (bzw. auf dem Drucker eine neue Zeile beginnt). Nichtdruckbare Zeichen werden über solche Escape-Sequenzen, eingeleitet durch "\" in den Programmtext eingefügt (meist, für die Ein- und Ausgabe oder in Strings). Hier eine Übersicht über einige Escape-Sequenzen:

Zeichen | Escape-Sequenz | Bedeutung
--- | --- | ---
" | \" | Anführungszeichen
' | \' | Apostroph
? | \? | Fragezeichen
\ | \\ | Backslash
BEL | \a | Bell
BS | \b | Backspace
FF | \f | Formfeed
NL | \n | Newline
CR | \r | Carriage Return
HT | \t | Horizontal-Tabulator
VT | \v | Vertical-Tabulator

### Einführendes Beispiel

Dies ist wohl eines der bekanntesten C-Programme:

    #include <stdio.h>      /* header file  '... .h' */
    int main(void)                /* Hauptprogramm 'main()' */
      {                           /* Beginn ... */
      printf("Hello world!\n");   /* schreibt "..." auf stdout */

       return 0;                  /* alles okay ... */
      }                           /* ... Ende Block */

Anmerkungen:

- C unterscheidet zwischen Groß- und Kleinbuchstaben (case sensitiv);
- Kommentare (bzw. allg. Zeichen, die der Compiler ignorieren soll) werden in `/* */` eingeschlossen; dies kann auch über Zeilen hinweg geschehen.
  Vorsicht: Kommentare dürfen nicht geschachtelt werden!
- Programm besteht aus Ausdrücken, die durch '`;`' terminiert sind (sog. "Statements" oder "Anweisungen");

    #include <stdio.h>

    int main(void)
      {
      int n, m;                             /* Deklaration der Variablen
                                             'n' und 'm' als integer */
      printf("2 ganze Zahlen eingeben: ");
      scanf("%d%d", &n, &m);        /* Werte einlesen von stdin */
      printf("\nSumme von %d und %d: %d\n",
                                n, m, n+m); /*Ergebnis*/
      return 0;
      }

Anmerkungen:

- Das Hauptprogramm heißt immer `main()`; formal ist das eine Funktion, deshalb `int main(void)` oder `int main(int argc, char **argv)`;
- `main()` sollte immer einen Wert zurückgeben;
- Am Beginn eines Blockes (Block: spezifiziert durch '{' und '}') müssen die hierin verwendeten Variablen deklariert werden (soweit dies nicht schon außerhalb geschehen ist); dies geschieht jeweils durch Angabe des Datentyps, gefolgt vom Namen der Variablen.
- Es ist üblich, Klammerebenen durch Einrücken zu verdeutlichen.
- Die Funktion `printf` dient der Ausgabe von Text und Inhalten von Variablen auf den Bildschirm (`stdout`); dazu ist die Angabe des gewünschten Formats nötig; die Formatangabe hat die Form `%`format.
- Zum Einlesen von Werten wird die Funktion `scanf` verwendet; die obigen Formatangaben sind im Wesentlichen die gleichen wie bei `printf`. Außerdem muß dem Variablennamen in der Parameterliste von `scanf` ein '`&`' voranstehen (tatsächlich werden nämlich die Adressen der Variablen übergeben).

Damit haben Sie auch schon eine wichtige Kontrollstruktur kennengelernt. Als Block wird bezeichnet, was in einem Klammerpaar `{ }` eingeschlossen ist. Ein solcher Block faßt die in ihm enthaltenen Statements zu einem sog. "Compound-Statement" zusammen; dies ist syntaktisch äquivalent zu einem einzelnen Statement. Blöcke verfügen über bemerkenswerte Eigenständigkeit: innerhalb jedes Blockes können lokale Variablen deklariert werden. Beispiel:

    int i=3;
      {
      int i=4;
      printf("%d", i);      /* output: 4 */
      }
    printf("%d", i);        /* output: 3 */

Noch ein Beispiel: Vorstellung einfacher Datentypen und ihrer Ausgabe mit printf(); Verwendung der Zuweisung und der arithmetischen Operatoren +, -, *, / und %.

    #include <stdio.h>
    #include <stdlib.h>

    int main(void)
    {
      /* Beachten Sie: Innerhalb eines Paares geschweifter Klammern  */
      /* immer zuerst alle benoetigten lokalen Variablen deklarieren */
      /* und dann die Anweisungen schreiben.                         */

      /* Deklaration der (lokalen) Variablen */
      int         i,ii;  /* printf-Format: %d  */
      short int   h,hh;  /* printf-Format: %hd */
      long int    l,ll;  /* printf-Format: %ld */
      char        c,cc;  /* printf-Format: %c  */
      float       f,ff;  /* printf-Format: %f  | %e  | %g  */
      double      d,dd;  /* printf-Format: %lf | %le | %lg */
      long double q,qq;  /* printf-Format: %Lf | %Le | %Lg */

      /* Addition zweier int-Zahlen */
      i=1;
      ii=-2;
      printf("int: i=%d, ii=%d, i+ii=%d\n",i,ii,i+ii);

      /* Ganzzahl-Division zweier short-int-Zahlen */
      h=7;
      hh=2;
      printf("short: h=%hd, hh=%hd, h/hh=%hd\n",h,hh,h/hh);

      /* Divisionsrest zweier long-int-Zahlen */
      l=7l;
      ll=2l;
      /* Achtung beim Prozent: Zur Ausgabe doppelt schreiben */
      printf("long: l=%ld, ll=%ld, l%%ll=%ld\n",l,ll,l%ll);

      /* Ausgabe zweier Zeichen */
      c='A';
      cc='B';
      printf("char: c=%c, cc=%c\n",c,cc);
      /* Probieren Sie auch die Zeichen '\n', '\t', '\b' aus */

      /* Multiplikation zweier float-Zahlen */
      f=1.3E4f;
      ff=-5.7E3f;
      printf("float: f=%f, ff=%f, f*ff=%f\n",f,ff,f*ff);

      /* Division zweier double-Zahlen (Gleitkomma-Division) */
      d=1.3E4;
      dd=-5.7E3;
      printf("double: d=%lf, dd=%lf, d/dd=%lf\n",d,dd,d/dd);

      /* Subtraktion zweier long-double-Zahlen */
      q=1.3E4l;
      qq=1.299999999E4l;
      printf("long double: q=%Lf, qq=%Lf, q-qq=%Lf\n",q,qq,q-qq);

      /* Ergebnis eines Vergleichs ist 0 oder 1 als int-Zahl */
      c='A';
      cc='B';
      printf("Vergleich von %c und %c ergibt %d.\n",c,cc,c==cc);

      /* Mehrere Zuweisungen in einem Ausdruck ausführen */
      c=cc='X';
      printf("Zwei Ixe: %c%c\n",c,cc);

      /* Man kann Gleitkommazahlen in verschiedenen Formaten
      ausgeben. Die 20 hinter dem % füllt links mit Leerzeichen
      auf 20 Zeichen auf. */
      dd=4E0/3.0;
      printf("Gleit- > %20lf %20le %20lg\n",dd,dd,dd);
      dd=4E9/3.0;
      printf("komma- > %20lf %20le %20lg\n",dd,dd,dd);
      dd=4E20/3.0;
      printf("formate> %20lf %20le %20lg\n",dd,dd,dd);

      /* Funktion main() und damit Programm beenden */
      return(0);
      }

### Ausdrücke

Unter einem Ausdruck versteht man die Verknüpfung von Operanden (Werte, z.B. Variable, Konstante) mittels Operatoren zu einem neuen Wert. Ein Ausdruck liefert somit immer einen Wert. C besitzt gegenüber anderen Programmiersprachen (z.B. Pascal) ein wesentlich erweitertes Ausdrucks- und Operator-Konzept:

- C besitzt sehr viele Operatoren, darunter viele, die in anderen Sprachen nicht vorkommen bzw. dort nicht als Operatoren betrachtet werden. Es gibt unäre, binäre und einen ternären Operator, die in einer Präzedenzreihenfolge angeordnet sind (näheres in Kapitel 4).

- Auch das Zuweisungssymbol "=" ist ein Operator, der einfache Zuweisungsoperator (es gibt daneben noch zusammengesetzte Zuweisungsoperatoren). Die Zuweisung ist also auch ein Ausdruck. Ihr Wert ist der Wert, der der linken Seite zugewiesen wird, z.B. hat

      a = 7

  den Wert 7.

- Jeder mit einem ; abgeschlossene Ausdruck ist in C eine Anweisung (Ausdrucksanweisung). Die Zuweisung wird ebenfalls erst durch den abschließenden ";" zu einer Anweisung.

- Auch ein -wie ein Prozeduraufruf aussehender- alleiniger Funktionsaufruf stellt -mit abschließendem ";" - eine Ausdrucksanweisung dar.

### L-Werte und R-Werte

Ausdrücke habe eine unterschiedliche Bedeutung, je nachdem, ob sie links oder rechts vom Zuweisungsoperator stehen. Im Beispiel

    a = b;

steht der Ausdruck auf der rechten Seite für einen Wert, während der Ausdruck auf der linken Seite die Stelle angibt, an der der Wert zu speichern ist. Wenn wir das Beispiel noch etwas modifizieren, wird der Unterschied noch deutlicher:

    a = a + 42;

Der Name a, der ja auch einen einfachen Ausdruck darstellt, wird hier in unterschiedlicher Bedeutung verwendet. Rechts vom Zuweisungsoperator ist der Wert gemeint, der in der Speicherzelle a gespeichert ist, und links ist die Adresse der Speicherzelle a gemeint, in der der Wert des Gesamtausdrucks auf der rechten Seite gespeichert werden soll. Aus dieser Stellung links oder rechts des Zuweisungsoperators wurden auch die Begriffe L-Wert (L-Value) und R-Wert (R-Value) abgeleitet.

> Ein Ausdruck stellt einen L-Wert dar, wenn er sich auf ein Speicherobjekt bezieht. Ein solcher Ausdruck kann links und rechts des Zuweisungsoperators stehen.

> Ein Ausdruck, der keinen L-Wert repräsentiert, stellt einen R-Wert dar. Er darf nur rechts des Zuweisungsoperators stehen. Einem R-Wert kann man also nichts zuweisen.

Ein Ausdruck, der einen L-Wert darstellt, darf auch rechts vom Zuweisungsoperator stehen, er hat dann aber, wie oben erwähnt, eine andere Bedeutung. Steht ein L-Wert rechts neben dem Zuweisungsoperator, so wird dessen Namen bzw. Adresse benötigt, um an der entsprechenden Speicherstelle den Wert der Variablen abzuholen. Dieser Wert wird dann zugewiesen. Links des Zuweisungsoperators muss immer ein L-Wert stehen, da man den Namen bzw. die Adresse einer Variablen braucht, um an der entsprechenden Speicherstelle den zugewiesenen Wert abzulegen.
Des Weiteren wird zwischen modifizierbarem und nicht modifizierbarem L-Wert unterschieden. Ein nicht modifizierbarer L-Wert ist z.B. der Name eines Arrays. Dem Namen entspricht zwar eine Adresse. Diese ist jedoch konstant und kann nicht modifiziert werden. Auf der linken Seite einer Zuweisung darf also nur ein modifizierbarer L-Wert stehen, jedoch kein R-Wert oder ein nicht modifizierbarer L-Wert. Nicht modifizierbare L-Werte liegen dann vor, wenn es sich bei dem L-Wert um einen Arraytyp, einen unvollständigen Typ, einen mit dem Typ-Attribut `const` versehenen Typ oder um einen `structure`- oder `union`-Typ handelt, von dem eine seiner Komponenten einen mit dem Attribut `const` versehenen Typ hat.

Bestimmte Operatoren können nur auf L-Werte angewandt werden. So kann man den Inkrementoperator ++ oder den Adressoperator & nur auf L-Werte anwenden. 5++ ist falsch, i++ ist korrekt (falls i eine Variable darstellt). Der Inhaltssoperator * kann auf L- und R-Werte angewandt werden.

Ein L-Wert ist also ein Ausdruck, der ein Datenobjekt bezeichnet. Außer dem schon besprochenen Fall eines L-Wertes auf der rechten Seite einer Zuweisung gibt es viele weitere Fälle bei denen ein L-Wert in den Wert umgewandelt wird, der in dem entsprechenden Objekt gespeichert ist. Ein L-Wert, der nicht von einem Array-Typ ist, wird stehts in den Wert gewandelt, der in dem entsprechenden Objekt gespeichert ist und ist damit kein L-Wert mehr, es sei denn das Objekt ist:

- Operand des sizeof-Operators
- Operand des Adress-Operators &
- Operand des Inkrement-Operators + +
- Operand des Dekrement-Operators
- der linke Operand des Punktoperators . bei Strukturen
- der linke Operand des Zuweisungsoperators

## 2.2 Auswahlstrukturen mit if .. else

Sie ist gekennzeichnet durch einen nicht linearen Ablauf mit einer Vorwärtsverzweigung. Der Ablauf gelangt an einen Entscheidungspunkt, an dem, abhängig von einer Bedingung, unterschiedliche Verarbeitungswege eingeschlagen werden. Das Entscheidungssymbol gibt eine Bedingung an (i. a. ein bedingter Ausdruck), deren Ergebnis ein Wahrheitswert ist.

#### einseitige Auswahl

Diese Alternativstruktur führt nur auf einem der beiden Verzweigungspfaden eine Anweisung(sfolge) aus und endet in der Zusammenführung beider Pfade.

`if (Bedingungsausdruck) Anweisung;`

#### zweiseitige Auswahl

Bei dieser Alternativstruktur führt jeder Verzweigungspfad auf jeweils eine eigene Anweisungsfolge. Sie endet auch wieder in einer Zusammenführung der Pfade.

    if  (Bedingungsausdruck)
       Anweisung1;
    else
       Anweisung2;

Pascal-Programmierer sollten beachten, daß hier vor dem "else" sehr wohl ein Semikolon steht. Beispiel 1:

    if (a > b)
      max = a;
    else
      max = b;

Beispiel 2:

    main()
    {
       int x, y, z;
       x = 3; z = 2;

       if ( z != 0 )        /*  auch:   if ( z )  */
          y = x/z;
       else
          printf("Division durch Null\n");

       printf("y = %d\n",y);
    }

Wichtig: `else` gehört im Zweifel immer zum letzten `if`. Gegebenenfalls mit `{ ... }` klarstellen, was wohin gehört. Beispiel:

    if(i==1)
      if(j==2)
        printf("i = 1, j = 2.\n");
      else
        printf("i = 1, j unbekannt.\n");
    else
      if(j==2)
        printf("i unbekannt, j = 2.\n");
      else
        printf("i und j unbekannt.\n");

Ketten: `if (a) b; else if (c) d; else if (e) f; else g;`. Beispiel:

    if(i==1)
      printf("i ist eins.\n");
    else if(i==2)
      printf("i ist zwei.\n");
    else if(i==3)
      printf("i ist drei.\n");
    else if(i==4)
      printf("i ist vier.\n");
    else
      printf("i ist eine Zahl.\n");

Vorsicht: Nicht `if (...) { ... } ; else { ... }`!
Entweder es kommt nur eine Anweisung nach dem `if (...)`, dann kommt ein Strichpunkt dahinter, bei mehreren Anweisungen werden diese mit `{ ... }` als Block geklammert und hinter die Klammer darf kein Strichpunkt.

Beispiel: Simulation eines Taschenrechners

    #include <stdio.h>

    void main(void)
      {
      char operator;
      int  wert1, wert2, erg ;

      if (scanf("%d %c %d",&wert1, &operator, &wert2) == 3)
        {
        if      (operator== '+')
                  erg = wert1 + wert2;
        else if (operator == '-')
                  erg = wert1- wert2;
        else if (operator=='*')
                  erg = wert1 * wert2;
        else if (operator=='/')
                  erg = wert1 / wert2;
        else    printf("Falsche Eingabe\n");
        printf("%d %c %d=%d\n",wert1,operator,wert2,erg);
        }
      else
        printf("zu wenig Eingabewerte\n");

Vorsicht! Die Bedingungen werden nur solange ausgewertet, bis das Ergebnis feststeht. Beispiel:

`if(x <= 0 || ((c = getchar()) != EOF))
printf("%d\n", c);`

Das Zeichen wird nur gelesen, wenn x > 0.
(EOF ist eine symbolische Konstante, die in stdio.h definiert ist. Sie hat bei Dateiende den Wert -1.)

Beispiel: Im EStG, Par. 32, ist folgendes Verfahren zur Berechnung der Einkommenssteuer festgelegt:
Die Einkommenssteuer beträgt in deutsche Mark

1. für zu versteuernde Einkommen bis 4752 DM: `0 DM;`
2. für zu versteuernde Einkommen von 4753 DM bis 18035 DM: `0.22x - 1045 DM;`
3. für zu versteuernde Einkommen von 18036 DM bis 80027 DM: `(((0.34y - 21,58)y + 392)y + 2200)y + 2911 DM;`
4. für zu versteuernde Einkommen von 80028 DM bis 130031 DM: `(70z + 4900)z + 26974 DM;`
5. für zu versteuernde Einkommen von 130032 DM und mehr: `0.56x-19561 DM.`

Dabei ist x das abgerundete zu versteuernde Einkommen, y ein Zehntausendstel des 17982 DM übersteigenden Teils des abgerundeten zu versteuernden Einkommens, und z ist ein Zehntausendstel des 79974 DM übersteigenden Teils des abgerundeten zu versteuerndes Einkommens.
Das zu versteuernde Einkommen ist zunächst vor jeglicher Berechnung auf den nächsten durch 54 ohne Rest teilbaren Betrag abzurunden, wenn es nicht bereits durch 54 ohne Rest teilbar ist.
Gesucht wird ein C-Programm, welches zu einem einzulesenden Einkommen die zu zahlende Einkommenssteuer berechnet und ausgibt.

    #include<stdio.h>
    #include<math.h>

    int main(void)
      {
      double steuer, einkommen, y;

      scanf(Daten,"%lf",&einkommen);
      if (einkommen < 0)
        printf("Einkommen sollte positiv sein\n");
      else
        {
        /* Zahl muss abgerundet werden */
        einkommen = floor(einkommen / 54) * 54;
        if (einkommen < 4753)
          steuer = 0;
        else if (einkommen < 18036)
          steuer = 0.22 * einkommen - 1045;
        else if (einkommen < 80028)
          {
          y = (einkommen - 17982)/10000.0;
          steuer = (((0.34*y - 21.58)*y + 392)*y + 2200)*y + 2911;
          }
        else if (einkommen < 130032)
          {
          y = (einkommen-79974)/10000.0;
          steuer = (70*y + 4900)*y + 26974;
          }
        else steuer = 0.56 * einkommen - 19561;
        }
      printf("%10g %10g\n",einkommen,steuer);
      }

## 2.3 Bedingungsausdrücke mit dem Operator ?:

Die Syntax eines Ausdrucks mit diesem Operator hat folgendes Aussehen:

`Bedingungsausdruck ? Ausdruck1 : Ausdruck2`

Ein Ausdruck mit dem Bedingungsoperator kann nicht alleine stehen (wie `if ...`), sondern innerhalb eines Ausdrucks (z. B. einer Wertzuweisung). Ist das Ergebnis des Bedingungsausdrucks wahr (!= 0), wird Ausdruck1 verwendet, sonst Ausdruck2.
Beispiele: Vorzeichenoperator (Zahl negativ: vz = -1, Zahl positiv oder Null: vz = +1) und Maximum wie oben bei "if...":

`vz = (Zahl < 0) ? -1 : +1`

`max = (a > b) ? a : b`

## 2.4 Mehrfachauswahl mit switch .. case

Bei dieser Struktur gibt es mehr als zwei Auswahlpfade, die aus einer Verzweigung ihren Ausgang nehmen und in einer Zusammenführung enden. Hier erfolgt die Abfrage nicht nach einer Bedingung, sondern der bedingte Ausdruck liefert einen Wert. Für jeden möglichen Ergebniswert ist ein Zweig vorgesehen. Existiert nicht für jeden möglichen Ergebniswert der Bedingung ein Pfad, ist ein zusätzlicher Pfad für alle nicht behandelten Fälle vorzusehen ("sonst"-Zweig; "default").

    switch (Ausdruck)
       {
       case W1: Anweisung1;
       case W2: Anweisung2;
                  ...;
       case Wn: Anweisungn;
       default: Anweisungdef;
       }

Wenn der Ausdruck den Wert Wi besitzt, wird die Anweisung(sfolge) Anweisungi und alle folgenden ausgeführt. Will man das vermeiden, muß der jeweilige `switch`-Zweig durch `break;` abgeschlossen werden. Das sieht dann so aus:

    switch (Ausdruck)
       {
       case W1: Anweisung1; break;
       case W2: Anweisung2; break;
                  ...;  break;
       case Wn: Anweisungn; break;
       default: Anweisungdef;
       }

Den Unterschied machen auch die beiden folgenden Ablaufdiagramme sichtbar:

Beispielprogramm:

    /* Beispiel fuer switch: Einlesen einer Zahl ohne Benutzung von scanf */

    #include <stdio.h>
    #include <stdlib.h>

    int main(void)
    {
      /* Lokale Variablen - teilweise mit Anfangswert */
      unsigned long zahl=0;
      enum boolean {false,true} ende = false;
      int zeichen;

      printf("Geben Sie eine Zahl ein.\n");
      while(!ende)
      {
        /* Lesen eines Zeichens aus der Eingabe */
        zeichen=getchar();
        switch(zeichen)
        /* oder kürzer: switch(zeichen=getchar()) */
        {
        case '0':
          zahl=zahl*10;
          break;
        case '1':
          zahl=zahl*10+1;
          break;
        case '2':
          zahl=zahl*10+2;
          break;
        case '3':
          zahl=zahl*10+3;
          break;
        case '4':
          zahl=zahl*10+4;
          break;
        case '5':
          zahl=zahl*10+5;
          break;
        case '6':
          zahl=zahl*10+6;
          break;
        case '7':
          zahl=zahl*10+7;
          break;
        case '8':
          zahl=zahl*10+8;
          break;
        case '9':
          zahl=zahl*10+9;
          break;
        default:
          ende=true;
          break;
        }
      }
      printf("Zahl %lu eingelesen, danach folgte '%c'.", zahl, zeichen);
      return(0);
    }

Zweites Beispiel. Berechnung der Datumsdifferenz zweier Tage.

    #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    void main()
    {
    int tag1, mon1, jahr, tag2, mon2, tage, i;

    printf("Anzahl der Tage zwischen zwei Tagen\n");
    printf("Jahr:");
    scanf("%i",&jahr);
    printf("1. Datum: Tag:");
    scanf("%i",&tag1);
    printf("Monat:");
    scanf("%i",&mon1);
    printf("\n2. Datum: Tag:");
    scanf("%i",&tag2);
    printf("Monat:");
    scanf("%i",&mon2);

    tage = tag2 - tag1;
    for (i=mon1; i<=mon2-1; i++)
      switch (i)
        {
        case 2: if (jahr%4 == 0 && jahr%100 != 0 || jahr%400 == 0)
                  tage = tage + 29;
        	    else
                  tage = tage + 28;
                break;
        case 4:
        case 6:
        case 9:
        case 11: tage = tage + 30; break;
        case 1:
        case 3:
        case 5:
        case 7:
        case 8:
        case 10:
        case 12: tage = tage + 31; break;
        };
      printf("\n Es liegen %3i Tage dazwischen.",tage);
      }

Ein ganz einfacher Rechner:

    #include <stdio.h>
    #include <stdlib.h>

    main()
      {
      double op1, op2;
      char op[2];

      while(scanf("%lf %1s %lf", &op1, op, &op2) != EOF) {
        switch(op[0])
          {
          case '+': op1 = op1 + op2; break;
          case '-': op1 = op1 - op2; break;
          case '*': op1 = op1 * op2; break;
          case '/': op1 = op1 / op2; break;
          default: printf("illegal operator\n"); continue;
          }
        printf(" = %g\n", op1);
        }
      }

## 2.5 Wiederholungsstrukturen mit while

Wiederholungsstrukturen ergeben sich, wenn eine Anweisungsfolge zur Lösung einer Aufgabe mehrfach durchlaufen werden soll (z. B. das Bearbeiten aller Komponenten eines Feldes). Es liegt ein nichtlinearer Verlauf mit Rückwärtsverzweigung vor. Die Programmierung einer Wiederholungsstruktur führt zu einer sogenannten "Programmschleife". Wichtig ist die Terminierung der Schleife, d. h. mindestens eine Anweisung muß dafür sorgen, daß eine Variable derart verändert wird, daß nach einer endlichen Zahl von Durchläufen die Abfragebedingung nicht mehr erfüllt ist.

#### Abweisende Wiederholung

In diesem Fall steht die Abfrage zu Beginn der Schleife (also vor der Anweisungsfolge). Ist die Bedingung schon beim Eintritt in die Anweisungsstruktur nicht erfüllt, wird die Anweisungsfolge überhaupt nicht ausgeführt.

`while (Bedingungsausdruck)
Anweisung;`

Es folgen nun einige Beispiele für while-Schleifen, zuerst die Berechnung des größten gemeinsamen Teilers:

    #include <stdio.h>
    #include <stdlib.h>

    void main(void)
      {
      int i,j;

      i = 2*2*3*3*5*17;
      j = 2*2*3*5*5*13;
      /* ggt = 2*2*3*5 = 60  */
      if(i <= 0 || j <= 0)
        {
        printf("Unzulaessige Werte\n");
        }
      else
        {
        while(i != j)
          {
          i = i%j; if (i == 0) i = j;
          j = j%i; if (j == 0) j = i;
          }
        printf("ggT(i,j) = %d\n",i);
      }

Das folgende Programm berechnet Quadratzahlen zwischen zwei Grenzen:

    main()                     /* Tabelle der Quadratzahlen */
      {
      int anf, ende, spanz;
      printf("Berechnung der Quadratzahlen von: ");
      scanf("%d", &anf);
      printf("Berechnung der Quadratzahlen bis: ");
      scanf("%d", &ende);
      printf("Anzahl der Spalten fuer Ausgabe : ");
      scanf("%d", &spanz);
      putchar('\n');
      while(anf <= ende)
        {
        printf("%3d x %3d = %6d ", anf, anf, anf*anf);
        if (anf % spanz)
          printf("    ");
        else
          putchar('\n');
        anf = anf + 1;
        }
      putchar('\n');
      }

## 2.6 Wiederholungsstrukturen mit for

Diese Anweisungsstruktur stellt die universellste Form der abweisenden Wiederholung dar.

`for (Ausdruck1; Ausdruck2; Ausdruck3)
Anweisung;`

- Ausdruck1:
  Der Anfangsausdruck wird einmal zu Beginn vor dem Durchlaufen der Schleife ausgeführt; er kann z.B. zum Vorbesetzen einer Zählvariablen benutzt werden.
- Ausdruck2:
  Darauf folgt eine abweisende Wiederholung, analog den Regeln der while-Schleife, d.h. der Bedingungsausdruck wird ausgewertet und nur bei erfüllter Bedingung der Anweisungsteil durchlaufen.
- Ausdruck3:
  Zuletzt wird der Veränderungsausdruck bearbeitet, z. B. kann hier die Zählvariable inkrementiert werden, die zu Beginn des nun folgenden Schleifendurchlaufs im Bedingungsausdruck geprüft wird, um ggf. die Schleife abzubrechen.

    for(init; bedingung; inkrement)
      anweisung;

entspricht

    init;
    while(bedingung)
      {
      anweisung;
      inkrement;
      }

Ein paar Beispiele für die Anwendung des for-Statements:

Programmcode | Ergebnis
--- | ---
      for(i=0; i<4; i++){
        for(j=0; j<4; j++){
          printf("*");
        }
        printf("\n");
      }
       ****
       ****
       ****
       ****
      for(i=1; i<=7; i++){
        for(j=1; j<= 4 - (abs(i-4)); j++){
          printf("*");
        }
        printf("\n");
      }
    *
    **
    ***
    ****
    ***
    **
    *
      for(i=1; i<=7; i++){
        for(j=1; j<= abs(i-4); j++){
          printf(" ");
        }
        for(j=1; j<= 4 - (abs(i-4)); j++){
          printf("*");
        }
        printf("\n");
      }
       *
      **
     ***
    ****
     ***
      **
       *
      for(i=1; i<=7; i++){
        int num1 = 3 - abs(4-i),
            num2 = 5 - 2 * num1;

        for(j=0; j< num1; j++){
          printf(" ");
        }
        printf("*");

        for(j=0; j< num2; j++){
          printf(" ");
        }
        if( num2 > 0)
          printf("*");

        printf("\n");
      }
    *     *
     *   *
      * *
       *
      * *
     *   *
    *     *

Das folgende komplette Programm erzeugt eine Multiplikationstabelle:

    #include <stdio.h>
    #include <stdlib.h>

    void main(void)
      {
      int i,j;

      printf(" * |   1   2   3   4   5   6   7   8   9  10\n");
      printf("---+----------------------------------------\n");
      for(i=1; i<=10; i++)
        {
        printf("%2d |",i);
        for(j=1; j<=10; j++)
          {
          printf(" %3d",i*j);
          }
        printf("\n");
        }
      }

## 2.7 Wiederholungsstrukturen mit do .. while

In diesem Fall steht die Abfrage im Gegensatz zu 2.5 am Ende der Schleife (also nach der Anweisungsfolge). Die Anweisungsfolge wird auf jeden Fall mindestens einmal ausgeführt. Man nennt diese Anweisungsstruktur daher auch nichtabweisende Wiederholung.

`do
Anweisung;
while (Bedingungsausdruck);`

Schleifenkontrolle am Ende, Berechnung von 5!:

    #include <stdio.h>
    #include <stdlib.h>

    void main(void)
      {
      int i,j;

      i = j = 1;
      do
        {
        j = j*i;
        i = i+1;
        } while (i <= 5);
      printf("5! = %d\n",j);
      }

## 2.8 Unstrukturierte Kontrollanweisungen

In C ist es möglich, beliebige Anweisungen durch eine Marke zu kennzeichnen. Die Marke folgt dabei den Namenskonventionen für Variablen. Die Marke wird mit einen Doppelpunkt abgeschlossen.

`Marke:`

Diese Marke kann mit dem `goto`-Befehl angesprungen werden.

`goto Marke;`

Daneben gibt es noch zwei weitere unstrukturierte Verzweigungsbefehle:

`break`
bricht die Bearbeitung der aktuellen strukturierten Anweisung ab (siehe `switch`).

`continue`
führt einen Sprung zum Schleifenende aus, um dann mit dem nächsten Schleifendurchlauf fortzufahren.

## 2.9 Zusicherungen (Assertions)

Das Konzept der Zusicherungen stammt aus dem Gebiet der Qualitätssicherung und Software-Verifikation. Eine Zusicherung dient dazu, an einer bestimmten Stelle in einem Programm sicherzustellen, daß eine Bedingung immer erfüllt ist (z.B. "Variable X ist an dieser Stelle größer als Null"). Durch die Formulierung und Prüfung dieser Bedingungen kann gewährleistet werden, daß sich ein Programm an einer bestimmten Stelle korrekt verhält und nicht in einem unbestimmten oder nicht erwarteten Zustand ist. Generell werden drei Arten von Zusicherungen unterschieden:

- Vorbedingungen (preconditions), d.h. Prüfungen, die zu Beginn eines Programmabschnitts durchgeführt werden. Ein klassisches Beispiel für Vorbedingungen sind Werteüberprüfungen für Funktionsparameter.
- Nachbedingungen (postconditions), die stets am Ende eines Programmabschnitts gelten müssen.
- Invarianten, also Bedingungen, die immer gelten müssen. Ein Beispiel hierfür wäre "die Zählvariable j der for-Schleife muss immer im gültigen Indexbereich des Arrays sein, über dem iteriert wird". Tests, ob derartige Zusagen zutreffen, sollten eigentlich unnötig sein. Trotzdem sollten auch "ewige Wahrheiten" abgeprüft werden, weil
  - das Programm gegenüber dem Entwurf fehlerhaft sein könnte,
  - sich nach Änderungen, Erweiterungen, Korrekturen usw. evtl. die Voraussetzungen verschieben,
  - ein Leser auf derartige Erkenntnisse aufmerksam gemacht wird.

Assertions

An bestimmten Punkten im Programm werden die Zusicherungen festgeschrieben und zwar als logischer Ausdruck in der `assert`-Anweisung ("Zusicherung"):

    	assert(expression);

expression ist ein logischer Ausdruck. `assert()` ust in der Include-Datei `assert.h` definiert. der Ausdruck wird jedesmal ausgewertet, wenn `assert`-Anweisung ausgeführt wird. Das Ergebnis kann sein:

`!= 0`: | keine Aktion
`0`: | Programm wird sofort abgebrochen unter Angabe von Dateiname, Zeilennummer und Klartext der verletzten Zusicherung.

Beispiel: Offenkundig fehlerhaftes Quelltextfragment (statt `<=` müßte `<` stehen):

    	// Werte von 0...9 verarbeiten
    	for(int i = 0;  i <= 10;  i++)
    	{
    		assert(i >= 0  &&  i < 10);
    		... i verarbeiten ...
    	}

Liefert bei Ausführung die Meldung

    	main.C:8: failed assertion `i >= 0 && i < 10'

Alle `assert`-Anweisungen eines Quelltextes lassen sich abschalten durch Compilerschalter `-DNDEBUG`. Der Code wird dann übersetzt, als ob keine `assert`-Anweisung existierte. Daher werden `assert`-Anweisungen während Entwicklung und Test eines Programms aktiviert und vor der Auslieferung der Software abgeschaltet.

Der Einsatz von Zusicherungen erfordert gesunden Menschenverstand (!). Es muß ein goldener Mittelweg zwischen Trivialitäten und Unwägbarkeiten gefunden werden. Die korrekte Abwicklung einzelner Sprachkonstrukte kann vorausgesetzt werden und muß nicht geprüft werden. Bedingungen, die an äußere Einflüsse (Benutzereingaben, Dateiinhalte, ...) gebunden sind, muß die Programmlogik behandeln, sie lassen sich nicht mit Assertions abhandeln.

Beim folgenden Beispiel wird mittels assert überprüft, ob der stream auch wirklich ungleich NULL ist. Ist er NULL, dann ist er mit Sicherheit nicht gültig, unser Ausdruck liefert einen Wert von 0 (Falsch) und das Programm wird angehalten. Schreiben Sie ein kleines Programm, das diese Funktion enthält und übergeben Sie einmal einen gültigen Dateizeiger und einmal NULL. Dann versuchen Sie beides nochmals, nachdem Sie vor der Includeanweisung für `assert.h` die Zeile`#define NDEBUG` eingefügt haben.

    int WriteInFile(FILE *fd)
      {
      assert(fd != NULL);
      fputs("Hallo Welt",fd);
      }

Zusicherungen verwendet man für Bedingungen, die aus der Entwurfslogik stammen. Passende Punkte für Zusicherungen sind:

- Anfang und Ende von Schleifenrümpfen;
- Trennstellen zwischen größeren zusammengesetzten Kontrollstrukturen;
- Eintritt in Programmabschnitte, die nur unter bestimmten Voraussetzungen funktionieren;
- Abschluß einer Fehlerbehandlung;

Wie bereits erwähnt, werden `assert`-Anweisungen nur dann ausgewertet, wenn die Prüfung von Zusicherungen ausdrücklich aktiviert ist. Standardmäßig sind Zusicherungen deaktiviert. Aufgrund dieser Tatsache sollten die Ausdrücke in `assert`-Anweisungen keinerlei Seiteneffekte enthalten, da sonst das Programmverhalten davon abhängt, ob Zusicherungen aktiviert sind oder nicht, wie das folgende Beispiel zeigt:

       assert (i++ < limit);

In dieser Zeile wird `i` nur dann inkrementiert, wenn Zusicherungen aktiviert sind. Andernfalls wird die Anweisung nicht ausgeführt und der Wert bleibt unverändert. Somit hängt das Verhalten des Programms davon ab, ob Zusicherungen aktiviert sind oder nicht.

## 2.10 Der C-Präprozessor

Bevor ein C-Quelltext compiliert wird, können noch formale Ersetzungen vorgenommen werden. Dies geschieht mit dem sog. "Präprozessor". Präprozessoranweisungen beginnen mit einem `#`. Da sie rein zeilenorientiert sind, stehen sie immer am Zeilenanfang, bzw. als erstes und einziges in einer Zeile und werden nicht durch einen Strichpunkt terminiert. Zunächst sind folgende Präprozessorbefehle wichtig:

- `#include` zum Einbinden von Dateien:
  - `#include <Standardbibliotheksdatei>` setzt an diese Stelle die genannte Datei aus der Standardbibliothek (d. h. der Suchpfad ist implementationsabhängig).
  - `#include "Meinedatei"` dient zum Einbinden eigener Dateien.
  `#include`-Anweisungen können geschachtelt werden, d.h. es können Dateien eingebunden werden, die ihrerseits `#include`-Anweisungen enthalten.

- `#define` zum Defininieren eines Makros:
  - Einfache Ersetzung; Beispiel:
    `#define MAX 1000`
    bewirkt, daß ab dieser Anweisung alle `MAX` mit `1000` ersetzt werden. Konstanten sollte man niemals explizit in den Quellcode schreiben, sondern immer solchermaßen formal definieren. Es ist Konvention (wenn auch syntaktisch nicht zwingend), daß so definierte Konstanten großgeschrieben werden.
  - Makroersetzung (Funktions-ähnlich); Beispiel:

          #define CMULT(x)  (3.8*(x))
          #define SQ(x)     ((x)*(x))
          #define MAX(a,b)  ((a) > (b) ? (a) : (b))

    Vorteil: Makroersetzungen funktionieren für alle Typen.
    Nachteil: Die Wahrscheinlichkeit von Seiteneffekten ist hoch; so ist z. B. die exzessive Klammerung in den obigen Beispielen absolut notwendig. würde die Definition ohne Klammern erfolgen, z. B.`SQ(x) x * x`, dann ergäbe der Aufruf `SQ(2 + 5)` nicht etwa `7 * 7 = 49`, sondern als Textersetzung `"2 + 5 * 2 + 5" = 2 + 10 + 5 = 17`. Weiterhin werden u. U. auch ganze Ausdrücke ersetzt, und also ebenso oft entwickelt.
  - `#define` wirkt nicht innerhalb von `" "`.
  - Die `#define`-Anweisung läßt sich rückgängig machen mit dem `#undef`-Befehl; Beispiel: `#undef MAX`

- `#if, #else, #endif`: Bedingte Compilierung. Beispiel:

      #if  MAX < 1000
        #define  MIN  100
      #else
        #define  MIN  200
      #endif

  Allgemein wertet `#if` einen konstanten Integerausdruck aus; statt '`<`' kann irgendein Vergleichsoperator stehen. Weiterhin läßt sich mit `#ifdef name` verzweigen, wenn dieses Token definiert, bzw. mit `#ifndef name`, wenn es nicht definiert ist, Beispiel:

      #ifdef  DEBUG
        printf("a hat hier den Wert:  %f\n", a);
      #endif

- Eigene Präprozessor-Fehlermeldungen lassen sich mit `#error` ausgeben, was außerdem den Compilierungsvorgang abbricht. Beispiel:

      #ifndef  AFLAG
         #error  "AFLAG" nicht definiert!
      #endif

Mit dem Präprozessor kann man auch Quatsch machen. Was tut beispielsweise das folgende Programm?

    #include <stdio.h>

    #define o scanf
    #define O printf
    #define ooh int
    #define OO "%d"
    #define Oho {
    #define oho }
    #define Oh main
    #define hohoho o##0
    #define oh *
    #define ho (
    #define Ho )
    #define harhar ;

    Oh ho Ho Oho ooh hohoho harhar o ho OO, &hohoho Ho harhar
    O ho OO, hohoho oh hohoho oh hohoho Ho harhar oho

Wenn man anschaut, was der Präprozessor draus macht, wird es schon etwas klarer. Der Aufruf des Compilers dazu lautet:

    gcc -E programm.c

Dabei kommt unter anderem heraus:

    main  (  )  {  int  o0  ;  scanf  (  "%d" , & o0  )  ;
    printf  (  "%d" , o0  *  o0  *  o0  )  ;  }

Mit ein bisschen Umformatieren gibt das:

    main()
      {
      int o0;

      scanf( "%d", &o0 );
      printf( "%d", o0*o0*o0 );
      }

Das ist offensichtlich ein Programm, das die dritte Potenz einer eingegebenen ganzen Zahl berechnet und ausgibt.

Zum Inhaltsverzeichnis | Zum nächsten Abschnitt

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