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: 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:

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:

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:

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:abcde
HP Anfang:1234 
Funktion Anfang:1????4??
Funktion Ende:1020304050
HP Ende:102340 

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:

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. 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

Zur Lösung geht man wie folgt vor: 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:

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 :

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 :

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:

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:

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: 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:

cZeichen(-folge) (auch blanks, tabs und newlines werden gelesen)
Default (keine Feldgrößenangabe): 1 Zeichen
dInteger (dezimal)
iInteger (dezimal) oder oktal (mit führender "0") oder sedezimal (mit führendem "0x" bzw "0X")
oInteger (oktal, mit oder ohne führende "0")
xInteger (sedezimal, mit oder ohne führendem "0x" bzw "0X")
uInteger (dezimal, nur positiv)
e, f, gGleitpunktzahl (beliebige Darstellung)
sString (Ergänzung mit abschließendem '\0')
pPointer

h vor d,i,o,u,xKurze Integerzahl (short)
l vor d,i,o,u,xLange 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);
  }

Zum Inhaltsverzeichnis Zum nächsten Abschnitt


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