6. Funktionen

6.1 Aufbau einer Funktion
6.2 Datentyp und Deklaration einer Funktion
6.3 Parameterübergabe
6.4 Speicherklassen und Geltungsbereiche von Namen
6.5 Rekursive Funktionen

6. Funktionen

- Funktionen sind Hilfsmittel, um Problemstellungen in kleine Teilprobleme zerlegen zu können

- sie dienen damit einer strukturierten Programmierung

- typische Anwendungen für Funktionen ist weiterhin die Erledigung immer wiederkehrender Aufgaben

- für die wichtigsten Aufgaben gibt es bereits Funktionen, sie sind in der C-Programmbibliothek enthalten

- für die eigenen Zwecke kann man natürlich auch eigene Funktionen schreiben

- C Programme bestehen typischerweise aus vielen kleinen und nicht aus wenigen großen Funktionen

6.1 Aufbau einer Funktion

- eine Funktion hat einen festgelegten Aufbau, der wie folgt aussieht:

name ( Parameterliste, optional ) 
{ 
    Vereinbarungen
    Anweisungen
    return Funktionswert,optional
} 

- die runden Klammern müssen stehen, damit name zur Funktion wird (Zwischenraum zwischen name und ( ist erlaubt

- die Parameterliste in den runden Klammern ist optional, d.h. sie muß nur vorhanden sein, wenn der Funktion wirklich Parameter übergeben werden

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

- 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 (siehe void-Datentyp für Funktionen)

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

name ( Parameternamen, optional ) 
Parameterdeklarationen, optional
{ 
    Vereinbarungen
    Anweisungen
    return Funktionswert,optional
} 

- die Parameternamen müssen alle in zugehörigen Parameterdeklarationen auftauchen

- die einfachste Funktion ist die leere Funktion:

dummy () {}

Beispielprogramm P6-1.C:

# include <stdio.h>
# define MAXLINE 1000
main()     /* alle Zeilen mit Suchmuster finden */
{
char line[MAXLINE];
while (getline(line,MAXLINE) > 0)
    if (index(line,"the") > 0)
        printf("%s",line);
}
getline(char s[], int lim)    /* Zeile in s ablegen, Länge
                                 liefern */
{ 
    int c,i;
    i=0;
    while (--lim>0 && (c=getchar()) != EOF && c!='\n')
        s[i++] = c;
    if (c == '\n')
        s[i++] = c;
    s[i] = '\0';
    return(i);
} 
index (char s[],char t[])    /* Position von t in s liefern,
                                -1 falls nicht da */
{ 
    int i, j, k;
    for (i=0; s[i] != '\0'; i++) {
        for (j=i,k=0; t[k]!='\0' && s[j]==t[k]; j++,k++)
            ;
        if (t[k] == '\0')
            return(i);
    }
    return(-1);
} 

zurück zum Inhaltsverzeichnis

6.2 Datentyp und Deklaration einer Funktion

- bisher haben wir nur int Funktionen besprochen

- Funktionsaufrufe und Funktionen ohne Typangabe werden wie int Funktionen behandelt

- da char immer in int gewandelt wird, sind damit auch schon die char Funktionen abgedeckt

- die meisten Funktionen sind int Funktionen

- soll eine Funktion einen anderen Datentyp als int liefern, so müssen 2 Dinge getan werden:

1. die Funktion muß vor ihrem ersten Aufruf als eine solche mit dem gewünschten Typ deklariert werden (die Typen der Parameter müssen hier nicht festgelegt werden, man kann dies aber tun (Prototyping, siehe am Ende dieses Kapitels))

2. der Funktionsdefinition selbst muß der Typ vorangestellt werden

Beispielprogramm P6-2.C

# define PI 3.1415
main()     /* Umfang und Fläche eines Kreises */
{
int i;
float umf(), radius;
double flae();
for (i=1,radius=1.; i <= 10; i++, radius += .5)
    printf("Kreis %2d Radius=%6.2f Umfang=%8.4f Fläche=\
        %8.4f\n",i,radius,umf(radius),flae(radius));
}
float umf(float rad) ;
{ 
    return(2.*PI*rad);
} 
double flae(float r) ;
{ 
    return(PI*r*r);
} 

Beispielprogramm P6-3.C

double atof(char s[])    /* Zeichenkette s nach double
                            wandeln */
{ 
    double val, power;
    int i, sign;
    for (i=0; s[i]==' ' || s[i]=='\n' || s[i]=='\t`; i++)
        ;     /* Zwischenraum uebergehen */
    sign = 1;
    if (s[i]=='+' || s[i]=='-')   /* Vorzeichen */
        sign = (s[i++]=='+') ? 1 : -1;
    for (val=0; s[i]>='0' && s[i] <='9'; i++)
        val = 10 * val + s[i]-'0';
    if s[i] == '.')
        i++;
    for (power=1, s[i]>='0' && s[i]<='9'; i++) {
        val = 10 * val + s[i]-'0';
        power *= 10;
    }
    return (sign * val / power);
} 

- Funktionen haben oft Wirkungen, die über den Funktionswert hinausgehen (z.B. getline: Füllen eines Zeichenvektors)

- der Funktionswert dient in diesen Fällen dann häufig nur als Statusanzeige

- der in der Funktion ermittelte Funktionswert muß in der aufrufenden Funktion nicht ausgewertet, also auch nicht zugewiesen werden

- soll eine Funktion grundsätzlich keinen Funktionwert liefern, dann sollte man sie mit dem speziellen Datentyp void deklarieren; die Funktion entspricht dann einer SUBROUTINE in FORTRAN bzw. einer PROCEDURE in PASCAL

Beispielprogramm P6-4.C

void help(int nr)      /* Ausgabe eines Hilfstextes */
{ 
    switch (nr) {
        case 1:
            printf("Hilfe 1\n");
            break;
        case 2:
            printf("Hilfe 2\n");
            break;
        case 3:
            printf("Hilfe 3\n");
            break;
        default:
            printf("Fehlerhafter Aufruf von HELP\n");
    }
} 

- will man in einer Funktionsdeklaration auch die Parametertypen deklarieren, dann kann man das so tun, daß man anstatt der Parameter nur deren Typen in der Funktionsdeklaration angibt:

float umf (float)
double flae (float)
int funcbei (int,float,double)

- nach einer solchen Deklaration der Parametertypen ist z.B. lint in der Lage, bei jedem Funktionsaufruf die richtigen Typen der verwendeten aktuellen Parameter sowie deren Anzahl abzuprüfen

zurück zum Inhaltsverzeichnis

6.3 Parameterübergabe

- alle Parameter werden "by value" übergeben, d.h. eine Kopie des Parameterwertes wird übergeben

- das bedeutet, daß Funktionen ihre Parameter nur innerhalb der Funktion, aber nicht im aufrufenden Programm ändern können

Beispielprogramm P6-5.C

main()     /* Beispiel fuer Parameteruebergabe und die
              Aenderbarkeit von Parametern in der Funktion */
{
float radius, neurad(), r;
int i;
i = 10;
radius = 5.23;
printf("Vor Aufruf: I=%2d RADIUS=%6.2f\n",i,radius);
r = neurad(i,radius);
printf("Nach dem Aufruf: I=%2d RADIUS=%6.2f\n",i,radius);
printf("Aber Funktionswert: R=%6.2f\n",r);
}
float neurad(int i, float rad) 
{ 
    i = 0;
    rad = rad * 2;
    printf("In der Funktion: I=%2d RADIUS=%6.2f\n",i,radius);
    return (rad);
} 

- sollen Parameter in der Funktion dauerhaft geändert werden können, so müssen Adressen der zu ändernden Objekte übergeben werden

- wird eine Vektor-(Feld-)name als Parameter übergeben, so wird eigentlich die (Anfangs-)Adresse des Feldes übergeben (deshalb funktioniert z.B. strcat (siehe P4-2.C))

Beispielprogramm P6-6.C

main()     /* Beispiel fuer Parameteruebergabe und die
              Aenderbarkeit von Parametern in der Funktion
                   Anwendung von Pointern */
{
float radius, neurad(), r;
int i;
i = 10;
radius = 5.23;
printf("Vor Aufruf: I=%2d RADIUS=%6.2f\n",i,radius);
r = neurad(&i,&radius);
printf("Nach dem Aufruf: I=%2d RADIUS=%6.2f\n",i,radius);
printf("Aber Funktionswert: R=%6.2f\n",r);
}
float neurad(int *i,float *rad) 
{ 
    *i = 0;
    *rad = *rad * 2;
    printf("In der Funktion: I=%2d RADIUS=%6.2f\n",i,radius);
    return (*rad);
} 

zurück zum Inhaltsverzeichnis

6.4 Speicherklassen und Geltungsbereiche von Namen

- wir haben bisher an verschiedenen Stellen schon einmal die 4 Speicherklassen extern, auto, static und register flüchtig kennengelernt

- im Zusammenhang mit Funktionen ist jetzt ein guter Zeitpunkt, diese Kenntnisse zu vertiefen

1. Speicherklasse auto

- alle Variablen, denen nicht explizit eine Speicherklasse zugewiesen wird und die nicht außerhalb von Funktionen vereinbart werden, fallen in die Speicherklasse auto

- man kann einer Variablen explizit die Speicherklasse auto zuordnen, indem man vor die Typangabe bei der Variablenvereinbarung das Schlüsselwort auto setzt

- automatische Variable werden bei jedem Funktionsaufruf neu erzeugt und beim Verlassen der Funktion wieder zerstört

- daher ist ihr Geltungsbereich auf die lokale Funktion, in der sie vereinbart wurden, beschränkt

- dies gilt auch für Blöcke (Variablen, die innerhalb von Blöcken vereinbart werden und die in die Speicherklasse auto fallen, sind nur lokal in diesem Block bekannt)

- auto Variablen können beliebig (nicht nur mit Konstanten) initialisiert werden

- nicht initialisierte auto Variablen haben einen undefinierten Wert (es existiert kein Weglasswert!)

- auto Vektoren können nicht initialisiert werden

2. Speicherklasse extern

- alle Objekte, die außerhalb von Funktionen vereinbart werden, sind in der Speicherklasse extern

- die Funktionsnamen selbst sind ebenfalls in der Speicherklasse extern

- externe Objekte sind ab ihrer Vereinbarung bis zum Ende des Quellencodes und sogar in anderen Quellencodedateien bekannt

- bei der Vereinbarung eines Objektes als extern kann es sich um eine Definition oder um eine Deklaration handeln

- die folgenden Vereinbarungen (immer außerhalb von Funktionen) unterscheiden sich wesentlich:

int sp = 0;                /* Definition */
double vector[N];
extern int sp;             /* Deklaration */
extern double vector[];

- bei einer Definition wird eine Variable erzeugt

- nur hier ist eine Initialisierung möglich

- fehlt eine Initialisierung, so wird der Weglasswert 0 eingesetzt

- bei einer Deklaration werden nur die Variableneigenschaften festgelegt, die Variable selbst muß an anderer Stelle im Quellencode definiert sein

- Deklarationen dürfen für eine bestimmte Variable mehrmals im (gesamten) Quellencode vorkommen, eine Definition aber nur einmal

Beispielprogramm P6-7.C

# define BUFSIZE 100
char buff[BUFSIZE];     /* Puffer fuer ungetch() */
int bufp = 0;           /* naechste freie Position */
getch()     /* (evtl. zurueckgest.) Zeichen holen */
{ 
    return ((bufp > 0) ? buff[--bufp] : getchar());
} 
ungetch(int c)     /* Zeichen zurueckstellen */
{ 
    if (bufp > BUFSIZE)
        printf("ungetch: Zuviele Zeichen\n");
    else
        buff[bufp++] = c;
} 

3. Speicherklasse static

- static Objekte können sowohl intern als auch extern sein

- static Objekte innerhalb von Funktionen sind nur lokal bekannt, behalten im Gegensatz zu auto Objekten aber ihre Werte zwischen den Funktionsaufrufen bei

- bzgl. der Initialisierung gilt dasselbe wie für externe Objekte, static Vektoren sind daher initialisierbar

- Zeichenketten innerhalb von Funktionen sind immer in der Speicherklasse static (z.B. printf() Parameterstring)

- static Objekte außerhalb von Funktionen sind externe Objekte, deren Namen aber nur in dieser Quellencodedatei bekannt ist

- Beispiele zur Initialisierung eines Vektors:

static int ndigit[10] = { 0,1,2,3,4,5,6,7,8,9 };
char string[] = "Dies ist ein String";

4. Speicherklasse register

- in dieser Speicherklasse können sich einfache Variable befinden

- sie entspricht in ihren sonstigen Eigenschaften der Speicherklasse auto

- der Compiler versucht register Variablen so zu verwenden, daß sie in einem wirklichen Hardwareregister der CPU gehalten werden

- ist dies nicht möglich, so wird register ignoriert und die Variable wie auto behandelt

zurück zum Inhaltsverzeichnis

6.5 Rekursive Funktionen

- Funktionen dürfen zwar nicht geschachtelt sein, aber sie können sich selbst direkt oder indirekt aufrufen

- bei jedem Funktionsaufruf werden neue eigene lokale Variablen erzeugt (außer bei der Speicherklasse static)

- viele Probleme lassen sich kompakt und elegant mittels Rekursion lösen

Beispielprogramm P6-8.C

printd(int n)     /* n dezimal ausgeben */
                  /* 1. Version ohne Rekursion*/
{ 
    char s[10];
    int i;
    if (n < 0) {
        putchar('-');
        n = -n;
    }
    i = 0;
    do {
        s[i++] = n%10 + '0';   /* naechstes Zeichen holen */
    } while ((n /= 10) > 0);   /* und durch Division 
								entfernen */
    while (--i >= 0)           /* in umgekehrter Reihen- */
        putchar(s[i]);         /* folge ausgeben */
} 

Beispielprogramm P6-9.C

printd(int n)     /* n dezimal ausgeben */
                  /* 2. Version  Rekursiv */
{ 
    int i;
    if (n < 0) {
        putchar('-');
        n = -n;
    }
    if ((i = n/10) != 0)
        printd(i);     /* printd rekursiv aufrufen */
    putchar(n%10 + '0');
} 

Beispielprogramm P6-10.C

long fak(int n)     /* Fakultaet berechnen */
{ 
    if (n <= 1)
        return(1);
    else
        return(n*fak(n-1));
}

zurück zum Inhaltsverzeichnis