Programmieren in C

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

# 3 Funktionen

## 3.1 Konzept

Es wurde bereits die Aufteilung von Programmen in einzelne Module angesprochen. Diese Aufteilung auch in einem Programm zu vollziehen erscheint daher wünschenswert. In allen höheren Programmiersprachen und auch im Befehlsumfang nahezu aller Prozessoren ist diese Möglichkeit in Form von Unterprogrammen realisiert. Es gilt also:

- Funktionen sind Hilfsmittel, um Problemstellungen in kleine Teilprobleme zerlegen zu können
- sie dienen damit einer strukturierten Programmierung
- typische Anwendung für Funktionen ist weiterhin die Erledigung immer wiederkehrender Aufgaben
- für die wichtigsten Aufgaben gibt es bereits Funktionen, sie sind in der C-Programmbibliothek enthalten
- für eigene Zwecke kann man natürlich auch eigene Funktionen schreiben
- C Programme bestehen typischerweise aus vielen kleinen und nicht aus wenigen großen Funktionen

In C wird nicht, wie in anderen Programmiersprachen, zwischen Prozeduren und Funktionen unterschieden, sondern es gibt nur Funktionen, d. h. Umterprogramme, die einen Wert (vom einem bestimmten Typ) zurückliefern. Man kann jedoch für den Rückgabewert den Typ `void` verwenden und somit eben nichts zurückliefern. Die Definition einer Funktion erfolgt durch Angabe eines Typs, gefolgt vom Funktionsnamen und einer Parameterliste in runden Klammern.

    int blabla ()
      {
      return (0);
      }

Der Aufruf kann dann z. B. als Anweisung `x = blabla();` erfolgen. Funktionen sind stets global: sie können nur außerhalb jeder anderen Funktion (einschließlich main()) definiert werden, und sind dann aus jeder Funktion (einschließlich derselbigen, siehe unten: Rekursion) aufrufbar. Funktionsdefinitionen lassen sich demnach in C nicht schachteln, sie werden alle auf einer "Ebene" definiert.

Die Definition einer Funktion wird syntaktisch beschreiben als:

Eine Funktion hat also einen festgelegten Aufbau (genauere Definition der ersten Zeile siehe unten):

    Typ Funktionsname(Parameterliste)
      {
        Vereinbarungen
        Anweisungen
        return (Funktionswert)     optional
      }

Die runden Klammern bei der Vereinbarung müssen stehen, damit Funktionsname zur Funktion wird. Die Parameterliste in den runden Klammern ist optional, d.h. sie muß nur vorhanden sein, wenn der Funktion wirklich Parameter übergeben werden. Andernfalls ist als Platzhalter void anzugeben. Die Parameterliste enthält sowohl die Namen als auch die Typdeklarationen der Parameter. Die Gesamtheit der Vereinbarungen und Anweisungen der Funktion selbst nennt man den Funktionskörper ("body"), er muß durch geschweifte Klammern eingeschlossen sein. Die return-Anweisung darf an beliebiger Stelle im Funktionskörper stehen, mit ihr erfolgt der Rücksprung in die aufrufende Funktion oder ins Hauptprogramm. Die return-Anweisung kann auch ganz fehlen, dann erfolgt der Rücksprung in die aufrufende Funktion beim Erreichen des Funktionsendes (der schließenden geschweiften Klammer um den Funktionkörper).
Der Funktionswert hinter der return-Anweisung wird meist in runde Klammern eingeschlossen. Dies ist aber nicht notwendig. Fehlt der Funktionswert, so wird an die aufrufende Funktion auch kein Wert zurückgegeben.

Ebenfalls erlaubt und bei älteren C-Programmen (nicht ANSI-C) häufig zu finden ist folgender Funktionsaufbau:

    Typ Funktionsname(optionale Parameternamen)
      Parameterdeklarationen
      {
        Vereinbarungen
        Anweisungen
        return (Funktionswert)  optional
      }

Die Parameternamen müssen alle in zugehörigen Parameterdeklarationen auftauchen.

Im Prinzip sieht ein C-Programm immer wie das folgende Beispiel aus:

    void main (void)
      {
      int x, y;
      ...
      blabla ();
      ...
      x = foobar(y);
      ...
      }

    void blabla (void)
      {
      ...
      }

    int foo ();
      {
      ...
      }

    int foobar (int x)
      {
      ...
      x = 10 * foo();
      ...
      }

Dabei ergibt sich ein Problem. In main() ist nälich noch gar nicht bekannt, welche Parameter die Funktionen blabla(), foo() und foobar() haben, welchen Typ die Parameter besitzen und welche Rückgabewert die Funktion hat. Die Funktionen werden ja erst unterhalb von main() definiert. Der C-Compiler nimmt dann int als Standardtyp an. Lösen läßt sich das Problem durch eine Vorwärts-Deklaration der Funktionen. Dabei werden nur Funktionsname und Parameter angegeben und die Angabe mit Strichpunkt abgeschlossen. Die Funktionsdefinition an anderer Stelle enthält dann auch den Code dazu. Zum Beispiel:

    void blabla (void);
    int foo ();
    int foobar(int x);

    void main (void)
      ...

      weiter wie oben
      ...

Das Syntaxdiagram für die Definition des Prototypen sieht so aus:

## 3.2 Parameter

Soll eine Funktion mit unterschiedlichen Eingabewerten (= Parametern) aufgerufen werden, kann bei der Vereinbarung eine Liste der Parameter hinter dem Funktions-Namen aufgeführt werden. Bei jedem dieser Parameter wird dessen Typ aufgeführt. Diese formalen Parameter haben einen frei wählbaren Namen, der innerhalb des Anweisungsteils der Funktion gültig ist und wie jede andere Variable verwendet werden kann.

    int quad (int x)
      {
      return (x * x);
      }

Bei C gibt es noch eine etwas ältere Form der Parameterangabe, bei der in den Klammern nur die Parameternamen und anschliessend deren Typ aufgeführt wird (Soll bei neuen Programmen nicht mehr verwendet werden!.

    int quad (x)
      int x;
      {
      return (x * x);
      }

Grundsätzlich gilt: Eine Funktion muß vor ihrer Verwendung deklariert werden. Es hat sich daher als zweckmäßig erwiesen, zu Beginn eines Programms nicht nur Variablen, sondern auch Funktionen zu deklarieren. Dies geschieht durch den Funktionsnamen mit Parameterliste. Statt des Anweisungsteils in geschweiften Klammern wird nur ein Strichpunkt gesetzt.

`int quad (int x);`

Die Funktion mit Anweisungsteil wird dann später im Programm definiert. Solche Prototyp-Deklarationen findet man auch in den Header-Dateien.

Beim Aufruf der Funktion werden die formalen Parameter durch die aktuellen Parameter (=Argumente) ersetzt (Parameterversorgung der Funktion). Ähnlich wie bei allen anderen Variablen können diese durch ihre expliziten Werte, Konstante oder Variable versorgt werden. Zum Beispiel:

`y = quad(25);`

Der Typ des aktuellen Parameters muß natürlich auch den Typ des formalen Parameters entsprechen (wird bei vielen höheren Programmiersprachen geprüft) --> sichere Programme.

Beispiel fuer eine Funktion, Potenzen berechnen:

    #include <stdio.h>

    int power(int,int);   /* Prototyp */

    int main(void)
      {
      int i;
      for (i = 1; i <= 10; ++i)
          printf("%d %d %d\n", i, power(2,i), power(-3,i));
      exit(0);
      }

    int power(int base, int n)
      {
      int i, p = 1;
      for (i = 1; i <= n; i++)
        p = p*base;
      return p;
      }

Bei vielen höheren Programmiersprachen wird bei den Parametern neben dem Typ auch noch die Art der Parameterersetzung festgelegt. Man unterscheidet zwischen folgenden Arten der Parameterersetzung:

- Der aktuelle Parameter wird ausgewertet und das Resultat dem formalen Parameter, der eine lokale Variable darstellt, zugewiesen. Diese Art wird Werte-Parameter (value parameter) genannt und ist die häufigste Form der Ersetzung. Anstelle des Parameters kann auch ein Ausdruck stehen (z. B.: quad(2*x + 1)).
- Der aktuelle Parameter ist eine Variable (handelt es sich um eine Array-Komponente, wird der Index-Ausdruck ausgewertet). Die Variable ersetzt den formalen Parameter. Diese Art wird Variablen-Parameter (variable parameter) genannt und muß immer dann verwendet werden, wenn der Parameter ein Resultat der Funktion darstellt (d. h. wenn Werte aus der Funktion heraus transportiert werden sollen).
  In C wird hier die Adresse der Variablen übergeben. Dies geschieht bei der Funktionsdefinition durch das Voranstellen eines Sternchens (*) vor den Parameternamen, z. B. `int *varpar`.
  Um beim Aufruf der Funktion den Wert herauszutransportieren, wird dem aktuellen Variablenparameter ein &-Zeichen vorangestellt, z. B. foo(&x);. Was sich hinter den Operatoren '*' und '&' genau verbirgt, erfahren Sie im Kapitel über Pointer. Für den Augenblick reicht die Regel:
  - Adressoperator:
    & liefert die Adresse einer Variablen zurück. Beispiel: `int a;`: Adresse von a ist `&a`
  - Inhaltsoperator:
    * gibt den Inhalt einer Speicherstelle mit einer bestimmten Adresse an. Beispiel: `int a;`: a = *(&a)
  Beispiel: Funktion, die die Inhalte zweier Variablen vertauscht.

      void  swap(int *n, int *m)
        {
        int  park;

        park = *n;
        *n = *m;
        *m = park;
        }

      void main(void)
        {
        int  a, b;
            ...
        swap(&a, &b);    /* Funktionsaufruf */
            ...
        }

Beispiel für formale und aktuelle Parameter:
Beim Programm für die quadratische Gleichung könnte man den Algorithmus zur Lösung der Gleichung als Unterprogramm formulieren, um es beliebig oft von einem Hauptprogramm aufrufen zu können, wobei jedesmal beliebige Parameter verwendet werden können um unterschiedliche Gleichungen zu lösen.

      float quadgl()
        {
          .... Anweisungen ....
        }

Vor Aufruf im Hauptprogramm muß zunächst die Versorgung der Variablen a, b, c mit den gewünschten Werten erfolgen.

      a = 2; b = 4; c = -20;
      quadgl();
            ...
      a = -2; b = 5; c = 31.5;
      quadgl();
            ...

Mit den Werten a, b, und c als Parametern wird das Funktion wesentlich flexibler; zunächst die Definition:

       float quadgl (float a, float b, float c)
        {
          .... Anweisungen ....
        }

Beim Aufruf im Hauptprogramm können nun die Parameter direkt übergeben werden:

       quadgl (2, 4, -20);
          ...
       quadgl (-2, 5, 31.5);
          ...

Die Parameterversorgung ist nicht auf die Angaben direkter Werte beschränkt. Selbstverständlich lassen sich auch beliebige Variablen oder Konstante in der Parameterliste aufführen, z. B.:

       quadgl (x, y, z);

#### Beispiel: Berechnung eines Kreisumfangs

    /* ... kreis.c ...  */
    #include <stdio.h>
    #include <stdlib.h>

    #define PI 3.1416

    double circle(float rad);   /* Prototyp der Funktion */

    int main(void) {             /* Hauptprogramm ruft Funktion auf */
      double perimeter;
      float  radius = 5;

      perimeter = circle(radius);

      return 0;
      }

    double circle(float rad) {  /* Funktionsdefinition */
      double result;
      result = 2.0 * rad * PI;
      return result;
    }

#### Beispiel: Steuerberechnung

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.

    double Steuer(double einkommen)
      {
      double steuer, y;
      /* Zahl muss abgerundet werden */
      einkommen = floor(ein / 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;
      return(steuer);
      }

#### Feld als Übergabeparameter

Da der Name eines Feldes ein Zeiger auf das erste Element des Feldes ist (mit anderen Worten: die Adresse des ersten Elements ist), wird bei Feldern immer eine Adresse übergeben. Das bedeutet, daß die Funktion immer mit dem Originalfeld arbeitet. Es findet keine Feldgrenzenüberprüfung beim Funktionsaufruf statt. Für die Vereinbarung der Aktualparameter hat man zwei Möglichkeiten:

1. Felddeklaration:
   Intern wird dann eine Typumwandlung nach Zeiger vorgenommen.
2. Zeiger (siehe später).

Beispiel: Feldelemente aufaddieren; Summe auf feld[0] speichern

    #include <stdio.h>
    void sum(int a[5],int);

    int main(void)
      {
      int feld[5] ={0,2,3,4,5};
      int n = 3, summe;

      printf("summe = %d\n",feld[0]);
      sum(&feld[0],n);
      printf("summe = %d\n",feld[0]);
      exit(0);
      }

    void sum(int a[5], int n)
      {
      while (n-- >1)
        a[0] += a[n];
      return;
      }

Das folgende Programm demonstiert die Übergabe von Arrays an Funktionen: Es ist nicht nötig, bei der Funktionsdefinition die Arraylänge festzulegen. Arrays werden stets per Referenzaufruf - also als Variablenparameter - übergeben. Die Funktion "vektorsumme" berechnet die Summe zweier int-Vektoren (= Arrays) a und b und speichert sie im Vektor c ab. Die Vektorsumme ist dabei definiert als c[i] = a[i] + b[i] für alle Arrayindizes i. Es wird vorausgesetzt, daß a, b und c gleich lang sind.

Die Vektorlänge ist hier nicht von vornherein festgelegt, so daß die Funktion für Vektoren unterschiedlicher Längen aufgerufen werden kann. Die konkrete Länge wird beim Aufruf im Parameter "laenge" angegeben. Da Arrays stets per Referenzaufruf übergeben werden, hat die Änderung der Werte in von c in der Funktion unmittelbare Auswirkung auf den aktuellen Parameter, also für das zu besetzenden Summenarray im aufrufenden Programm.

    #include <stdio.h>

    void vektorsumme(int a[],int b[],int c[],int laenge)
      {
      /* Berechnung der Komponentensummen in einer Schleife */
      int i;
      for (i = 0; i<laenge; i++)
      c[i] = a[i] + b[i];
      }

    void printvektor(int x[],int laenge)
      {
      printf(" {");
      for (i = 0; i<laenge; i++)
        printf(" %2d ",x[i]);
      printf(" }\n");
      }

    main()
      {
      int x[5] = {1,3,5,7,9};   /* erster Summand, mit Initialisierung */
      int y[5] = {2,4,6,8,10};  /* zweiter Summand, mit Initialisierung */
      int sum[5];               /* zur Aufnahme der berechneten Summe */
      int i;

      vektorsumme(x,y,sum,5);

      printf("Die Vektorsumme von x =");
      printvektor(x,5);
      printf("                und y =");
      printvektor(y,5);
      printf("               betraegt");
      printvektor(sum,5);

      return 0;
      }

## 3.3 Verfügbarkeit und Lebensdauer von Namen

Ein Programm besitzt eine Struktur. Für Namen (Variablen, Konstante, Funktionen) im Programm sind folgende beiden Gesichtspunkte von Wichtigkeit:

- Verfügbarkeit (Scope):
  Bereiche eines Programmes, in denen auf ein Objekt (Variable oder Funktion) zugegriffen werden kann. C unterscheidet vier Verfügbarkeitsbereiche:
  - Programm
  - Modul (Datei)
  - Funktion
  - Block (Verbundanweisung, { ... })

  Objekte, die im Programm (nicht in der Funktion `main()`!) oder einem Modul definiert werden, sind globale Objekte. Funktionen sind in C daher immer global.

  Objekte, die innerhalb eines Blocks oder einer Funktion definiert werden, sind lokale Objekte. Loakale Objekte können nur Variable sein.

- Lebensdauer (Duration):
  Dauer der Zuordnung von Speicherplatz zu einem Objekt. Man unterscheidet:
  - Gesamte Programmlaufzeit (static duration):
    Der Speicherbereich wird dem Objekt zu Programmbeginn zugeordnet. Die Zuordnung wird erst mit dem Programmende wieder aufgehoben.
  - Ausführungszeit des Blockes, in dem die Definition des Objektes erfolgte (automatic duration).
    Dem Objekt wird mit Eintritt des Programmes in den Block automatisch Speicher zugewiesen. Mit Verlassen des Blockes wird diese Speicherzuweisung wieder aufgehoben.

Der Gültigkeitsbereich eines Namens, der innerhalb eines Funktion vereinbart wurde ist demzufolge auf diese Funktion begrenzt. Dies gilt in gleicher Weise auch für die Parameter der Funktion. Die beschränkte Verfügbarkeit von Namen bietet einige Vorteile:

- Variable und Konstante können innerhalb der Funktion einen bestimmten Wert erhalten.
- Namen, die lokal vereinbart wurden, sind außerhalb der Funktion unbekannt. Somit können verschiedene Funktionen Variablen gleichen Namens verwenden, ohne daß ein Konflikt entsteht (Unabhängiges Erstellen von Funktionen im Team möglich).
- Globale Variablen können von allen Funktionen gleichermaßen genutzt werden.
- Globale Variablen werden durch lokale Variablen gleichen Namens "überdeckt".

Durch den letzten Punkt ist eine Aussage über den Gültigkeitsbereich von Namen etwas komplizierter. Eine Variable, die im Hauptprogramm vereinbart wurde, "lebt" sicher für die gesamte Programmlaufzeit. Ist jedoch in einem Funktion eine Variable gleichen Namens definiert, so kann die globale Variable innerhalb der Funktion nicht angesprochen werden - sie wird gewissermaßen ausgeblendet. Es gilt also die Regel, daß lokale Vereinbarungen die globalen überdecken. Eine globale Variable besitzt zwar eine Lebensdauer, die vom Programmbegin bis zum Programmende reicht, der Gültigkeitsbereich kann hingegen "Löcher" haben.

Das folgende (völlig sinnlose) Programm zeigt beispielhaft den Gültigkeitsbereich von Namen. Die Linien auf der rechten Seite geben den Gültigkeitsbereich der Variablen an. Ist die Linie durchgezogen (|), ist der entsprechende Name gültig. Ist die Linie gepunktet (:), ist der Name von einer lokalen Variablen überdeckt. Fehlt die Linie ganz, ist die entsprechende Variable unbekannt.

    /*  Demoprogramm */                             Namen des HP  Namen des UP
    int a, b, c, d;  /* globale Var.*/              a  b  c  d    b  c  e
                                                    |  |  |  |
    void ausblende(void)                            |  |  |  |
      {                                             |  |  |  |
      int b, c, e;                                  |  :  :  |    |  |  |
      printf("Funktion-Anfang: %d %d %d %d %d\n",   |  :  :  |    |  |  |
                                  a, b, c, d, e);   |  :  :  |    |  |  |
      a = 10;                                       |  :  :  |    |  |  |
      b = 20;                                       |  :  :  |    |  |  |
      c = 30;                                       |  :  :  |    |  |  |
      d = 40;                                       |  :  :  |    |  |  |
      e = 50;                                       |  :  :  |    |  |  |
      printf("Funktion-Ende: %d %d %d %d %d\n",     |  :  :  |    |  |  |
                              a, b, c, d, e);       |  :  :  |    |  |  |
      }                                             |  :  :  |    |  |  |
                                                    |  |  |  |
    void main(void)                                 |  |  |  |
      {                                             |  |  |  |
      a = 1;                                        |  |  |  |
      b = 2;                                        |  |  |  |
      c = 3;                                        |  |  |  |
      d = 4;                                        |  |  |  |
      printf("HP-Anfang: %d %d %d %d\n",            |  |  |  |
                            a, b, c, d);	        |  |  |  |
      ausblende();                                  |  |  |  |
      printf("HP-Ende: %d %d %d %d\n",              |  |  |  |
                          a, b, c, d);              |  |  |  |
      }                                             |  |  |  |

Startet man das Programm, sieht die Ausgabe folgendermaßen aus ("??" markiert Variable, die noch keinen Wert haben):

Variable: | a | b | c | d | e
--- | --- | --- | --- | --- | ---
HP Anfang: | 1 | 2 | 3 | 4 |
Funktion Anfang: | 1 | ?? | ?? | 4 | ??
Funktion Ende: | 10 | 20 | 30 | 40 | 50
HP Ende: | 10 | 2 | 3 | 40 |

Man sieht ganz deutlich, daß die Zuweisungen an die lokalen Variablen b und c innerhalb der Funktion keine Auswirkung auf die globalen Variablen gleichen Namens haben. Hingegen werden die globalen Variablen a und d durch die Zuweisung im Funktion verändert. Die Variable e ist nur innerhalb der Funktion gültig.

Funktionen sollten immer so geschrieben werden, daß man sich bei deren Verwendung nicht um Interna kümmern muß, sie also als "black box" ansehen kann. Dies ist ein weiteres Argument gegen die Verwendung von globalen Variablen.

Globale Variable werden typischerweise verwendet, um Größen, die in vielen Funktionen in einem Programm(-Projekt) bedeutsam sind, mitzuteilen, ohne sie ständig als Variablen übergeben zu müssen. Um sicherzustellen, daß diese nicht irgendwo versehentlich geändert werden, kann man sie vorteilhaft als const deklarieren.

Merke: Globale Objekte - also auch alle Funktionen - "leben" grundsätzlich während der gesamten Programmlaufzeit. Lokale Objekte (Variable) besitzen eine Lebensdauer, die auf die Blockausführungszeit begrenzt ist, sie können aber auch für die gesamte Programmlaufzeit vereinbart werden (siehe Speicherklassen).

## 3.4 Rekursion und Iteration

### Rekursion

Mit Rekursion wird der Fall bezeichnet, wenn ein Aufruf eines Funktion innerhalb des Anweisungsteils der Funktion selbst erfolgt (Funktion "ruft sich selbst" auf). Durch rekursive Algorithmen sind oft überraschend kurze und elegante Lösungen möglich. Der Grundgedanke besteht dabei darin, durch den rekursiven Aufruf das gegebene Problem solange zu verkleinern, bzw. in identische kleinere Teilprobleme aufzuteilen, bis ein Trivialfall (Abbruchsbedingung) erreicht wird.

Rekursion: Ein rekursiver Algorithmus enthält zunächst noch ungelöste Probleme, zu deren Lösung man denselben Algorithmus nochmals anwenden muß.

Eine Beschreibung eines rekursiven Algorithmus liegt also dann vor, wenn in einer elementaren Anweisung ein Verweis auf den eigenen Algorithmus erfolgt. (Man sagt auch: "Der Algorithmus ruft sich selbst auf.")

Dabei wird der Code der Funktion mehrfach rekursiv durchlaufen, d. h. der Code ist nach wie vor nur einmal vorhanden, aber er wird mehrfach mit unterschiedlichen Werten durchlaufen. Möglich ist das nur durch eine besondere Eigenschaft der Funktions-Codes, die als "Wiedereintrittsfähigkeit" bezeichnet wird.

Um das zu erreichen, dürfen die lokalen Variablen und Parameter nicht unter einer festen Speicheradresse gespeichert werden, sondern es muß ihnen bei jedem Aufruf der Funktion ein neuer Speicherplatz zugewiesen werden ("dynamische" Speicherverwaltung). Dazu wird in der Regel ein Stack verwendet, der meist per Software realisiert wird. Beim Funktions-Aufruf werden - soweit vorhanden - die Parameter auf den Stack gerettet und für die lokalen Variablen Platz auf den Stack reserviert (die Rücksprungadresse befindet sich, ebenfalls auf dem Stack). Bei Werteparametern wird der Wert selbst gespeichert, bei Variablenparametern die Adresse der Variablen.

Mit jedem rekursiven Aufruf wächst also der Speicherbedarf des Stack. Da irgendwann jeder Speicher aufgebraucht ist, muß die rekursive Aufruffolge irgendwann durch ein sogenanntes Abbruch-Kriterium unterbrochen werden. Das führt uns zu einer generellen Analyse eines rekursiven Funktion. Der Anweisungsteil eines rekursiven Funktion besteht aus:

- Test eines oder mehrerer Parameter oder globaler Variablen auf Erfülltsein des Abbruchkriteriums.
- Falls das Abbruchkriterium nicht erfüllt ist, erfolgt der Aufruf der Funktion mit mindestens einem modifizierten Parameter (damit das Abbruchkriterium erreicht wird).

Ein ähnliches Verfahren ist übrigens aus der Mathematik bekannt, der Beweis eines Lehrsatzes durch vollständige Induktion. Rekursion ist nicht an eine bestimmte Programmiersprache gebunden, sie ist auch auf Ebene des Maschinencodes möglich (der Stack muß dann gegebenenfalls zusätzlich realisiert werden).

Anmerkung: Bei der Formulierung von rekursiven Algorithmen besteht leicht die Gefahr, die Bedingung der Finitheit zu verletzen.

Rekursive Algorithmen ermöglichen zum Teil sehr elegante Programmierung. Die Stackoperationen beim wiederholten Aufruf des Funktion machen die Ausführung aber langsamer. Grundsätzlich ist jeder rekursive Algorithmus auch durch Wiederholungsanweisungen zu realisieren. Problem: Nachweis, daß Rekursion vor einem Stack-Überlauf abbricht.

### Beispiel: Zahlenumwandlung Dezimal-Binär

Zunächst die nicht-rekursive Variante.

Programm in C-Notation:

    #include <stdio.h>
    /* Programm zur Umwandlung von Dezimalzahlen in Binärzahlen */
    void main(void)
      {
      int dez, i;
      int dual[8];

      for(i = 0; i < 8; i++)
        dual[i] = 0;                      /* Dualzahl löschen */
      scanf("%d", &dez);
      i = 0;
      if ((dez >= 0) && (dez <= 255))    /* Wert zulässig? */
        do
          {                               /* sukzessive Division */
          dual[i] = dez % 2;              /* aktuelle Dualstelle speichern */
          dez = dez/2;                    /* abdividieren */
          i++;
          } while(dez > 0);
      for(i = 7; i > -1; i--)
        printf("%1d", dual[i]);           /* Ausgabe Dualstellen */
      printf("\n");                       /* neue Zeile */
      }

### Zahlenumwandlung Dezimal-Binär rekursiv

Formalisierung des Problems:
d sei die zu konvertierende Dezimalzahl.

- Falls d > 1 ist, wende den Algorithmus auf den Wert (d div 2) an.
- Gib die letzte Stelle der Dualzahl (d mod 2) aus.

Damit ist die Berechnung auf einen Induktionsschluß von n dualen Stellen auf n-1 duale Stellen zurückgeführt. Die Umkehrung der Ausgabereihenfolge erfolgt implizit durch die Rekursion.

Beispiel: Aufrufstruktur der Binärumwandlung der Dezimalzahl 13. Senkrecht untereinander stehende Kästchen (durch gestrichelte Linien verbunden) bezeichnen dieselbe "Inkarnation" der Funktion.

Vorteil: Es muß keine Array-Variable für die Speicherung der Dualstellen vereinbart werden. Der Wertebereich ist lediglich durch die Größe des Aufruf- und Parameter-Stacks begrenzt.

Nachteil: Durch die rekursiven Aufrufe erhöhen sich Speicherbedarf und Rechenzeit.

Programm in C-Notation:

    #include <stdio.h>
    /* dezimal-dual Wandlung */

    void bin(int dezimal)
      {
    	if (dezimal > 1)         /* Abbruchbedingung */
    	  bin(dezimal / 2);         /* rekursiver Aufruf */
    	printf("%1d", dezimal % 2);	/* Ausgabe i-te Stelle */
      }

    void main(void)
      {
    	int dez;
    	scanf("%d ", &dez);  /* Eingabe */
    	bin(dez);                /* Funktionsaufruf */
    	printf("\n");            /* neue Zeile */
      }

Zum Testen wurde die FUnktion bin um zwei AUsgaben erweitert:

    void bin(int dezimal)
      {
        printf("Aufruf von bin(%d)\n",dezimal);
    	if (dezimal > 1)           /* Abbruchbedingung */
    	  bin(dezimal / 2);           /* rekursiver Aufruf */
    	printf("Ruecksprung von bin: %1d\n", dezimal % 2);	/* Ausgabe i-te Stelle */
      }

Für die Eingabe 179 liefert `bin()` dann folgende Ausgabe (zur besseren Erkennbarkeit sind die Zeilen entsprechend der Aufrufverschachtelung eingerückt):

    Aufruf von bin(179)
      Aufruf von bin(89)
        Aufruf von bin(44)
          Aufruf von bin(22)
            Aufruf von bin(11)
              Aufruf von bin(5)
                Aufruf von bin(2)
                  Aufruf von bin(1)
                  Ruecksprung von bin: 1
                Ruecksprung von bin: 0
              Ruecksprung von bin: 1
            Ruecksprung von bin: 1
          Ruecksprung von bin: 0
        Ruecksprung von bin: 0
      Ruecksprung von bin: 1
    Ruecksprung von bin: 1

Weiteres Beispiel:
Beim Spiel Türme von Hanoi hat man n unterschiedlich große Lochscheiben, die sich auf drei möglichen Ablageplätzen befinden können. In der Ausgangssituation befinden sich alle Scheiben als Turm der Größe nach geordnet auf einem Platz (die größte Scheibe unten, die kleinste Scheibe oben).
Das Problem besteht darin, den Turm auf einen bestimmten anderen Platz zu bringen, wobei

- immer nur eine Scheibe bewegt werden und
- niemals eine grössere Scheibe auf einer kleineren liegen darf.

Zur Lösung geht man wie folgt vor:

- zuerst werden n-1 Scheiben "irgendwie" auf den übrigen dritten Platz gebracht (--> das verkleinerte Problem),
- dann bringt man die unterste Scheibe auf den Endplatz (Trivialfall),
- danach werden auch die n-1 Scheiben auf den Endplatz gebracht (ebenfalls verkleinertes Problem).

Kaum ein anderes Beispiel demonstriert so eindrucksvoll die Eleganz von rekursiven Algorithmen:

    /* hanoi : Zugfolge fuer n Scheiben von Platz p1 nach Platz p2 */
    void  hanoi(int n, int p1, int p2)
      {
      int parkplatz;

      if(n > 1)
        {
        /* n-1 Scheiben auf Parkplatz */
        parkplatz = 6-p1-p2;
        hanoi(n-1, p1, parkplatz);
        }
      /* unterste Scheibe auf Endplatz */
      printf("Zug von %d nach %d\n", p1, p2);
      if(n > 1)
        /* n-1 Scheiben auf Endplatz */
        hanoi(n-1, parkplatz, p2);
      }

Die Rekursivität kann auch über mehrere Aufrufebenen erfolgen. Ein Algorithmus A verweist z. B. in einer elementaren Anweisung auf den Algorithmus B, und dieser verweist wieder auf den Algorithmus A.

### Iteration

Ein iterativer Algorithmus enthält eine Wiederholung in der, ausgehend von bekannter Information, Zwischenergebnisse erzeugt werden, die wiederum die Basis für die Zwischenergebnisse im nächsten Wiederholungs- (Iterations-) Schritt bilden.

Iterative Algorithmen treten in der Datenverarbeitung sehr häufig auf. Man kann grundsätzlich 2 Arten von Iterationen unterscheiden:

- Algorithmen mit einer vorher bekannten Anzahl von Wiederholungsschritten. Beispiel: Berechnung von an:

  an = a * a * ... *a * a

  mit n Iterationsschritten.

- Algorithmen mit einer vorher unbekannten Anzahl von Wiederholungsschritten. Beispiel: Suche nach der größten 2-er Potenz kleiner/gleich M:

  Gegeben ist eine natürliche Zahl M. Gesucht ist eine natürliche Zahl k, für die gilt: 2k+1 > M >= 2k

Beispiel für eine iterative Berechnung, die näherungsweise Wurzel-Berechnung:

    #include
    #include
    void main()
    {
    float a, eps, x, y;
    printf("Näherungsweise Wurzelberechnung");
    printf("\nWert a: ");scanf("%f",&a);
    printf("Grenze: ");scanf("%f",&eps);
    y=a/2;
    do
      {
      x = y;
      y = (x + a/x)/2;
      }
      while (fabs(y-x) > eps);
    printf("Ergebnis:%f", y);
    }

In der Mathematik werden häufig rekursive Definitionen benutzt, die aber bei der Umsetzung in einen Berechnungsalgorithmus sowohl als Iteration als auch als Rekursion verwirklicht werden können. Ein Beispiel haben wir schon bei der Dezimal-Binär-Umwandlung gesehen.

Anmerkung: Die rekursive Formulierung eines Algorithmus ist meist kürzer und daher übersichtlicher, aber wegen der unzulänglichen Implementierung von rekursiven Verfahren sollten für Datenverarbeitungsanlagen wenn möglich immer iterative Verfahren gewählt werden.

Nicht immer kann zu einer rekursiven Definition sehr leicht ein iterativer Berechnungsalgorithmus gefunden werden. Beispiel: Berechnung der Ackermann-Funktion A(n,m)

              A(n,m) = A(n-1,A(n,m-1))
                      mit
              A(n,0) = A(n-1,1)
                      und
              A(0,m) = m + 1

## 3.5 Der exit-Status

Wenn ein Programm seinen Ablauf beendet hat sollte es dem System mitteilen, auf welche Weise ("alles OK", "Fehler aufgetreten", etc.) diese Beendigung eingetreten ist. Dazu dient der exit-Status. Der exit-Status ist eine ganze Zahl, die im System unmittelbar nach Programmbeendigung in irgendeiner Form zurückbleibt. Die Beachtung des exit-Status wird spätestens dann wichtig, wenn Programme sich gegenseitig aufrufen und abhängig vom Ausgang Entscheidungen treffen müssen.

Der exit-Status eines Programms ist der Rückgabewert der Funktion `main`, also i.A. der Wert der in `main` mit `return` zurückgeliefert wird. Zur sofortigen Beendigung eines Programms bei einem kritischen Fehler - wenn etwa eine Eingabedatei nicht geöffnet werden konnte - und zum definierten Setzen des exit-Status verwendet man die Bibliotheksfunktion

`void exit(int status);`

Nach Konvention bedeutet der Wert 0, daß alles OK ist, während eine von 0 verschiedene Zahl einen Fehlerindikator darstellen kann.

Ein Beispiel:

    int  main(void)
      {
      ...
      if(fehler)
        exit(1);
      ...
      return 0;
      }

## 3.6 Endlichkeit von Programmen

Die Rekursion und die Schleifenstruktur sind ein Charakteristikum von Computer-Programmen, denn sie spezifiziert die Repetition einer Handlung, wozu sich Automaten besonders gut eignen. Ihre Eigenschaft, selbst nach tausenden von Wiederholungen derselben Handlung weder zu ermüden noch in der Zuverlässigkeit und Exaktheit nachzulassen, ist besonders wertvoll. Andererseits ist gerade diese Unermüdlichkeit des Computers ein Grund für erhöhte Aufmerksamkeit auf der Seite des Programmierers. Er hat dafür zu sorgen, daß alle Prozesse, die nach einem bestimmten Programm ablaufen können, nach einer endlichen Anzahl von Wiederholungen terminieren. Er muß die Endlichkeit des Programms garantieren können. Leider ist in der Praxis das Nichtterminieren eines Prozesses (genannt "Hängenbleiben" des Computers) ein ebenso häufiges wie kostspieliges Vorkommnis. Es kann mit der nötigen Gewissenhaftigkeit beim Programmieren verhältnismäßig leicht vermieden werden. Die dazu nötigen Vorsichtsmaßregeln sollen anhand des allgemeinen Strukturschemas erläutert werden.

Offensichtlich ist es eine Minimalforderung, daß durch die Anweisung A (mindestens) eine Variable derart verändert wird, daß nach einer endlichen Anzahl von Durchläufen die Bedingung B nicht mehr erfüllt ist. Allgemein kann die Endlichkeit einer Repetition folgendermaßen hergeleitet werden: Es wird eine ganzzahlige Größe N in Abhängigkeit von Variablen des Programms postuliert und gezeigt,

1. daß N > 0 gilt, falls B erfüllt ist, und
2. daß der Wert von N durch die Ausführung von A stets abnimmt.

Die Anwendung dieser Regel sei am Beispiel des folgenden Programms erläutert.

    #include <stdio.h>
    /* Groesster gemeinsamer Teiler */

    int a, b;

    void main(void)
      {
      scanf("%d %d", &a, &b);   /* Eingabe */
      while (a != b)
        {
        if (a > b)
          a = a - b;
        else
          b = b - a;
        }
      printf("GGT: %d\n",a);	/* Ausgabe GGT = a = b */
      }

Die Anweisung A lautet

> a = a - b wenn a > b
> b = b - a wenn b > a

und die Bedingung B heißt a = b. Dabei sind a und b natürliche Zahlen, mit Anfangswerten a > O, b > O und a != b. Eine zweckmäßige Wahl von N ist N = max(a,b).

Der Effekt von A auf N muß in zwei gesonderten Fällen betrachtet werden. Ist a > b, so bleibt b unverändert, und der Wert von a wird um b erniedrigt. Da anfänglich a > O, b > O und a != b, bleiben die ersten Beziehungen erhalten, und N nimmt ab. Ist b > a, so bleibt a unverändert, b = max(a,b) = N nimmt um den Betrag a ab, und die Beziehungen a > 0 und b > 0 bleiben erhalten. Da also N = max(a,b) stets abnimmt, anderseits aber min(a,b) positiv bleibt, muß nach einer endlichen Anzahl von Durchläufen max(a,b) = min(a,b) werden, also a != b nicht mehr erfüllt sein. Damit ist die Endlichkeit dieses Programms erwiesen.

## 3.7 Seiteneffekte

Als Seiteneffekt werden Zuweisungen an globale Variable bezeichnet. Solche Effekte tragen meist zur Verschleierung der Programmstruktur bei und erschweren gewöhnlich die Verifikation und Änderbarkeit des Programms. So sind sie oft ein Quell versteckter und seltsamer Fehler. Zur Abschreckung mögen die beiden folgenden Programme dienen.

    #include <stdio.h>
    int z;

    int f (int x)
      {
      z = z - 100;         /* Seiteneffekt */
      return(x*x -1);
      }

    void main(void)
      {
      z = 100; printf("%d %d\n", f(z), z);
      z = 100; printf("%d %d\n", f(100)*f(z), z);
      z = 100; printf("%d %d\n", f(z)*f(100), z);
      }

Die Ausgabe zeigt, daß die Kommutativität der Multiplikation außer Kraft scheint:

    10001         0
    10001      -100
    100020001  -100

Das zweite Programm zeigt ebenfalls schlimme Effekte:

    #include <stdio.h>
    int i;

    int f (int x)
      { int h;
      h = x + i;          /* globale Variable ! */
      i = i + 10;         /* Seiteneffekt */
      return(h);
      }

    int g (int x)
      {
      return(f(i) + x);   /* globale Variable ! */
      }

    void main(void)
      {
      i = 0;                               /* 1. Aufruf mit i = 0 */
      printf("%d %d %d\n", i, f(i), i);
      printf("%d %d %d\n", i, g(i), i);
      i = 0;                               /* 2. Aufruf mit i = 0, */
      printf("%d %d %d\n", i, f(10), i);   /* aber der Konstanten 10 */
      printf("%d %d %d\n", i, g(10), i);
      }

Es ergibt sich folgende Ausgabe:

     0    0   10
    10   30   20
     0   10   10
    10   30   20

Dazu noch ein sinvolleres Beispiel. Eine statische Variable ist lokal in einer Funktion vereinbart. Sie wird jedoch beim Verlassen der Funktion nicht gelöscht, sondern bleibt bestehen, so daß beim nächsten Funktionsaufruf ihr Wert noch zur Verfügung steht. Statische Variablen werden automatisch mit 0 initialisiert.

    #include <stdio.h>

    int zaehler()
      {
      static int count;
      count++;
      return count;
      }

    main()
      {
     /* Das Hauptrogramm ruft die Funktion dreimal auf und gibt dann
        jeweils den aktuellen Wert der statischen Varaiablen aus. */

      printf("%d\n",zaehler());  /* Ausgabe: 1 */
      printf("%d\n",zaehler());  /* Ausgabe: 2 */
      printf("%d\n",zaehler());  /* Ausgabe: 3 */
      }

Alternativ könnte man eine globale Variable verwenden. Die Variable "count" im folgenden Beispiel wird außerhalb der Funktionen deklariert. Sie ist damit eine globale Variable, die von allen Funktionen benutzt werden kann.

    #include <stdio.h>

    int count = 0; /* GLOBAL */

    void fun_1(void)
      { count++; }

    void fun_2(void)
      { count++; }

    main()
      {
      count++; /* zaehlt den Aufruf von main() */
      fun_1();
      fun_2();
      printf("Anzahl durchlaufener Funktionen: %d\n",count);
      }

## 3.8 Kommandozeilenparameter

Unter Verwendung von Kommandozeilen-Parameter versteht man die Übergabe von Argumenten an die Funktion `main()`. Normalerweise sieht das so aus:

`int main(int argc, char *argv[])`

Dabei gibt `argc` die Zahl der Argumente an, und `argv` ist ein Array, in dem diese Argumente in Form von Strings vorliegen. An dieser Stelle kommt Ihnen die Angabe `*argv[]` wahrscheinlich seltsam vor, due Hintergründe werden im Kapitel über Pointer erlätert. Da sich aber `argv` wie ein Array behandeln lä&stzlig;, soll das hier nicht weiter stören. (Die Bezeichnungen `argc` und `argv` sind Konvention, aber syntaktisch aber nicht vorgeschrieben.) Beispiel: ein Programm hei&ßt foo und wird aufgerufen mit

`foo myfile 1 3.8`

dann sind die Argumente von `main` folgenderma&ßen belegt:

`argc` | : | 4 (Zahl der Argumente, und zwar:)
`argv[0]` | : | `"/...`<Pfad>`.../foo"`
`argv[1]` | : | `"myfile"`
`argv[2]` | : | `"1"`
`argv[3]` | : | `"3.8"`
`argv[4]` | : | `NULL`

Um Strings in Zahlen zu konvertieren, stehen die z.B. die Funktionen `int atoi(char*)` und `double atof(char*)` (deklariert in `<stdlib.h>`) zur Verfügung.
Ein Programmbeispiel:

    #include <stdio.h>

    int  main(int argc, char *argv[])
      {
      int  i;
      double  df;

      if(argc != 3)
        {
        printf("usage: %s <i>  <df>\n", argv[0]);
        return 1;
        }
      i  = atoi(argv[1]);
      df = atof(argv[2]);
      printf("  i:  %d      df:  %f\n", i, df);
      return 0;
      }

## 3.9 Ein- und Ausgabefunktionen

Die Sprache C selbst enthält keine Elemente zur Programm-Ein- und Ausgabe. Vielmehr muß jegliche Programm-Ein- und Ausgabe mittels Funktionen realisiert werden. Daher sind in der zu jedem C-System gehörenden Standardbibliothek eine Reihe entsprechender Funktionen enthalten.

Durch die Normung der Standardbibliothek (ANSI-C-Bibliothek) ist bei Anwendung dieser Funktionen eine weitgehende Portabilität gewährleistet. Allerdings existieren in vielen C-Bibliotheken neben den in der ANSI-Norm festgelegten Standard-Funktionen weitere, nicht genormte Funktionen, deren Anwendung allerdings die Portabilität herabsetzt.

Jegliche Ein- und Ausgabe in C geschieht über die Datei-Schnittstelle. Geräte, wie z. B. der Drucker oder eben die Konsole werden dabei auch als Dateien behandelt. In der Standardbibliothek sind einige Geräten zugeordnete Standard-Dateien definiert :

- `stdin` (Standard-Eingabe, Tastatur)
- `stdout` (Standard-Ausgabe, Bildschirm)
- `stderr` (Standard-Fehlerausgabe, Bildschirm)

Unter Bezugnahme auf die Standard-Dateien stdin und stdout kann somit eine Ein-/Ausgabe über die Konsole mit Hilfe der allgemeinen Dateibearbeitungs-Funktionen realisiert werden. Es existieren darüber hinaus aber für die Ein-und Ausgabe über die Konsole spezielle relativ einfach anwendbare Funktionen :

- Funktionen zur zeichenweisen Ein- und Ausgabe (einschließlich Funktionen zur Ein- und Ausgabe von Zeichenketten)
- Funktionen zur formatierten Ein- und Ausgabe

Anmerkung : Mit Hilfe der in einigen Betriebssystemen (z.B. MS-DOS, UNIX) realisierten Umleitung der Standard-Eingabe und Standard-Ausgabe auf der Kommando-Ebene kann mit diesen Funktionen auch eine Bearbeitung anderer Dateien realisiert werden.

Mit den Ein-/Ausgabefunktionen der Standard-Bibliothek eng verknüpft ist die Header-Datei

`stdio.h`

In ihr sind die für die Anwendung der Funktionen benötigten Funktionsdeklarationen (Function Prototypes) sowie einige Typen und Konstante (Makros), die mit der Realisierung und Anwendung der Funktionen in Zusammenhang stehen, definiert. U.a. wird die Konstante `EOF` definiert, die zur C-internen Kennzeichnung des Dateiendes dient. Der Wert dieser int-Konstanten ist nicht mit dem Wert eines eventuellen im Betriebssystem verwendeten Dateiende-Zeichens (z.B. CTRL-D unter UNIX, CTRL-Z unter DOS) identisch, sondern beträgt i. a. -1.

Zur problemlosen und einfachen Anwendung der Standardbibliotheks-Funktionen ist es daher zweckmäßig die Header-Datei stdio.h mittels

`#include <stdio.h>`

in das C-Programm-Modul einzubinden.

### Die Ausgabefunktion printf

`int printf(controlstring, arg1, arg2, ... )`

Das folgende Programm gleicht dem ersten C-Beispiel:

    #include

    void main(void)
      {
      printf("Hallo Welt\n");
      }

Die Ausgabe dieses Programms ist

    Hallo Welt

Dahinter kommt ein Zeilenvorschub (\n). Das erste Argument von printf ist ein Formatstring - ein String der das Ausgabeformat beschreibt. Entsprechend den C-Konventionen muß der String mit einem NUL-Zeichen (\0) abgeschlossen sein. Wenn der String als Konstante geschrieben wird, ist automatisch garantiert, daß er richtig abgeschlossen ist.

Die printf-Funktion kopiert die Zeichen aus dem Format auf die Standardausgabe, bis entweder das Ende des Strings oder ein %-Zeichen erreicht wird. Anstatt das im Format gefundene %-Zeichen auszugeben, sucht printf nach weiteren Zeichen hinter dem %-Zeichen, um herauszufinden, wie das nächste Argument umgewandelt werden soll. Das umgewandelte Argument wird anstelle des %-Zeichens und der nächsten paar Zeichen ausgegeben. Da das Format im obigen Beispiel kein %-Zeichen enthält, entspricht die Ausgabe genau den im Format angegebenen Zeichen. Das Format legt zusammen mit den entsprechenden Argumenten jedes einzelne Zeichen in der Ausgabe fest. Dazu gehört auch der Zeilenvorschub, mit dem eine Zeile abgeschlossen wird.

Der Rückgabewert von printf ist die Anzahl der ausgegebenen Zeichen.

Die printf-Funktion hat zwei verwandte Funktionen: fprintf und sprintf. Während printf auf die Standardausgabe schreibt, kann fprintf nur auf eine Ausgabedatei schreiben. Die entsprechende Datei muß in der fprintf-Funktion als erstes Argument angegeben werden. Daher bedeuten `printf (Ausgabe);` und `fprintf(stdout, Ausgabe);` exakt dasselbe.

Die Funktion sprintf wird eingesetzt, wenn die Ausgabe nicht in eine Datei erfolgen soll. Das erste Argument von sprintf ist die Adresse eines Zeichenarrays, in dem sprintf seine Ausgabe ablegt. Daß das Array groß genug ist, um die von sprintf erzeugt Ausgabe aufzunehmen, liegt in der Verantwortlichkeit des Programmierers. Die weiteren Argumente sind identisch mit printf. Die Ausgabe von sprintf wird immer mit einem NUL-Zeichen abgeschlossen. Die einzige Möglichkeit, um ein NUL-Zeichen auf andere Art und Weise auszugeben, ist die Verwendung des Formats %c.

Alle drei Funktionen liefern als Ergebnis die Anzahl der übertragenen Zeichen zurück. Im Fall von sprintf wird das NUL-Zeichen am Ende der Ausgabe nicht mitgezählt. Wenn printf oder fprintf während des Schreibens auf einen Ein-/Ausgabefehler treffen, geben Sie einen negativen Wert zurück. In diesem Fall kann man nicht mehr feststellen, wieviele Zeichen geschrieben wurden. Da sprintf keine Ein/Ausgabe durchführt, sollte niemals ein negativer Wert zurückgegeben werden.

Da der Formatstring die Datentypen der weiteren Argumente festlegt und dieser Formatstring während der Ausführung erstellt werden kann, ist es für eine C-Implementierung sehr schwer festzustellen, ob die Argumente von printf die richtigen Datentypen enthalten. Wenn man also `printf("%d\n", 0.1);` schreibt oder `printf (%g\n", 2);` dann erhält man nur Unsinn. Es ist aber äußerst unwahrscheinlich, daß dies vor dem Programmstart entdeckt werden kann. Den meisten Implementierungen entgeht auch eine Anweisung wie `fprintf("error\n");` Der Programmierer hat hier fprintf verwendet, weil er eine Meldung auf stderr ausgeben wollte, hat aber vergessen, stderr anzugeben. Wahrscheinlich wird das Programm abstürzen, da fprintf den Formatstring als Dateistruktur interpretiert.

#### Einfache Formatangaben

Jedes Formatelement wird mit einem %-Zeichen eingeleitet, hinter dem wenn auch manchmal nicht sofort - ein Zeichen folgt, das als Formatcode bezeichnet wird, mit dem die Art und Weise der Umwandlung bestimmt wird. Andere Zeichen können wahlweise zwischen dem %-Zeichen und dem Formatcode angegeben werden. Diese Zeichen dienen zur näheren Spezifikation des Ausgabeformats und werden später noch ausführlich erläutert. Das häufigste Format ist sicherlich %d, das einen Integerwert in Dezimalschreibweise ausgibt. Zum Beispiel ergibt

`printf ("2 + 2 = %d\n", 2 + 2)`

die Ausgabe `2 + 2 = 4` hinter der ein Zeilenvorschub folgt.

Das Format %d ist eine Anforderung, daß ein Integer ausgegeben werden soll. Es muß daher ein entsprechendes int-Argument vorliegen. Der Dezimalwert des Integer ersetzt das Format %d ohne vorangestellte oder nachfolgende Nullen während der Kopie auf die Ausgabe. Wenn der Integer negativ ist, wird als erstes Zeichen ein Minuszeichen ausgegeben.

Das Format %u verarbeitet einen Integer so, als wäre er unsigned. Deshalb ergibt `printf("%u\n", -37);` auf einer Maschine mit 32-Bit-Integern die Ausgabe `4292967259`.

Beachten Sie, daß char- und short-Argumente automatisch zu einem int erweitert werden. Das kann auf Maschinen, bei denen char-Werte als signed verarbeitet werden, zu einigen Überraschungen führen. Um diese Probleme zu vermeiden, sollten Sie das Formatelement %u für uns igned-Werte reservieren.

Die Formatelemente %o, %x und %X geben Integerwerte mit der Basis 8 oder 16 aus. Das Element %o liefert oktale Ausgaben, während die Elemente %x und %X hexadezimale Ausgaben erzeugen. Der einzige Unterschied zwischen %x und %X ist:

- %x liefert die Buchstaben a, b, c, d, e und f für die Werte der Ziffern 10 bis 15.
- %X verwendet die Buchstaben A, B, C, D, E und F

Oktale und hexadezimale Werte sind immer vorzeichenlos. Zum Beispiel gibt

    int n = 108;
    printf("%d dezimal = %o oktal = %x hexadezimal\n", n, n, n);

die Ausgabe

    108 dezimal = 154 oktal = 6c hexadezimal

Das Formatelement %s dient zur Ausgabe von Strings: Das entsprechende Argument muß Die Adresse eines Strings sein. Die Zeichen werden ab der Stelle, die vom Argument adressiert wird, bis zum ersten erkannten NUL-Zeichen ('\0') ausgegeben. Ein String mit einem %s-Formatelement muß mit einem '\0'-Zeichen abgeschlossen sein. Dies ist die einzige Möglichkeit, damit printf das Ende das Strings erkennen kann. Wenn ein String, der an ein %s-Element übergeben wird, nicht richtig abgeschlossen ist, dann wird printf die Ausgabe solange fortsetzen, bis es irgendwo im Speicher ein '\0'-Zeichen findet. Die Ausgabe kann dann wirklich sehr lang werden.

Da das Formatelement % s jedes Zeichen im entsprechenden Argument ausgibt, bedeuten `printf(s)` und `printf ("%s", s)` nicht dasselbe. Das erste Beispiel behandelt jedes %-Zeichen in s als den Anfang eines Formatcodes. Das kann zu Problemen führen, wenn ein anderer Formatcode als %% vorkommt, da dann das entsprechende Argument fehlt. Das zweite Beispiel gibt jeden mit NUL abgeschlossenen String aus.

Das Formatelement %c gibt ein einzelnes Zeichen aus: `printf ("%c", c)` entspricht `putchar(c)`, hat aber den Vorteil, daß man den Wert von c auch in einem größeren Zusammenhang ausgeben kann. Das Argument, das bei einem %c Formatelement angegeben werden muß, ist ein int, der bei der Ausgabe in einen char umgewandelt wir. Zum Beispiel ergibt

`printf("Der Dezimalwert von '%c' ist %d\n", '*', '*');`

die Ausgabe `Der Dezimalwert von '*'ist 42`

Drei Formatelemente stehen für die Ausgabe von Fließkommazahlen zur Verfügung: %g, %f und %e. Das Formatelement %g ist am nützlichsten, wenn man Fließkommazahlen darstellen will, die nicht in Spalten ausgegeben werden müssen. Damit wird der entsprechende Wert (der unbedingt ein float oder double sein muß) ohne nachfolgende Nullen mit bis zu sechs signifikanten Ziffern ausgegeben. Zusammen mit `math.h` ergibt

`printf("Pi = %g\n", 4 * atan(l.0));`

die Ausgabe `Pi = 3.14159`. Führende Nullen werden in der Genauigkeit nicht berücksichtigt. Die Werte werden nicht abgeschnitten, sondern gerundet: `printf("%g\n", 2.0 / 3.0);` liefert die Ausgabe `0.666667`. Wenn die Zahl größer als 999999 ist, dann würde der Wert entweder mit mehr als sechs signifikanten Stellen oder falsch Wert ausgeben. Das Formatelement %g löst dieses Problem, indem solche Werte in der wissenschaftlichen Schreibweise ausgegeben werden:

`printf("%g\n", 123456789.0);`

liefert die Ausgabe `1.23456e+08` Der Wert wird wiederum auf sechs signifikante Stellen gerundet. Wenn die Größenordnung des Werts zu klein ist, wird die erforderliche Anzahl an Zeichen zur Darstellung der Werte ebenfalls sehr groß. Es ist zum Beispiel sehr unschön, wenn man PI * 10-10 als `0.00000000031459` schreibt. Sowohl kompakter als auch leichter zu lesen ist `3.14159e-10`. Diese beiden Darstellungsformen weisen genau dieselbe Länge auf, wenn der Exponent -4 ist (zum Beispiel ist `0.000314159` genauso lang wie `3.14159e-04`). Das %g-Formatelement fängt daher erst bei einem Exponenten von -5 mit der wissenschaftlichen Darstellung an.

Das Formatelement %e verwendet zur Ausgabe von Fließkommazahlen in jedem Fall einen expliziten Exponenten: n wird im %e-Format als `3.141593e+00` ausgegeben. Das Formatelement %e gibt immer sechs Ziffern hinter dem Dezimalpunkt aus, und nicht bloß sechs signifikante Ziffern.

Mit dem Formatelement % f werden Fließkommazhalne immer ohne einen Exponenten ausgegeben, so daß n als `3.14159` ausgegeben wird. Auch das %f-Format gibt sechs Ziffern hinter dem Dezimalpunkt aus. Ein sehr kleiner Wert kann demnach als Null erscheinen, auch wenn das gar nicht der Fall ist, und eine sehr große Zahl wird mit vielen Ziffern ausgegeben:

`printf("%f\n", le38);`

wird als `10000000000000000000000000000000000000.000000` ausgegeben. Da die Anzahl der hier ausgegebenen Ziffern die Genauigkeit der meisten Computer übersteigt, kann das Ergebnis auf verschiedenen Maschinen unterschiedlich sein.

Die Formatelemente % E und % G verhalten sich genauso wie die entsprechenden Formatelemente mit Kleinbuchstaben, außer daß der Exponent mit einem großen E und nicht mit einem kleinen e dargestellt wird.

Das Formatelement %% gibt ein %-Zeichen aus. Es ist insofern einzigartig, als in diesem Fall kein entsprechendes Argument angegeben werden muß. Die Anweisung

`printf("%%d gibt einen Dezimalwert aus\n");`

ergibt also die Ausgabe `%d gibt einen Dezimalwert aus`.

#### Modifizierer

Die Funktion printf verarbeitet auch noch weitere Zeichen, mit denen ein Formatelement genauer spezifiziert werden kann. Diese Zeichen werden zwischen dem %-Zeichen und dem folgenden Formatcode angegeben.

Integer gibt es in drei verschiedenen Längen: short, long und int. Wenn ein kurzer Integer als Funktionsargument angegeben wird, wird er automatisch in einen normalen Integer umgewandelt. Das gilt auch für die Funktion printf, aber für die long-Integer benötigen wir noch eine Möglichkeit, um sie zweifelsfrei anzugeben. Dies erreicht man durch ein l direkt vor dem Formatcode, so daß sich dann %ld, %lo, %lx und %lu als neue Formatcodes ergeben. Diese modifizierten Codes verhalten sich dann für long wie ihre nicht modifizierten Entsprechungen.

Der Modifizierer für die Ausgabebreite vereinfacht die Ausgabe von Werten in Feldern fester Länge. Er wird zwischen dem %-Zeichen und dem nachfolgenden Formatcode angegeben und legt die Mindestanzahl an Zeichen fest, die mit dem entsprechenden Formatelement ausgegeben werden sollen. Wenn der auszugebende Wert das Feld nicht voll ausfüllt, werden Leerzeichen auf der linken Seite eingefügt, damit das Feld breit genug ist. Falls der Wert zu groß für das Feld ist, wird das Feld entsprechend vergrößert. Der Modifizierer für die Ausgabebreite schneidet ein Feld niemals ab. Wenn man mit diesem Modifizierer Zahlenspalten ausgeben will, dann verschiebt ein zu großer Wert die nachfolgenden Werte in der Zeile nach rechts. Der Modifizierer für die Ausgabebreite kann bei allen Formatcodes angegeben werden, sogar bei %%. Beispielsweise gibt die Anweisung `printf("%8%\n");` ein rechts ausgerichtetes %-Zeichen in einem acht Zeichen langen Feld aus.

Der Modifizierer für die Genauigkeit legt die Anzahl der Ziffern in der Darstellung von Zahlen oder Strings fest. Der Modifizierer wird mit einem Dezimalpunkt angegeben, hinter dem mehrere Ziffern folgen. Er steht hinter dem %-Zeichen und dem Modifizierer für die Ausgabebreite, aber immer noch vor dem Formatcode:

- Bei den Integer-Formatelementen %d, %o, %x und %u gibt dieser Modifizierer die Mindestanzahl der Zeichen an, die ausgegeben werden sollen. Wenn der Wert nicht soviele Ziffern umfaßt, werden Nullen vorangestellt. Die Anweisung `printf("%.2d/%.2d/%.4d\n", 7, 14, 1789);` ergibt also die Ausgabe `07/14/1789`.
- Bei den Formatelementen %e, %E und %f gibt die Genauigkeit die Anzahl der Ziffern hinter dem Dezimalpunkt an. Wenn die Flags (die wir gleich danach besprechen) nichts anderes angeben, erscheint nur dann ein Dezimalpunkt, wenn die Genauigkeit größer als Null ist. Zum Beispiel:

      double pi;
      pi = 4 * atan(1.0);
      printf("%.Of %.lf %.2f %.3f %.6f %.10f\n", pi, pi, pi, pi, pi, pi);
      printf("%.Oe %.le %.2e %.3e %.6e %.10e\n", pi, pi, pi, pi, pi, pi);

  die folgende Ausgabe:

      3 3.1 3.14 3.142 3.141593 3.1415926536 3e+OO 3.le+OO 3.14e+OO 3.1415926536e+OO

- Bei den Formatelementen %g und %G gibt die Genauigkeit die Anzahl der signifikanten Ziffern an, die ausgegeben werden sollen. Wenn in den Flags nichts anderes angegeben wurde, werden alle bedeutungslosen Nullen entfernt und der Dezimalpunkt gelöscht, falls dahinter keine Ziffern mehr folgen.
- Im Falle der %s-Formatelemente gibt die Genauigkeit an, wieviele Zeichen des entsprechenden Strings ausgegeben werden sollen. Wenn im String nicht genug Zeichen vorhanden sind, um die geforderte Genauigkeit zu erfüllen, wird die Ausgabe gekürzt. Mit Hilfe des Modifizierers für die Ausgabebreite kann die Ausgabe wieder verlängert werden. Mit Hilfe des Formatelements `%15.15s` wäre garantiert, daß genau 15 Zeichen ausgegeben werden.
- In den Formatelementen c und % wird die Genauigkeit nicht berücksichtigt.

#### Flags

Zwischen dem %-Zeichen und der Feldbreite können noch weitere Zeichen angegeben werden, mit denen man die Wirkung eines Formatelements weiter beeinflussen kann. Diese Zeichen werden als Flags bezeichnet. Die Flagzeichen haben folgende Bedeutungen:

- Das Flag - wirkt sich nur dann aus, falls eine Breite angegeben wurde (da man nur dann etwas zum Ausfüllen hat, wenn die Breite größer ist, als es für den Wert notwendig ist). In diesem Fall erscheinen alle Leerzeichen zum Auffüllen auf der rechten, und nicht auf der linken Seite.
  Beim Ausdruck von Strings in Spalten fester Länge sieht es normalerweise besser aus, wenn man sie links ausrichtet. Daher ist ein Format wie `%14s` meistens ein Fehler und sollte wohl `%-14s` heißen.
- Das Flag + gibt an, daß jeder numerische Wert mit einem Vorzeichen als erstes Zeichen ausgegeben werden soll. Es werden also auch alle nicht negativen Werte mit einem Pluszeichen ausgegeben. Das Flag hat keinerlei Bezug zum Flag -.
- Wenn ein Leerzeichen als Flag verwendet wird, dann bedeutet das, daß ein einzelnes Leerzeichen vor jedem numerischen Wert erscheinen soll, wenn kein Vorzeichen vorhanden ist. Dies wird meist für links ausgerichtete Spalten von Zahlen verwendet, in denen kein Pluszeichen vorkommen soll. Wenn die Flags + und Leerzeichen im selben Formatelement angegeben werden, hat das Flag + den Vorrang. Die Formatelemente `% e` und `%+e` sind für die Ausgabe von Zahlenspalten in wissenschaftlicher Schreibweise besser geeignet als das normale Formatelement `%e`: Durch das Vorzeichen (oder das Leerzeichen) wird sichergestellt, daß alle Dezimalpunkte untereinander stehen.
- Das Flag # verändert das Format der numerischen Werte geringfügig abhängig von den einzelnen Formatelementen. Beim %o-Format wird die Genauigkeit erhöht, und zwar so weit, daß die erste ausgegebene Ziffer eine 0 ist. Damit können Oktalwerte so ausgegeben werden, wie sie von C-Programmierern am leichtesten erkannt werden. In gleicher Weise stehen bei den Formatelementen %#x und %#X die Zeichenfolgen 0x bzw. 0X vor der Ausgabe.
  Das Flag # erzwingt die Ausgabe eines Dezimalpunkts, auch wenn dahinter keine weiteren Ziffern mehr kommen, und es verhindert das Weglassen nachfolgender Nullen in dem Formaten %g und %G.

Die Flags sind außer dem Leer- und dem Pluszeichen alle voneinander unabhängig.

#### Variable Feldbreite und Genauigkeit

Viele C-Programme definieren sorgfältig die Länge eines Strings als feste Konstante, damit sie leichter zu ändern ist geben aber die Ausgabebreite in den Ausgabeanweisungen als Integerkonstante an. Es wäre also nicht allzu klug, wenn wir eines unserer früheren Beispiele wie folgt umschreiben würden:

    #define NAMESIZE 14
    char name[NAMESIZE];
    ...
    printf(" ... %.14s ... ", ... , name, ...);
    ...

Jemand der später einmal NAMESIZE verändern will, wird wahrscheinlich übersehen, daß er alle printf-Aufrufe ebenfalls verändern muß. Es ist jedoch nicht möglich, NAMESIZE direkt in der printf-Anweisung anzugeben:

    printf("... %.NAMESIZEs, ...", name, ...);

funktioniert nicht, da der Präprozessor innerhalb von Strings keine Ersetzungen vornimmt.

printf erlaubt daher, daß die Feldbreite oder Genauigkeit indirekt angegeben werden kann. Dazu schreibt man anstelle der Feldbreite oder der Genauigkeit das Zeichen *. In diesem Fall holt sich printf die tatsächlichen Werte aus der Argumentliste, bevor der Wert ausgegeben wird. Im obigen Beispiel sollte es also

    printf("... %.*s, ...", ..., NAMESIZE, name, ...);

heißen. Wenn die Konvention mit dem * sowohl für die Feldbreite als auch die Genauigkeit verwendet wird, erscheint das Argument mit der Feldbreite zuerst, dahinter kommt das Argument für die Genauigkeit und anschließend der Wert, der ausgegeben werden soll.

    printf("%*.*s\n", 12, 5, str);

hat also dieselbe Wirkung wie

    printf("%12.5s\n", str);

Wenn das Zeichen * für die Feldbreite verwendet wird und der entsprechende Wert negativ ist, hat das denselben Effekt, als ob das Flag - ebenfalls angegeben worden wäre.

#### Zusammenfassung: Formatierte Ausgabe in C mit "printf"

Allgemeine Form: | `printf(Controlstring, Arg1, Arg2, ... )`
Controlstring: | Ausgabe von Text und Steuerung der Ausgabeformte. Er kann enthalten: darstellbare Zeichen Zeichenersatzdarstellungen ('\n', '\t' usw.) Umwandlungsspezifikationen
Arg1, Arg2, ...:: | Die auszugebenden Werte (Argumente). Anzahl und Typ sind durch den "controlstring" festgelegt
Umwandlungsspezifikation: | Sie haben die Form: `%[Formatangabe]Konvertierungszeichen` Konvertierungszeichen:
c | Einzelzeichen
d, i | Integerzahl (dezimal,konegativ)
u | Integerzahl (dezimal,nur positiv)
o | Integerzahl (oktal, nur positiv, ohne führende "0")
x, X | Integerzahl (sedezimal, nur positiv, ohne führende "0x")
e, E | Gleitpunktzahl (float oder double) in Exponentendarstellung
f | Gleitpunktzahl (float oder double) in Dezimalbruchdarstellung
g, G | kürzeste Darstellung von e oder f
s | String
p | Pointer (implementierungsabhängige Darstellung)
Formatangabe:

### Die Eingabefunktion scanf

`scanf(controlstring, arg1, arg2, ... )`

"scanf" liest die naechsten Zeichen von stdin, interpretiert sie entsprechend den im Steuerstring "`controlstring`" vorliegenden Typ- und Formatanangaben und weist die dem geäß konvertierten Werte den durch ihre Adresse referierten Variablen `arg1, arg2, ...` zu. Anzahl und Typ der Variablen sind durch "`controlstring`" festgelegt.

Die der Zeichen-Werte-Konvertierung zugrundeliegenden Umwandlungsspezifikationen werden durch White-Space-Character (Blank, Tab, Newline) bzw. durch Längenangaben im Steuerstring getrennt.

Funktionswert: Die Anzahl der erfolgreich zugewiesenen Werte bzw. EOF (= -1), wenn beim Lesen des ersten Wertes versucht wurde, über die Eingabe des Dateiendezeichens hinaus zu lesen.

Die Umwandlungsspezifikation haben ein einheitliches Format:

    %[*][ziff]Konvertierungszeichen
      Ĥ   Ĥ
      Ĥ   maximale Eingabefeldgröße (Kann weggeleassen werden.
      Ĥ   Bei Konvertierungszeichen c: Anzahl der einzulesenden Zeichen.)
      Ĥ
      Zeichen zum Überlesen des nächsten Eingabefeldes
      (assignment suppression; kann weggeleassen werden.)

#### Konvertierungszeichen:

c | Zeichen(-folge) (auch blanks, tabs und newlines werden gelesen) Default (keine Feldgrößenangabe): 1 Zeichen
d | Integer (dezimal)
i | Integer (dezimal) oder oktal (mit führender "0") oder sedezimal (mit führendem "0x" bzw "0X")
o | Integer (oktal, mit oder ohne führende "0")
x | Integer (sedezimal, mit oder ohne führendem "0x" bzw "0X")
u | Integer (dezimal, nur positiv)
e, f, g | Gleitpunktzahl (beliebige Darstellung)
s | String (Ergänzung mit abschließendem '\0')
p | Pointer

h vor d,i,o,u,x | Kurze Integerzahl (short)
l vor d,i,o,u,x | Lange Integerzahl (long)
l vor e,f,g | double
L vor e,f,g | long double

#### Beispiel:

      int   i, jwert;
      float zahl;
      ...
      scanf("%2d %f %*d %d",&i,&zahl,&jwert)
      ...

Nach Eingabe von: `56789 0123 457` haben die Variablen folgende Werte: i = 56, zahl = 789.0, jwert = 457. Die Zahl 0123 wird überlesen!

### Die Eingabefunktion gets

`char *gets(char *s);`

"gets" liest die nächsten Zeichen von stdin bis zum nächsten Newline-Character ('\n') bzw bis das Dateiende erreicht ist. Die gelesenen Zeichen werden in dem durch "s" referierten String ohne eventuell gelesenes '\n' abgelegt. An s wird automatisch Das Stringende-Zeichen '\0'angehängt. Funktionswert ist die Anfangsadresse des Strings "s" oder ein NULL-Pointer bei Erreichen des Dateiendes ohne vorheriges Lesen von Zeichen. Im Fehlerfall wird ebenfalls ein NULL-Pointer zurückgegeben. Es wird nicht überprüft, ob genügend Speicherplatz für den String zur Verfügung steht.

### Die Ausgabefunktion puts

`int puts(const char *s);`

"puts" gibt den durch "s" referierten String (ohne '\0'-Character!) nach stdout aus, wobei ein abschließendes '\n' angefügt wird. Funktionswert ist ein nicht-negativer Wert bei Fehlerfreiheit und EOF (-1) im Fehlerfall.

#### Beispiel:

    /* String einlesen und wieder ausgeben */
    #include <stdio.h>

    char str[100];

    void main(void)
      {
      if (gets(str) != NULL)
        puts(str);
      else
         printf("Leereingabe\n");
      }

### Die Eingabefunktion getchar

`int getchar(void);`

"getchar" liest das nächste Zeichen von stdin und gibt das gelesene Zeichen (als int-Wert zurück. Bei Eingabe des Fileendezeichens für Textdateien oder im Fehlerfall wird EOF (-1) zurückgegeben.

#### Beispiel:

    /* Zeichen kopieren von stdin nach stdout */
    #include <stdio.h>

    int ch;

    void main(void)
      {
      while ((ch = getchar()) != EOF)
        putchar(ch);
      }

### Die Ausgabefunktion putchar

`int putchar(int c);`

"putchar" gibt das Zeichen "c" (nach Umwandlung in unsigned char) nach stdout aus. Funktionswert: das ausgegebene Zeichen (als int-Wert) oder EOF (-1) im Fehlerfall.

Das folgende Programm demonstriert eine kreative Form der Ausgabe.

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

    void xmastree(int order);

    int main(int argc, char *const *argv)
      {
      if ( argc > 1 ) {xmastree(atoi(argv[1])); }
      else            {puts("usage: xmas order"); }
      return 0;
      }

    /*
     * Diese Funktion druckt den Weihnachtsbaum
     */
    void xmastree(int order)
      {
      int top, height, width, ornament,
          leading_spaces, middle_spaces;

      top = order + 1;  /* 'order + 1' wird öfter gebraucht */
      /* Die führenden Leerzeichen zentrieren den Baum in der Mitte:
       * order * (order + 1) / 2 */
      leading_spaces = order * top / 2 + 1;
      middle_spaces = 0;

      /* Baum ausgeben, Schleife für die Hoehe */
      for( height = 0; height < top; ++height )
        {
        /* Die 'Aeste' */
        for( width = 0; width <= height; ++width )
          {
          /* Verzierung an jedem Ast bis auf den ersten */
          ornament = height && !width;
          if(width) middle_spaces += 2;
          /* Trick: Statt Schleife lassen wir printf die Leerzeichen drucken */
          printf("%*s", leading_spaces, "");
          if(ornament) { putchar('*'); }
          else         { --leading_spaces; }
          /* Trick wie oben  '/' etliche leerzeichen '\' */
          printf("/%*s\\", middle_spaces, "");
          if(ornament) putchar('*');
          putchar('\n');
          }
        }
      }

## 3.10 Debuggen auf Quelltextebene

C verfügt über einen einfachen Mechanismus zum Debuggen auf Quelltextebene: Kritische Variableninhalte, die untersucht werden sollen, werden einfach zum Bildschirm, zum Drucker oder auf einen zweiten Bildschirm geleitet. Damit die zum Debuggen erforderliche Codierung nicht in der endgültigen Fassung auftaucht, kann mit dem Präprozessor Debugging-Code ein- bzw. ausgeschaltet werden. Diese Technik soll am Beispiel der Rekursion gezeigt werden. Die Rekursion kann leicht zu schwer erkennbaren Fehlern führen. Hier wird eine Technik vorgestellt, die auch mit ANSI-C durchführbar ist.

#### Problem

Das folgende Programm `kaprek.c` zur rekursiven Bestimmung von Kapitalendwerten bei Wiederanlage der Zinsen soll durch Debugging-Informationen ergänzt werden. Diese sollen zeigen, wann die Funktion `Endkapital()` betreten wird. Dabei sollen der Dateiname und die Zeilennummer angegeben werden. Bei Bedarf soll das Programm auch ohne die Debugging-Informationen compiliert werden können. Dabei kann man auf zwei Standardmakros, `__FILE__` und `__LINE__`, zurückgreifen ("`__`" sind übrigens zwei aufeinanderfolgende Unterstreichungszeichen). Es gibt noch zwei weitere Standardmakros, die mitunter nützlich sein können, `__DATE__` und `__TIME__`.

    /* Zinseszinsrechnung */
    /* Rekursion mit Debugging-Informationen */
    /* kaprek.c */

    #include
    #define DEBUG 1

    #if DEBUG
    #define ANFANG(Funktion) \
            printf("Start %s [%s:%d]\n", Funktion, __FILE__, __LINE__);
    #define ENDE(Funktion) \
            printf("Ende  %s [%s:%d]\n", Funktion, __FILE__, __LINE__);
    #else
    #define ANFANG(Funktion)
    #define ENDE(Funktion)
    #endif

    float Endkapital (float Einz, float Zins, float Zeit);

    main ()
      {
      printf ("Endkapital ... : %.2f DM", Endkapital (1000, 10, 2));
      return (0);
      }

    /* Rekursive Berechnung des Endkapitals */
    float Endkapital (float Einz, float Zins, float Zeit)
      {
      float ergebnis;

      ANFANG ("Endkapital")
      if (Zeit == 1)
        ergebnis = Einz * (1 + Zins/100);
      else
        ergebnis = (1 + Zins/100) * Endkapital(Einz, Zins, Zeit - 1);
      ENDE ("Endkapital")
      return(ergebnis);
      }

Der Programmlauf ergibt:

    Start Endkapital [modrek2.C:31]
    Ende  Endkapital [modrek2.C:36]
    Start Endkapital [modrek2.C:31]
    Ende  Endkapital [modrek2.C:36]
    Endkapital ... : 1210.00 DM

In der Bildschirmausgabe ist zu erkennen, wann das Modul Endkapital() betreten und wann es verlassen wurde. Weiterhin wird jeweils die aktuelle Zeilennummer ausgegeben.

#### Analyse

Anfang und Ende der Funktion `Endkapital()` sind mit `ANFANG ("Endkapital")` und `ENDE ("Endkapital")` versehen worden. Bitte beachten: es folgt kein Strichpunkt, da dieser bereits im Makro vorhanden ist. Makros werden ja vom Präprozessor rein textuell ersetzt. Diese Makros haben aber nur dann eine Bedeutung, wenn DEBUG wahr ist (`#define DEBUG 1`). In diesem Fall haben die Makros Anfang und Ende die im `#if`-Zweig angegebene Bedeutung:

    #define ANFANG(Funktion) \
            printf("Start %s [%s:%d]\n", Funktion, __FILE__, __LINE__);
    #define ENDE(Funktion) \
            printf("Ende  %s [%s:%d]\n", Funktion, __FILE__, __LINE__);

Anderenfalls haben sie keine Bedeutung, da im `#else`-Zweig ein leeres Makro definiert wird. Das Makro muß nur vorhanden sein, damit der Compiler keinen Fehler meldet:

    #define ANFANG(Funktion)
    #define ENDE(Funktion)

Diese Technik eignet sich gut zum Debuggen von Quelltext. Solange das Programm noch nicht einwandfrei läuft, werden so Debuggingzeilen untergebracht. Sobald das Programm für den Produktions-Betrieb compiliert werden soll, werden diese Zeilen für den Compilationsvorgang deaktiviert (`#define DEBUG 0`). Sie werden nicht aus dem Quelltext herausgenommen, da dies nicht nur Arbeit verursacht, sondern sie bei Programmänderungen und -erweiterungen auch wieder von Nutzen sein können.

Da die Makroersetztung auf Quellniveau erfolgt, wird auch kein "toter Code" im Binärprogramm erzeugt. Das Schema läßt sich beliebig erweitern, indem man beispielsweise auch Variablenwerte "tracen" kann. Durch die Definition eines weiteren Makros kann man auch den Resultatwert nachverfolgen:

    /* Zinseszinsrechnung */
    /* Rekursion mit Debugging-Informationen */
    /* kaprek.c */

    #include
    #define DEBUG 1

    #if DEBUG
    #define ANFANG(Funktion) \
            printf("Start %s [%s:%d]\n", Funktion, __FILE__, __LINE__);
    #define ENDE(Funktion) \
            printf("Ende  %s [%s:%d]\n", Funktion, __FILE__, __LINE__);
    #define TRACE(Floatvar, Floatwert) \
            printf("Variable %s: %f [%s:%d]\n",
                    Floatvar, Floatwert, __FILE__, __LINE__);
    #else
    #define ANFANG(Funktion)
    #define ENDE(Funktion)
    #define TRACE(Floatvar, Floatwert)
    #endif

    float Endkapital (float Einz, float Zins, float Zeit);

    main ()
      {
      printf ("Endkapital ... : %.2f DM", Endkapital (1000, 10, 2));
      return (0);
      }

    /* Rekursive Berechnung des Endkapitals */
    float Endkapital (float Einz, float Zins, float Zeit)
      {
      float ergebnis;

      ANFANG ("Endkapital")
      if (Zeit == 1)
        ergebnis = Einz * (1 + Zins/100);
      else
        ergebnis = (1 + Zins/100) * Endkapital(Einz, Zins, Zeit - 1);
      TRACE("ergebnis", ergebnis)
      ENDE ("Endkapital")
      return(ergebnis);
      }

## 3.11 Gliederung von Funktionen

Eine Funktion läßt sich in ihrem grundsätzlichen Aufbau und der Befehlsabfolge in der Regel folgendermaßen skizzieren:

- Initialisierung
- Dateneingabe
- Berechnung/Verarbeitung
- Datenausgabe
- Nachbereitung

Dieser Ablauf lässt sich bei allen Programmtypen anwenden. Die gezeigte Gliederung einer Funktion mag auf den ersten Blick trivial erscheinen, jedoch wird oft der Struktur einer Funktion häufig zu wenig Beachtung geschenkt. Werden alle Initialisierungen am Anfang einer Funktion durchgeführt, ist leicht überprüfbar, ob auch wirklich alle Variablen initialisiert wurden. Bei Änderungen an der Funktionen kann man sich beispielsweise schnell einen Überblick über die Startwerte der Funktion verschaffen.

Die Trennung von Dateneingabe, Datenverarbeitung und Datenausgabe hat den Vorteil, dass die Algorithmen zur Datenverarbeitung klar getrennt von Datenein- und -ausgabe vorliegen. Dies dient der Verständlichkeit und erleichtert damit die Wartung der Algorithmen.

#### Initialisierung

Alle zur Berechnung nötigen Größen, wie Parameter, Konstanten, Zwischen-Ergebnisse etc. werden hier aufgeführt. Der Zugriff auf die Werte kann beispielsweise durch Zugriff auf Übergabeparameter der Funktion (bevorzugt), globale Werte oder den Aufruf von Funktionen erfolgen, die solche Werte liefern. Generell müssen alle statischen Daten mit sinnvollen Werten initialisiert werden. Achten Sie auch auf Datentypen und Speicherklassen.

Variablen, in denen beispielsweise Sensorwerte gespeichert werden, sollten am Anfang auf einen Wert gesetzt werden, der signalisiert, dass der Eingangswert nicht gültig ist. Für diesen Wert wird auch oft die Abkürzung SNV (Signal nicht verfügbar) bzw. SNA (signal not available) verwendet. Variablen, die Systemzustände speichern, initialisiert man mit dem Startzustand. Bei sicherheitsrelevanten Zuständen sollte dies auch ein sicherer Zustand sein.

#### Dateneingabe

Wie schon erwähnt, erhält eine Funktion die zur Verarbeitung nötigen Eingangsdaten durch Übergabeparameter der Funktion, den Zugriff auf globale Variablen oder den Aufruf von Funktionen, die diese Werte erzeugen oder abrufen.

Meist stellen Betriebssysteme und Treiber die Daten nicht in der von der Funktion gewünschten Form bereit. Bevor die Funktion die Daten verwenden kann, müssen diese vorverarbeitet werden. Hier erfolgt auch eine Prüfung der Werte nach verschiedenen Kriterien:

- Prüfen der Daten auf Plausibilität.
  Physikalische Randbedingungen führen meist dazu, dass Daten oft nur bestimmte Werte annehmen können. Wenn Sie wissen, um welche Beträge die Eingangsdaten ihre Werte von Verarbeitungszyklus zu Verarbeitungszyklus maximal ändern können, dann eliminieren Sie z. B. Werte, die physikalisch zu diesem Zeitpunkt gar nicht auftreten können.
- Ausnutzen physikalischer Gesetzmäßigkeiten.
  Der Zusammenhang einzelne Daten lässt sich in der Regel nicht in einer Umrechnungsformel darstellen, man kann aber oft den Zusammenhang in einer Bedingung oder Ungleichunh beschreiben, z. B. "Ist Wert A größer Null, ist auch Wert B größer Null" oder "Wert X muss monoton steigend sein". Diese Beschreibung kann man nutzen, um fehlerhafte Daten herauszufiltern.
- Prüfen der Daten auf Gültigkeit.
  Dazu gehören neben den oben angeführten Kriterien u. a. das Einhalten von Zeitvorgaben für den Datenempfang und die die Gültigkeitsdauer der Daten.

#### Nachbereitung

Oft ist keine Nachbereitung nötig. Manchmal sind bestimmte Systemzustände oder Systemgrößen, die während der Berechnung verändert wurden, vor Beendigung der Funktion wieder herzustellen. Bei Programmen in Hochsprachen sorgt üblicherweise der Compiler automatisch für die Speicherung von lokalen Größen auf dem Stack. Zur Nachbereitung gehören u. a. Operationen, welche die Protokolldatei(en) aktualisieren.

Zum Inhaltsverzeichnis | Zum nächsten Abschnitt

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