![]() |
Algorithmen & Datenstrukturen
|
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).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:

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:
void swap(int *n, int *m)
{
int park;
park = *n;
*n = *m;
*m = park;
}
void main(void)
{
int a, b;
...
swap(&a, &b); /* Funktionsaufruf */
...
}
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);
/* ... 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;
}
Die Einkommenssteuer beträgt in deutsche Mark
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);
}
#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;
}
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.
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: | 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).
|
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:
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.

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 */
}

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
/* 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.
Iterative Algorithmen treten in der Datenverarbeitung sehr häufig auf. Man kann grundsätzlich 2 Arten von Iterationen unterscheiden:
an = a * a * ... *a * a
mit n Iterationsschritten.
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
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;
}

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,
#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 > bund 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).
b = b - a wenn b > a
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.
#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 -100Das 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);
}
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;
}
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 :
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.
Das folgende Programm gleicht dem ersten C-Beispiel:
#includeDie Ausgabe dieses Programms istvoid main(void) { printf("Hallo Welt\n"); }
Hallo WeltDahinter 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.
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:
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.
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:
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
#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.
| Allgemeine Form: | printf(Controlstring, Arg1, Arg2, ... ) | ||||||||||||||||||||
| Controlstring: | Ausgabe von Text und Steuerung der Ausgabeformte. Er kann enthalten:
| ||||||||||||||||||||
| Arg1, Arg2, ...:: | Die auszugebenden Werte (Argumente). Anzahl und Typ sind durch den "controlstring" festgelegt | ||||||||||||||||||||
| Umwandlungsspezifikation: | Sie haben die Form: %[Formatangabe]Konvertierungszeichen
Konvertierungszeichen:
Formatangabe: |
"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.)
| 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 |
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!
"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.
"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.
/* String einlesen und wieder ausgeben */
#include <stdio.h>
char str[100];
void main(void)
{
if (gets(str) != NULL)
puts(str);
else
printf("Leereingabe\n");
}
"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.
/* Zeichen kopieren von stdin nach stdout */
#include <stdio.h>
int ch;
void main(void)
{
while ((ch = getchar()) != EOF)
putchar(ch);
}
"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.
/* Zinseszinsrechnung */ /* Rekursion mit Debugging-Informationen */ /* kaprek.c */ #includeDer Programmlauf ergibt:#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); }
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.
#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 |