Teil 14 „Subroutines“

Der C-Kurs
Antworten
Benutzeravatar
bodo
User
Beiträge: 319
Registriert: 14.02.2007, 17:21
Kontaktdaten:

Teil 14 „Subroutines“

Beitrag von bodo » 13.11.2010, 13:54

C für BASIC-Programmierer

Arbeiten mit dem z88dk Cross-Compiler

von Jens Sommerfeld und Bodo Wenzel

Teil 14

Bisher waren wir auf ein „flaches“ Programm beschränkt und durften nur Funktionen aufrufen, die andere Leute geschrieben hatten. In diesem Teil geht es um „Subroutines“, also Unterprogramme. Manche Programmiersprachen unterscheiden zwischen Prozeduren und Funktionen, bei denen erstere nie einen Wert zurückgeben, letztere aber immer. Bei C werden alle Unterprogramme als Funktionen bezeichnet.

Weil C ja keine Zeilennummern kennt, gibt es einen anderen, natürlicheren Weg, eine Funktion zu kennzeichnen. Sie bekommt einfach einen Namen. Mit diesem Namen wird sie schließlich auch aufgerufen.

Was macht nun eine Funktion aus? Dazu schauen wir uns die Funktion an, die wir immer schreiben, wenn wir ein C-Programm schreiben:

Code: Alles auswählen

int main(void) {
    puts("HALLO WELT.");

    return 0;
}
Die „sichtbare“ Schnittstelle, also das, was ein Aufrufer wissen muss, nennt man footprint, weil es gewissermaßen der Fußabdruck der Funktion ist. Er besteht aus:
  • Datentyp des Rückgabewertes
  • Name der Funktion
  • Argumentenliste (je Argument ein Datentyp und optional ein Name)
Um eine Funktion dem Compiler bekanntzumachen, ohne sie ganz zu definieren, gibt man ihm für jede aufzurufende Funktion eine Deklaration in Form eines Prototypen. Diese sind z.B. in den Headerdateien wie „stdio.h“ enthalten. Für unsere Hauptfunktion lautet der Prototyp:

Code: Alles auswählen

int main(void);
Der Unterschied dieser Deklaration zur Definition ist, dass nach der Argumentenliste in runden Klammern kein Anweisungsblock in geschweiften Klammern folgt, sondern einfach nur ein Semikolon.

Ein einfaches Beispiel

Nehmen wir an, wir brauchen in einem Programm die Signumfunktion: sie liefert +1, wenn das Argument positiv ist, 0, wenn das Argument Null ist, und -1, wenn das Argument negativ ist.

Damit können wir bereits den Prototypen hinschreiben, unter der Annahme, dass nur ganze Zahlen als Argumente übergeben werden:

Code: Alles auswählen

int sgn(int);
Jetzt fehlt uns nur noch die Implementierung, aber das ist ja einfach. Bei der Definition einer Funktion müssen wir natürlich den Argumenten Namen geben, die wir bei der Deklaration noch weglassen durften:

Code: Alles auswählen

int sgn(int zahl) {
    int ergebnis;

    if (zahl < 0) {
        ergebnis = -1;
    } else if (zahl == 0) {
        ergebnis = 0;
    } else {
        ergebnis = +1;
    }

    return ergebnis;
}
Jetzt bauen wir das in ein Testprogramm ein, denn wir sind ja sorgfältige Programmierer:

Code: Alles auswählen

#include <stdio.h>

int sgn(int);

int main(void) {
    printf("sgn(%d) = %d\n", -999, sgn(-999));
    printf("sgn(%d) = %d\n", -1, sgn(-1));
    printf("sgn(%d) = %d\n", 0, sgn(0));
    printf("sgn(%d) = %d\n", +1, sgn(+1));
    printf("sgn(%d) = %d\n", +999, sgn(+999));

    return 0;
}

int sgn(int zahl) {
    int ergebnis;

    if (zahl < 0) {
        ergebnis = -1;
    } else if (zahl == 0) {
        ergebnis = 0;
    } else {
        ergebnis = +1;
    }

    return ergebnis;
}
Wie ihr seht, wird eine Funktion durch Angabe ihres Namens mit einem Paar runder Klammern aufgerufen. In den Klammern werden die aktuellen Parameter angegeben. Falls eine Funktion einen Wert zurückgibt, kann dieser in einem Ausdruck verwendet werden.

Die Reihenfolge, in der ihr die einzelnen Funktion im Quelltext ablegt, ist prinzipiell egal. Der Compiler muss nur bereits die Footprints kennen, wenn er die Aufrufe sieht. Viele Programmierer schreiben daher am Anfang die Prototypen hin, dann die Hauptfunktion, und dann die anderen Funktionen, sinnvoll geordnet.

Mehr Details

Lasst uns die einzelnen Punkte noch einmal genauer betrachten. Denn das Aufteilen einer Problemlösung in die richtigen Funktionen (und Daten, aber das heißt dann schon fast „objektorientierte“ Entwicklung) ist das A und O der Programmierung. Ein Programm in Funktionen zu teilen heißt, dem Prinzip „Teile und Herrsche!“ zu folgen. Ein gut entworfenes Programm erschließt sich dem Leser praktisch von selbst. Er muss nicht in die Funktionsdefinitionen schauen, um herauszubekommen, was die aufgerufenen Funktionen machen.

Der Rückgabewert einer Funktion kann z.B. direkt das gewünschte Ergebnis sein. Wenn mehrere Werte zurückzugeben sind, brauchen wir andere Mechanismen, die wir in späteren Teilen kennenlernen. Ansonsten kann eine Funktion zweitrangige Informationen zurückgeben, wie printf() beispielsweise die Anzahl ausgegebener Zeichen zurückgibt. Recht häufig ist auch die Rückgabe von Erfolg oder Misserfolg. Wenn eine Funktion nichts zurückgibt, muss als Datentyp void angegeben werden.

Code: Alles auswählen

void putchar(int); /* bekommt ein Zeichen, liefert nichts */
int getchar(void); /* bekommt nichts, liefert ein Zeichen */
Der Name einer Funktion sollte so gewählt werden, dass ihre Funktionalität direkt erkennbar ist. Viele Programmierrichtlinien schreiben vor, dass das erste Stück einem Verb in Befehlsform entspricht. Als Zeichen sind die üblichen Verdächtigen erlaubt, also kleine und große Buchstaben, Ziffern (aber nicht als erstes Zeichen), und der Unterstrich. Laut ISO müssen mindestens die ersten sechs Zeichen zur Unterscheidung herangezogen werden, die meisten Compiler verwenden alle Zeichen. Wie immer werden große und kleine Buchstaben unterschieden! Beispiele:

Code: Alles auswählen

int v24_open(void);
void v24_close(void);
int v24_receive_character(void);
int v24_send_character(int);
Wenn eine Funktion Argumente bekommt, müssen beim Prototyp mindestens deren Datentypen angegeben werden. Mehr ist nämlich für den Compiler nicht interessant, im fertigen Programm existieren die Namen sowieso nicht mehr. Bei der Definition einer Funktion müssen den Argumenten auch Namen gegeben werden, damit sie im Anweisungsblock referenziert werden können. Wenn eine Funktion keine Parameter bekommt, ist es guter Stil, in die runden Klammern ein void zu schreiben.

Einige Programmiersprachen unterscheiden die Übergabe von Argumenten by value oder by reference. Dabei bedeutet by value, dass vom eigentlichen Argument der Wert berechnet wird und als Kopie an die Funktion übergeben wird. Wenn dagegen ein Argument by reference übergeben wird (Dies geht wohl nur mit Variablen des Aufrufers.), wird stattdessen direkt die Variable in der aufgerufenen Funktion verwendet. Deren Inhalt kann dabei auch verändert werden!

In C braucht ihr euch darum keine Sorgen zu machen. Alle Argumente werden by value übergeben, und ihr könnt sie wie lokale Variablen betrachten. Das folgende Beispielprogramm zeigt das:

Code: Alles auswählen

#include <stdio.h>

void function(int parameter) {
    printf("FUNCTION START, PARAMETER = %d\n", parameter);
    parameter /= 2;
    printf("FUNCTION ENDE, PARAMETER = %d\n", parameter);
}

int main(void) {
    int variable;

    variable = 81;
    printf("MAIN START, VARIABLE = %d\n", variable);
    function(variable);
    printf("MAIN ENDE, VARIABLE = %d\n", variable);

    return 0;
}
In diesem Beispielprogramm ist auch die Reihenfolge von main und der aufgerufenen Funktion umgekehrt im Vergleich zum ersten Beispiel. Weil deshalb beim Aufruf der Funktion der Footprint bereits bekannt ist, kann eine Zeile mit dem Prototypen fehlen.

Die Anweisungen innerhalb einer Funktion realisieren die gewünschte Funktionalität. Sie sind für den Aufrufer, also den Benutzer der Funktion, eigentlich uninteressant, Hauptsache, sie machen das Richtige(TM).

Der Aufruf einer Funktion geschieht wie beschrieben durch Angabe des Namens und eines Paares runder Klammern. Die Anzahl der Argumente sowie ihre Typen müssen mit dem Prototypen zusammenpassen, wobei C bestimmte Typumwandlungen (Alle ganzzahligen Zahlen sind zum Beispiel ineinander umwandelbar; dabei können natürlich unerwünschte Nebenwirkungen auftreten, wie Zahlenüberläufe.) erlaubt und dann selbst durchführt. Wenn eine Funktion einen Wert zurückgibt, muss dieser nicht unbedingt verwendet werden. Er kann auch getrost ignoriert werden, mit den entsprechenden Konsequenzen. ;-)

Eine Funktion kann nur einen „Eingang“ haben, denn sie hat ja auch nur einen Namen, der beim Aufruf angegeben werden kann. Allerdings kann eine Funktion jederzeit mit return verlassen werden. Wenn die Funktion einen Wert zurückgibt, wird dieser direkt nach dem return hingeschrieben, dabei sind auch komplexe Ausdrücke möglich. Die Signumfunktion kann also auch so geschrieben werden:

Code: Alles auswählen

int sgn(int zahl) {
    if (zahl < 0) {
        return -1;
    } else if (zahl == 0) {
        return 0;
    } else {
        return +1;
    }
}
Im Gegensatz zu BASIC sind keine Vorkehrungen nötig, damit ein Hauptprogramm nicht in eine Funktion „hineinläuft“. Denn das jeweils äußerste Paar geschweifter Klammern begrenzt den Umfang der jeweiligen Funktion.

Hausaufgaben
  1. Schreibt den Prototypen einer Funktion mit drei char-Argumenten, die einen int zurückgibt. Sie soll „tu_es“ heißen.
  2. Wie lautet der Prototyp einer Funktion namens „drucke“, die eine Ganzzahl als Argument bekommt und nichts zurückgibt?
  3. Was ist an folgendem Quelltext falsch? Tipp: es sind mehrere Fehler... Korrigiert das Programm.

    Code: Alles auswählen

    #include <stdio.h
    
    void print_msg(void);
    
    int main(void) {
        print_msg("AUSZUGEBENDER TEXT");
    
        return 0;
    }
    
    void print_msg(void) {
        puts("AUSGEGEBENER TEXT");
    
        return 0;
    }
  4. Schreibt eine Funktion, die zwei ganze Zahlen als Parameter bekommt und die erste durch die zweite teilt. Der Quotient soll zurückgegeben werden. Sorgt dafür, dass nicht durch 0 geteilt wird, und macht euch Gedanken, welches Ergebnis dann zurückgegeben werden soll.
  5. Schreibt ein Testprogramm für die Funktion aus Aufgabe 4.
B0D0: Real programmers do it in hex.

Antworten