Inhalt
6.3 Die
Hauptfunktion int main()
6.4 Das
vollständige Programm exptab.c
Mittels top-down-Analyse zerlegt man ein komplexes Problem solange in kleinere Teilprobleme bis diese überschaubar werden. Diese Teilprobleme kann man zunächst als Unterprogramme realisieren und auch einzeln testen, ehe man sie zu einem Ganzen zusammenfasst, um schließlich das komplexe Problem zu lösen.
=> Unterprogramme
dienen der Strukturierung eines Programms,
1. der
Ausgliederung wiederkehrender Berechnungen und
2. der Zerlegung komplexer Probleme in kleinere.
Verarbeitung von Unterprogrammen im Compiler
Kellerung
Unterprogramme: Unterprogrammaufruf
Unterprogrammvereinbarung
(Deklaration und Definition)
Unterprogrammrücksprung
In C werden Unterprogramme grundsätzlich als Funktionen realisiert, diese liefern höchstens einen Funktionswert. Da das Hauptprogramm int main() auch eine Funktion darstellt, ist ein C-Programm im Wesentlichen eine Folge von C-Funktionen.
modul.c
Direktiven für den Präprozessor
globale Deklarationen
Typ funktion_1( . . . )
Typ funktion_2( . . . )
. . .
Typ funktion_n( . . . )
int main( . . . )
{
lokale Deklarationen
Anweisungsfolge
}
Typ funktion_1( . . . )
{
lokale Deklarationen
Anweisungsfolge
}
Typ funktion_2( . . . )
{
lokale Deklarationen
Anweisungsfolge
}
. . .
Typ funktion_n( . . . )
{
lokale Deklarationen
Anweisungsfolge
}
Wir wollen die
Unterprogrammtechnik anhand einer Reihenentwicklung für die Exponentialfunktion
mit
einführen und
gleichzeitig Fortpflanzung von Fehlern durch die Zahlendarstellung im Rechner
untersuchen.
Reihenentwicklung:
Algorithmus für die Reihenentwicklung
Berechne den neuen Summanden aus dem alten und addiere ihn zur bis dahin berechneten Summe. Wiederhole das solange, bis der neue Summand so klein ist, dass er keinen Beitrag zur Summe bildet, d.h. die Summe sich nicht mehr verändert (Rechnergenauigkeit).
(1) summeNeu = 1, summand = 1, i = 1 Startwerte
(2) berechne
(a) summand = summand * x / i Berechnen des neuen Summanden
(b) summeAlt = summeNeu Berechnen der neuen Summe
(c) summeNeu = summeNeu + summand
(d) i = i + 1
solange summeNeu ≠ summeAlt Abbruch
Programm für die Reihenentwicklung
(1) summeNeu = 1; summand = 1; i = 1;
(2) do
{
summand = xx / i++;
/* (a), (d) */
summeAlt
= summeNeu; /* (b)
*/
summeNeu += summand; /* (c) */
} while( summeNeu != summeAlt);
Zunächst wird eine Funktion mit
dem Namen myexp
definiert, welche einen Parameter double xx besitzt und einen Funktionswert vom Typ double
zurückliefert.
Jede Funktion, außer der Hauptfunktion int main(), wird üblicherweise in zwei Schritten vereinbart. Diese Vorgehensweise entspricht der top-down-Analyse: Im ersten Schritt wird dem Compiler mitgeteilt, welche Funktionen benötigt werden, im zweiten Schritt wird festgelegt, welche Operationen die Funktionen ausführen.
1. Schritt: Deklaration einer Funktion (Festlegen des Prototyps)
Dem Compiler muss vor dem ersten Aufruf einer Funktion der Funktionsname (symbolische Adresse der Funktionsdefinition), der Typ des Funktionswerts, die Parametertypen und ihre Anzahl bekannt sein.
Syntax:
Funktionstyp Funktionsname ( [ Parametertyp [Parametername] , ...] ) ;
Funktionstyp:
· Gibt den Typ des Funktionswertes an, kein Funktionswert: void.
Parametertyp:
· Treten Parameter in der Funktion auf, so müssen ihre Typen aufgelistet werden.
· C-Funktionen benötigen stets eine Parameterliste, auch wenn sie leer ist: f().
Parametername:
· C-Name, Parameternamen können bei der Deklaration angegeben werden.
Die Deklaration einer Funktion erfolgt in der Regel im globalen Deklarationsteil des Programms oder in einer Headerdatei. Eine Headerdatei besitzt als Suffix .h und fasst mehrere Deklarationen zusammen.
Deklaration der Funktion myexp
double myexp( double); /* globale Deklaration */
Deklaration der
Sandardfunktionen und der Funktionen aus der Mathematischen Bibliothek
# include
<stdio.h> /*
Standardfunktionen */
# include
<math.h> /* mathematische
Funktionen */
2. Schritt: Definition einer Funktion
Die Folge der Anweisungen, die beim Funktionsaufruf ausgeführt werden soll, wird nach der Definition der Funktion int main() oder auf einem anderen Modul zusammengefasst.
Syntax:
Funktionstyp Funktionsname ( [ Parametertyp Parametername
, ...] )
zusammengesetzte
Anweisung
Parametername:
· C-Name, Parameternamen müssen bei der Definition angegeben werden:
Die formalen Parameter sind für die Definition der Funktion als Platzhalter der Argumentwerte notwendig und müssen deshalb dem Compiler mitgeteilt werden.
Semantik:
1. Die Parameterübermittlung erfolgt durch eine Kopie der Werte der Argumente - call by value. Falls notwendig werden die Argumente in den Parametertyp konvertierten.
2. Die aufgerufene Funktion wird mit diesen Werten ausgeführt.
Definition der Funktion myexp
/*
* Exponentialfunktion (Reihenentwicklung)
* e hoch x = 1 + x + x hoch 2 / 2! + x hoch 3 / 3! + ...
*/
double myexp( double xx)
{
double
summeAlt, summeNeu = 1, summand = 1;
int
i = 1; /* Zaehler fuer
Durchlaeufe */
do /*
Reihenentwicklung */
{ /*Berechnung des neuen Summanden
*/
summand *=
xx / i++;
/*Berechnung der neuen Summe */
summeAlt = summeNeu;
summeNeu +=
summand;
} while((
summeNeu != summeAlt) /*&& (i < 100)*/);
return
summeAlt;
}
Eine Funktion wird entweder nach der Abarbeitung der letzten Anweisung mit einem zufälligen Funktionswert oder durch die return - Anweisung verlassen.
Syntax: return [ Ausdruck ] ;
Semantik:
· Der Wert des Ausdrucks wird in den Funktionstyp konvertiert und als Funktionswert zurückgegeben, d.h. im Programm an die Stelle des Funktionsaufrufs gesetzt.
· Das Programm wird an der Aufrufstelle fortgesetzt.
· Der Rückgabewert wird i. R. von der aufrufenden Funktion ausgewertet.
Die Funktion double myexp(
double); liefert einen Wert für die Exponentialfunktion zurück. Dieser
wurde in der Variablen summeAlt berechnet.
double myexp( double xx)
{
. . .
return
summeAlt; /*
double */
}
Syntax: Funktionsname
( [ Argument , ...] )
Der Funktionsaufruf ist ein Ausdruck, kann also in komplexen Ausdrücken als Operand verwendet werden.
Funktionsname:
· C-Name, symbolischer Bezeichner für die Speicheradresse der Funktionsdefinition.
Argument:
· C benötigt beim Funktionsaufruf eine Argumentenliste, auch wenn sie leer ist: f().
· Gibt man keine Argumentenliste an, so erfolgt kein Funktionsaufruf, sondern es wird die Adresse der Funktionsdefinition ermittelt: f.
· Anzahl und Typen der Argumente sind durch die Funktionsvereinbarung festgelegt.
· Für Argument kann ein beliebiger mit dem vorgegebenen Typ verträglicher ( in ihn konvertierbarer ) Ausdruck eingesetzt werden
· Geschachtelte Funktionsaufrufe sind erlaubt.
Semantik:
1. Die Werte der Argumente werden berechnet, in den vorgegebenen Parametertyp konvertiert und eine Kopie des Wertes der Funktion übergeben (call by vallue).
2. Die aufgerufene Funktion wird mit diesen Werten ausgeführt und danach wird die Aufrufstelle durch den Funktionswert, falls vorhanden, ersetzt und die Ausführung des Programms fortgesetzt.
In einem Testprogramm exptab.c soll mit der Funktion myexp und der Funktion exp aus der mathematischen Bibliothek
experimentiert werden. Wir finden dort sowohl Funktionsaufrufe vorgefertigter
Funktionen als auch den unserer Funktion.
exptab.c
/*
* Exponentialfunktion e hoch x
* Aufruf: gcc –Wall –ansi –O exptab.c –o
exptab –lm
*/
# include
<stdio.h>
# include
<math.h>
/* mathematische Funktionen */
double myexp( double); /* Exponentialfunktion */
/*
Rahmenprogramm zur Exponentialfunktion */
int main()
{
double x; /* Argument */
/* Vorbereitung
Ueberschrift */
printf(
"\nExponentialfunktion - e hoch x
-\n");
/* Eingabe */
printf(
"\nx = ");
scanf(
"%lf", &x);
/* Verarbeitung
und Ausgabe*/
/* Vergleich mit
Standardfunktion aus <math.h> */
printf("e ^ %g = %g,
%g\n", x, myexp( x), exp( x));
return
0;
}
· Es muss genau eine Funktion mit dem Namen main geben, diese ist der Einstiegspunkt in das Programm und darf selbst nicht aufgerufen werden.
· Für die Funktion int main() fällt die Funktionsdeklaration mit der Funktionsdefinition zusammen.
· Sie ist vom Typ int und muss einen Rücksprung return intAusdruck ; aufweisen.
· Der Rückgabewert kann vom übergeordneten Programm ausgewertet werden. Vereinbarungsgemäß wird der Rückgabewert 0 als korrekter Programmlauf und jeder anderer Wert als Fehlercode betrachtet.
/*
* Hauptprogramm
*/
int
main()
{
. . .
return 0; /* int */
}
Das Programm wird erweitert:
Durch eine Schleife wird die Möglichkeit der Berechnung mehrerer Werte gesteuert.
Hier nun das
vollständige Programm:
exptab.c
/*
* Exponentialfunktion e hoch x
* Aufruf gcc -Wall -O -ansi exptab.c -o exptab
-lm
* tabelliert Zwischenergebnisse
*/
# include
<stdio.h>
# include
<math.h> /* mathematische
Funktionen */
double myexp(
double); /* Exponentialfunktion
*/
/*
* Rahmenprogramm zur Exponentialfunktion
*/
int main()
{
double x; /* Argument */
char
c[ 2]; /* Abbruch
j/n */
/* Vorbereitung
Ueberschrift */
printf( "\nExponentialfunktion - e hoch
x -\n");
do
{
/* Eingabe */
printf( "\nx = ");
scanf( "%lf", &x);
/* Verarbeitung
und Ausgabe*/
/* Vergleich mit
Standardfunktion aus <math.h> */
printf("e ^ %g = %g, %g\n", x, myexp( x), exp( x));
/* Nachbereitung
*/
printf( "Noch einmal j/n? ");
scanf(
"%1s", c);
} while( c[ 0]
== 'j');
return
0;
}
/*
* Exponentialfunktion (Reihenentwicklung)
* e hoch x = 1 + x + x hoch 2 / 2! + x hoch 3 / 3! ...
*/
double myexp(
double xx)
{
double
summeAlt, summeNeu = 1, summand = 1;
int i = 1, j = 0;
/* Testausdruck-Ueberschrift
*/
printf( "\n%3s%25s%25s%25s\n",
"i", "summand", "summeAlt",
"summeNeu");
do /*
Reihenentwicklung */
{ summand *=
xx / i++;
summeAlt = summeNeu;
summeNeu += summand;
/* Testausdruck */
printf(
"%3d%25.10g%25.10g%25.10g\n",
i,
summand, summeAlt, summeNeu);
j++; /* Blaettern */
if( j ==
20){ j = 0; fflush( stdin); getchar();}
} while((
summeNeu != summeAlt) /*&& (i < 100)*/);
return
summeAlt;
}
Test - Ergebnisse
Exponentialfunktion
- e hoch x -
x = 1
e ^1 = 2.718282, 2.718282
x = -3
e ^ -3 = 0.0497871, 0.0497871
x = -10
e ^-10 = 0.000045, 0.000045
x = -13.5
e ^ -13.5 = 1.37097e-06, 1.37096e-06
x = -19.5
e ^ -19.5 = -2.45344e-10, 3.39827e-09
Durch höhere Genauigkeit lassen sich die Fortpflanzungsfehler nicht vermeiden.
Einschränkung des Fehlers
Diese Fehler
treten für auf. Man berechnet
deshalb
. Für
ist dann
.
Header -
Deklarationen von Bibliotheksfunktionen
Zahlreiche Funktionen stehen in Bibliotheken zur Verfügung. Ihre
Daklarationen sind in Headerdateien zusammengefasst und stehen damit für den
globalen Deklarationsteil zur Verfügung, # include < * .h >. Die
Headerdateien haben einen Namen mit dem Suffix .h, sind ASCII-Dateien
und damit lesbar und befinden sich standardmäßig im Verzeichnis /usr/include.
Die Deklarationen aus einer Headerdatei werden im Präprozessorlauf anstelle
# include < *.h > (festgelegter Suchpfad für Headerdateien /usr/include)
#
include "
*.h " (Suchpfad: aktuelles Verzeichnis)
als globale Deklarationen in das Programm gesetzt.
Übersicht über
die wichtigsten Header der Standardbibliothek
<assert.h> |
Testhilfen |
|
<ctype.h> |
Zeichenverarbeitung |
isdigit,... |
<errno.h> |
Fehlernummern |
|
<float.h> |
Interne Datenformate (Gleitkommatypen) |
FLT_MAX,... |
<limits.h> |
Interne Datenformate (Ganzzahlige Typen) |
INT_MAX,... |
<local.h> |
Länderspezifische Darstellung von Zahlen |
|
<math.h> |
Mathematische Funktionen |
sin, cos,... |
<setjmp.h> |
Sprünge zwischen Funktionen |
|
<signal.h> |
Behandlung von Signalen |
|
<stdarg.h> |
Behandlung von Funktionen mit variabler Parameterzahl |
|
<stddef.h> |
Elementare Typen |
size_t, NULL,... |
<stdio.h> |
Ein-/ Ausgabe |
printf, scanf,... |
<stdlib.h> |
Diverse Hilfsroutinen |
malloc,... |
<string.h> |
Stringverarbeitung |
strcpy, strcat,... |
<time.h> |
Termine und Zeiten |
time |
Bibliotheken -
Definitionen von Bibliotheksfunktionen
Die Definition der Funktionen stehen bereits übersetzt in
Bibliotheken bereit. Dabei kann
es zu einer Bibliothek mehrere Header geben. Die Bibliotheken selbst stehen in festgelegten Verzeichnissen /lib und /usr/lib. Ihr Name hat den
Aufbau liblibery.a für statische und liblibery.so* für dynamische Bibliotheken.
Alle Standard-C-Funktionen sind in der Bibliothek libc.a bzw. libc.so enthalten. Zu ihr gehören zum
Beispiel die Header stdio.h, stdlib.h, string.h und
weitere.
Möchte
man die Funktionen einer Bibliothek auflisten, so wechsele man in das
Verzeichnis der Bibliothek und führe den folgenden Befehl aus:
$ cd /lib
$ nm Bibliotheksname
Alle mathematischen Bibliotheksfunktionen erhält man mittels:
$ nm
libm.so | pg
Erläuterungen zum Gebrauch von C-Funktionen findet man hingegen im
Manual:
$ man C-Funktion
Binder
Der Binder hat die Aufgabe, die benötigten Funktionen aus den
Bibliotheken zu dem Objektcode des Programms hinzuzuladen und daraus ein
ausführbares Programm zu erzeugen (s. Kapitel 1). Für die Funktionen aus der
Bibliothek libc.a macht er das automatisch.
Für andere Funktionen muss man das dem Binder durch die Option –llibery
mitteilen. Zum Beispiel sind Programme mit Funktionen der mathematischen
Bibliothek mit dem Binderhinweis –lm zu übersetzen.
Beispiel aus exptab.c
/*
* Exponentialfunktion e hoch x
* Aufruf: gcc –Wall –ansi –O exptab.c –o
exptab –lm
*/
# include
<stdio.h>
# include <math.h> /* mathematische Funktionen */
. . .
int main()
{
. . .
/* Vergleich mit
Standardfunktion aus <math.h> */
printf("e ^ %g = %g, %g\n", x, myexp( x), exp( x));
. . .
return 0;
}
Es
ist möglich, sich eigene Bibliotheken zu erzeugen.