Funktionen
Willemers Informatik-Ecke

Mathematik

Sie kennen vermutlich Funktionen aus der Mathematik. Der Aufruf von Funktionen lehnt sich stark an dieses Vorbild an. So wird beispielsweise der Aufruf der Sinusfunktion im Programm genau so formuliert, wie Sie es aus der Mathematik kennen:

a = sin(alpha);
Dabei ist sin der Funktionsname. alpha ist der Parameter, den die Funktion vom Aufrufer übergeben bekommt, und die Funktion liefert ihr Ergebnis an die Variable a. Wie der Sinus eines Winkels berechnet wird, ist aus Sicht des Aufrufers nicht zu erkennen. Diese Details sind in der Funktion zusammengefasst. Im Grunde ist es für den Aufrufer eigentlich auch gar nicht interessant, wie der Sinus berechnet wird. Er weiß, was ein Sinus ist, und möchte gern das Ergebnis haben.

Anweisungsblock mit Namen

Wenn Sie eine Funktion selbst schreiben, fassen Sie mehrerer Anweisungen unter einem Namen zusammen. Man nennt das auch ein Unterprogramm. Eine solche Funktion sollte möglichst so gebildet werden, dass sie eine klar abgegrenzte Aufgabe hat. Die Funktion lässt sich über ihren Namen von jeder beliebigen Stelle des Programms aus beliebig oft aufrufen. Nach Ausführung der Funktion kehrt das Programm an den Ort zurück, von wo aus sie aufgerufen wurde, und setzt die Verarbeitung in der nächsten Anweisung fort.

main()

Eine Funktion war bereits in allen bisher betrachteten Programmen vorhanden: die Funktion namens main(). Diese Funktion wird vom Betriebssystem aufgerufen, um das Programm zu starten. Sie können aber weitere Funktionen hinzufügen und diese aus der Funktion main() heraus aufrufen. Innerhalb der Funktionen können wiederum andere Funktionen aufgerufen werden.

Rückgabewert

Eine Funktion liefert typischerweise einen Rückgabewert. Dieser Wert wird beim Beenden der Funktion mit Hilfe des Befehls return an den Aufrufer gesandt. Welchen Typ der Rückgabewert hat, wird bei der Definition der Funktion angegeben. Dieser von der Funktion zurückgegebene Wert kann einer Variablen zugewiesen werden.

Beispiel

Als erstes Beispiel schreiben wir eine Funktion namens naechste(). Diese Funktion gibt bei jedem Aufruf eine Zahl zurück, die um eins höher ist als beim letzten Aufruf. Zum Zählen wird eine globale Variable namens zaehler eingesetzt, die innerhalb der Funktion inkrementiert wird. Anschließend wird deren Wert mit dem Befehl return zurückgegeben. Zum Testen wird die Funktion mehrfach aufgerufen und der Wert auf dem Bildschirm angezeigt.

[Funktion naechste (naechste.cpp)]

#include <iostream> 
using namespace std;
int zaehler=0; // diese Variable hält den Zählerstand

int naechste()
// aendert die globale Variable zaehler
{
    zaehler++; // erhöht die globale Variable
    return zaehler;
}

int main()
{
    int n;
    n = naechste();
    cout << n << endl;
    naechste();         // hier wird n nicht verändert!
    cout << n << endl;
    n = naechste();
    cout << n << endl;
}

Rückgabetyp

Die Definition der Funktion naechste() beginnt mit dem Rückgabetyp int. Der Typ des Rückgabewertes ist weit gehend frei wählbar. Er darf lediglich kein Array sein, da ein Array kein L-Value ist. Während in C der Rückgabewert einer Funktion weggelassen werden kann und dann automatisch als int angenommen wird, ist dies in ANSI-C++ nicht erlaubt, wird aber von einigen Compilern toleriert. Ein fehlender Rückgabetyp wird vom GNU-Compiler als Fehler, von Visual C++ dagegen als Warnung gemeldet. Soll eine Funktion keinen Rückgabewert liefern, gibt der Programmierer void als Typ an.

Funktionsname

Auf den Rückgabetyp folgt der Name der Funktion. Er folgt den gleichen Spielregeln wie die Namen von Variablen Er beginnt mit einem Buchstaben oder einem Unterstrich. Es folgen beliebig viele Buchstaben, Ziffern oder Unterstriche. Groß- und Kleinschreibung sind signifikant, werden also unterschieden. Es lohnt sich, einige Zeit in die Suche nach einem treffenden Namen zu investieren. Er sollte das Programm leicht lesbar machen und gleichzeitig leicht zu merken sein, damit Sie beim Aufruf der Funktion nicht jedes Mal den Namen nachschlagen müssen.

Parameter

Dem Funktionsnamen folgt ein Klammernpaar, das Parameter enthalten kann. Hat die Funktion keine Parameter, bleibt die Klammer leer oder enthält das Schlüsselwort void. Was Parameter sind und wie sie aufgebaut sind, wird später behandelt.

Funktionsrumpf

In geschweiften Klammern folgt der Funktionsrumpf. Das ist ein Anweisungsblock, der die Befehle enthält, die ausgeführt werden, wenn die Funktion aufgerufen wird. In diesem Block kann der Befehl return die Funktion beenden. Sofern die Funktion einen Rückgabewert hat, muss dem Befehl return ein Wert folgen.

Der Syntaxgraph einer Funktionsdefinition ist in Abbildung (graffunction) zu sehen.

Die Grafik "Syntaxgraph für die Funktionsdefinition" (graffunction) steht im Buch an dieser Stelle.

Rückgabetyp
Er kann ein beliebiger Typ außer einem Array sein. Alternativ werden Funktionen ohne Rückgabewert durch void gekennzeichnet. Im Gegensatz zu C muss der Typ in C++ explizit genannt werden.
Funktionsname
Er beginnt mit einem Buchstaben oder einem Unterstrich. Danach folgen Buchstaben, Ziffern und Unterstriche in beliebiger Reihenfolge. Er folgt also dem Syntaxgraph für Bezeichner
Parameter
Eine Funktion kann keinen oder fast beliebig viele Parameter haben, die durch Kommata getrennt sein müssen.[1] Ein Parameter gleicht einer Variablendefinition. Diese wird in den nächsten Abschnitten näher erläutert.
Block
Das ist die Folge von Anweisungen, die den Inhalt einer Funktion ausmacht. Diese Befehle werden durchlaufen, wenn eine Funktion aufgerufen wird.

Aufruf

Der Aufruf einer Funktion erfolgt durch Nennung des Funktionsnamens. An den Funktionsnamen schließt sich immer ein Klammerpaar an, das gegebenenfalls auch Parameter enthalten kann. Dieses Klammerpaar ist zwingend erforderlich. Die Parameter des Aufrufs müssen zu den Parametern der Funktion passen.

Besitzt die Funktion einen Rückgabewert, kann der Funktionsaufruf als Ausdruck verwendet werden. Er kann also beispielsweise auf der rechten Seite einer Zuweisung stehen.

void

Das nächste Beispiel zeigt eine Funktion ohne Rückgabewert. Sie soll eine Trennlinie auf dem Bildschirm ausgeben, wenn sie aufgerufen wird. Dabei darf der Rückgabetyp keineswegs einfach weggelassen werden. Stattdessen muss hier das Schlüsselwort void angegeben werden. Das englische Wort void bedeutet so viel wie »leer«, »bar« oder »nichtig«.

[Einfache Funktion trennlinie]

void trennlinie()
{
    cout << "-------------------------" << endl;
    return;
}

int main()
{
    trennlinie();
    cout << "Programm zur Ermittlung..." << endl;
    trennlinie();
}

return

In einer Funktion ohne Rückgabewert hat der Befehl return kein Argument. Er beendet allerdings auch hier die Funktion. Im Beispiel steht allerdings der Befehl return direkt vor dem Funktionsende. Da die Funktion sowieso hier beendet wird, könnte er auch weggelassen werden. Die Definition der Funktion trennlinie() erfolgt vor ihrem ersten Aufruf. Das ist eine sinnvolle Vorgehensweise, weil der Compiler so die Funktion bereits vor ihrem ersten Aufruf kennt und prüfen kann, ob der Parameter und der Name in Ordnung sind. Ist das einmal nicht möglich, müssen Sie einen Prototyp vor dem Aufruf deklarieren. Informationen finden Sie weiter unten. In der Funktion main() wird die Funktion trennlinie() zweimal aufgerufen. An der Stelle des Aufrufs wird die Funktion einmal durchlaufen und kehrt hinter die Aufrufposition zurück.

Hintergrund

Rücksprungadresse

Wie kommt es eigentlich, dass das Programm nach dem Aufruf einer Funktion immer die richtige Rücksprungadresse findet? Dafür besitzt jedes Programm einen so genannten Stack (engl. Stapel). Das ist eine Speicherstruktur, in der Daten abgelegt werden können, die beim Lesen in umgekehrter Reihenfolge wieder heruntergenommen werden. Einen solchen Stapel können Sie mit Büchern simulieren. Wenn Sie diese aufeinander stapeln, können Sie immer nur das oberste Buch lesen, und Sie bekommen die Bücher in der umgekehrten Reihenfolge zurück, wie sie aufgestapelt wurden. In dieser Weise wird bei jedem Aufruf einer Funktion die Herkunftsadresse auf den Stack gelegt. Wenn eine Funktion endet, wird die letzte Adresse vom Stack wieder heruntergeladen und dorthin zurückgekehrt. Egal wie verschachtelt die Aufrufe auch sein mögen, die Rücksprünge sind gesichert.

Debugger

Aus diesen Stackinformationen kann im Falle eines Absturzes ein Debugger entnehmen, über welche Funktionsaufrufe ein Programm seinen Weg in die ewigen Jagdgründe gefunden hat.

Qualitätsverbesserung

Die Möglichkeit, Abläufe, die an verschiedenen Stellen im Programm gebraucht werden, in einer Funktion zusammenzufassen und mehrfach aufzurufen, führt zu einer Verringerung der Fehlerquellen. Denn so sind Fehler ausgeschlossen, die beim Kopieren des Quelltexts an andere Stellen auftreten können. Von einer Korrektur an einer Funktion profitieren alle Programmteile, die diese Funktion aufrufen. Es kann aber durchaus sinnvoll sein, Funktionen zu bilden, auch wenn sie nur einmal aufgerufen werden. Funktionen dienen auch als Ordnungsmittel, um umfangreiche Detailprobleme in mehrere, überschaubare Aufgaben zu untergliedern. Dazu kommt, dass die Abgeschlossenheit einer Funktion die Übersicht erhöht und schon dadurch die Fehlerzahl vermindert.
[1]
Der eine oder andere Compiler kann Beschränkungen bezüglich der Anzahl der Parameter aufweisen. Allerdings sind allzu viele Parameter auch ein Zeichen schlechten Entwurfs.