Inhaltsverzeichnis
Funktionen
Funktionen erlauben uns, auf dem aufzubauen, was andere vor uns programmiert haben.— Brian Kernighan & Dennis Ritchie : Programmieren in C
Funktionen
Ein (prozedurales) Programm ist im wesentlichen eine Ansammlung von Funktionen, die direkt oder indirekt von der Funktion main() aufgerufen werden.
Funktionen gliedern Anweisungsblöcke in kleinere, auch wiederholt ausführbare Einheiten. Sie verbergen Einzelheiten vor dem Nutzer, lassen sich in Module und Bibliotheken zusammenfassen und in mehreren Programmen verwenden.
Die Deklaration gibt die Schnittstelle für den Aufruf der Funktion bekannt. Die von der Funktion abzuarbeitenden Anweisungen werden bei der Definition festgelegt.
Deklaration
Eine Funktion muss vor dem Aufruf mindestens durch Angabe ihres Funktionskopfes (Prototyp) angemeldet (deklariert) werden. Eine Funktion kann mehrfach deklariert werden, sofern die Prototypen übereinstimmen.
Syntax:
Ergebnistyp Funktionsname(
Parameterliste)
throw-Deklaration;
Die Parameterliste enthält, durch Komma getrennt, die Parameter der Funktion, jeweils mit Typ und (optional) Name des Parameters. Bei parameterlosen Funktionen kann die Parameterliste leer bleiben. Dies ist gleichbedeutend mit der Angabe void in der Funktionsklammer.
void faul(); // void faul(void);
Drei Punkte …
stehen für eine
bei der Deklaration unbestimmte Anzahl von Parametern.
Variable Argumentlisten werden mit der Bibliothek
<<cstdarg>>
verarbeitet.
int printf(char const* fmt, ...); // in <cstdio>
Die throw-Deklaration muss nicht angegeben werden (siehe Ausnahmebehandlung).
Definition
Funktionsdefinitionen geben nach dem Funktionskopf den Funktionsrumpf anstelle des Semikolons an. Jede genutzte Funktion muss im Programm genau einmal definiert werden. Funktionsdefinitionen sind nicht ineinander verschachtelbar. Die Reihenfolge der Funktionsdefinitionen im Quelltext ist beliebig (sofern die Funktionen vor ihrem Aufruf deklariert wurden).
Syntax:
Ergebnistyp Funktionsname(
Parameterliste)
throw-Deklaration
{
// Anweisungen
}
double parabel(double x) { return x*x-2*x+4; }
Die Rückkehranweisung
return
Ausdruck ;
beendet die Abarbeitung der Funktion.
Rückkehranweisungen können mehrfach, auch mitten im Funktionsrumpf stehen.
Der Wert des Ausdrucks wird (falls notwendig) in den Ergebnistyp umgewandelt und als Ergebnis an den Aufrufer der Funktion übergeben. Der Aufrufer kann dieses Ergebnis auswerten und weiterverwenden, ist dazu jedoch nicht verpflichtet.
Bei Funktionen mit dem Ergebnistyp void
kann return;
(ohne Ausdruck!) entfallen.
Als inline definierte kleine Funktionen vermeiden aufwendige Funktionsaufrufe. Statt dessen wird der Funktionsrumpf in den Code eingefügt.
inline int minimum(int x, int y) { return x<y? x:y; }
Aufruf
Der Aufruf einer Funktion erfolgt durch Angabe ihres Namens mit der Funktionsklammer. Die Anzahl der Argumente in der Funktionsklammer muss mit der Anzahl der Parameter in der Deklaration übereinstimmen.
double y = 2*parabel(2.71828);
Parameter
In der Parameterliste der Funktionen wird festgelegt, welchen Typ übergebene Werte, Referenzen, Zeiger oder Felder haben.
void funktion(int wert, int& referenz, int* zeiger, int feld[], int vorgabe=0);
Vorgabewerte (engl. default arguments) ergänzen unvollständige Argumentlisten.
Wertparameter
Wertparameter sind innerhalb der Funktion gültige lokale Variablen. Ein Wertparameter erhält beim Aufruf der Funktion eine Kopie des Argumentwertes. Stimmt der Typ des Argumentes nicht mit dem Parametertyp überein, erfolgt eine implizite Typumwandlung. Wertparameter sind innerhalb der Funktion frei änderbar. Ihre Änderung hat keinen Einfluss auf den Wert des übergebenen Argumentes (Input-Parameter).
Referenzparameter
Ein Referenzparameter erwartet eine Variable als Argument,
deren Typ mit dem Parametertyp übereinstimmt.
Für die Dauer des Funktionsaufrufs wird die Referenz
mit dem Argument identifiziert.
Übernommen wird die Adresse (&
) der Argumentvariable.
Damit kann der Wert der Argumentvariable
aus der Funktion heraus geändert werden (Output-Parameter).
Durch const-Deklaration
kann die unabsichtliche Veränderung des Argumentes unterbunden werden
(Input-Parameter).
Zeigerparameter
Zeigerparameter übernehmen eine (gültige?) Speicheradresse,
über die mit dem Inhaltsoperator *
lesend oder schreibend auf den Speicher zugegriffen werden kann.
Als Argument ist ein zum Parameter typgleicher Zeiger einzusetzen.
Zeigerparameter können sowohl auf Einzelwerte als auch auf Felder
verweisen.
Das (unabsichtliche) Ändern des verwiesenen Speicherplatzes kann durch const-Deklaration des Zeigers unterbunden werden. Der Zeiger selbst ist eine Kopie und damit in der Funktion frei änderbar, z.B. um durch das Feld zu wandern.
Feldparameter
Felder werden als Zeiger auf das Anfangselement übernommen. Sie haben damit Referenzverhalten. Größenangaben bei Feldparametern haben lediglich Informationswert für den Nutzer. Feldparameter können auch als Zeiger deklariert werden.
Durch const-Deklaration des Feldes verpflichtet sich die Funktion, den Feldinhalt nicht zu ändern.
Vorgabewerte
Parameter können mit Vorgabewerten versehen werden. Danach dürfen keine Parameter ohne Vorgabewert mehr folgen.
float dezimal(int zaehler=0, int nenner=1) { return float(zaehler)/nenner; } float null = dezimal(); float zwei = dezimal(2); float einhalb = dezimal(1,2);
Beim Aufruf werden die übergebenen Argumente links beginnend den Parametern zugeordnet. Für die fehlenden Argumente werden die Vorgabewerte eingesetzt.
Ein- und/oder Ausgabe
Je nach der beabsichtigten Richtung des Datenflusses wird unterschieden in
- Input (Eingabe in die Funktion),
- Output (Ausgabe aus der Funktion),
- In & Output (Ein- und Ausgabe).
Die Art der Übergabe sollte sich danach richten, wie groß die Übergabekosten beim Kopieren oder Verschieben des Datentyps X
sind:
billig / nicht kopierbar ( int , std::unique_ptr<T> ) | leicht / erträglich verschiebbar ( std::string , std::vector<T> ) | teuer ( BigBlob ) |
|
---|---|---|---|
Output | X f() | f(X& x) |
|
In-/Output | f(X& x) |
||
Input | f(X x) | f(X const& x) |
|
- Kopie behalten | -"- | f(X const& x) , f(X&& x) + std::move(x) | |
- Verschieben | f(X&& x) |
Die Unterscheidung zwischen Kopieren und Verschieben (Weitergabe-Referenz X&&
, untere 2 Zeilen) ist ab C++11 möglich, um optimales Laufzeitverhalten zu erreichen.
Zeigerparameter an Stelle von Referenzparametern erweitern die Möglichkeiten1):
- Ein Zeiger kann auf ein Objekt zeigen (oder auch nicht). Es liegt in der Verantwortung des Programmierers zu entscheiden, ob dies zu prüfen ist.
- Ein nicht-konstanter Zeiger kann über einen Speicherbereich wandern. Hier sollte klar sein, wo aufzuhören ist.
Funktionen überladen
Gleichnamige Funktionen mit unterschiedlicher Parameterliste werden überladene Funktionen genannt.
int minimum(int x, int y) { return x<y? x:y; } double minimum(double x, double y) { return x<y? x:y; }
Funktionsschablonen definieren einen ganzen Satz von gleichnamigen Funktionen, bei denen mindestens ein Typ eines Parameters bis zum Funktionsaufruf offen gelassen wird (Beispiel template).
template <class T> T minimum(T x, T y) { return x<y? x:y; }
Der Compiler prüft beim Übersetzen eines Funktionsaufrufes Anzahl und Typ der Argumente und vergleicht diese mit den bekanntgemachten Prototypen. Damit ist bestimmbar, welche überladene Funktion aufgerufen werden soll.
std::cout << minimum(1, 3) << '\n'; // minimum(int x, int y) std::cout << minimum(1.0, 3.0) << '\n'; // minimum(double x, double y) std::cout << minimum('a', 'b') << '\n'; // minimum<>(char x, char y)
Existiert eine spezielle Funktion, so hat diese Vorrang vor der Schablone. Findet sich kein Kandidat oder kommen mehrere in Betracht, so ist das ein Fehler.
main()-Funktion
Jedes Programm enthält eine Hauptfunktion. (In plattformspezifischen Programmiersystemen kann diese Funktion unzugänglich sein.)
int main(int argc, char *argv[]) { // ... return statuscode; }
Nach Initialisierung globaler Variablen beginnt der Programmablauf in dieser Funktion. Das Programm endet mit der Rückgabe eines ganzzahligen Statuscodes an das aufrufende Programm (Betriebssystem). Ein Programm mit dem Ergebnis 0 gilt als erfolgreich beendet. Im Rückgabewert kann eine Fehlerdiagnose codiert werden.
Die traditionell [in K&R] als argc
(argument counter)
und argv
(argument vector) benannten Parameter
enthalten die Anzahl der Wörter und Zeiger auf die Wortanfänge
der Kommandozeile, mit der das Programm gestartet wurde:
rename oldname newname ^ ^ ^ argv[0] argv[1] argv[2] argc==3
In argv[0]
steht der Programmname.
Werden Kommandozeilenparameter nicht benötigt,
kann int main()
parameterlos definiert werden.
Nachfolgender Ergebnistyp
In Kombination mit dem Schlüsselwort auto ist die Angabe eines trailing return type möglich (C++11).
auto sum(int x, int y) -> int { return x+y; };
Auch der Ergebnistyp eines Ausdrucks kann eingesetzt werden:
auto product(int x, int y) -> decltype(x*y) { return x*y; };
Ab C++14 kann der Compiler den Rückgabetyp selbst erschließen, sofern sich nicht mehrere Rückkehranweisungen widersprechen:
auto product(int x, int y) { return x*y; };
Anonyme Funktionen
Ein Lambda-Ausdruck [capturelist](parameterliste) → ergebnistyp { Anweisungen }
definiert ein Funktionsobjekt zur sofortigen oder späteren Verwendung.
auto twice = [](int x) { return 2+x; }; int n = twice(3);
Auch hier kann der Compiler den Ergebnistyp selbst erschließen.
Lambda-Ausdrücke beginnen mit einer eckigen Klammer.
Darin ist anzugeben, welche lokalen Variablen der Umgebung im Lambda-Audruck bekannt sein sollen.
Dieses Einbeziehen der Umgebung wird Einschluss (Closure) genannt.
Die Angabe [=]
übernimmt alle Werte, [&]
alle Variablen als Referenz.
Auch Variablenlisten und gemischte Angaben sind möglich.
In Methoden definierte Lambdas können [this]
einschließen, um an Attribute des umgebenden Objektes heranzukommen.
Lambda-Ausdrücke sind u.a. in Algorithmen nützlich, die Funktionen/Funktoren bzw. Prädikate als Parameter erwarten. Mit unmittelbar aufgerufenen Lambda-Ausdrücken (IILE) lassen sich komplexere konstante Daten initialisieren:
auto const squares = [max = 10]() // "0" "1" "4" ... "81" { std::vector<std::string> v; for (int i = 0; i < max; ++i) v.push_back(std::to_string(i*i)); return v; }(); // <- Aufruf hier