C++ Tutorial

Funktionen

Funktionen werden hauptsächlich aus folgenden Gründen eingesetzt:

  • Öfters benötigte Anweisungen müssen nur einmal geschrieben werden und können dann beliebig oft ausgeführt werden.
  • Erst mithilfe von Funktionen ist es möglich, ein größeres Programm in kleinere, logische Teile zu unterteilen.

Syntax

Eine Funktion hat folgenden Aufbau:

RTYP FktName ([PARAMETER])
{
    ANWEISUNGEN
    [return RWERT;]


FktName ist der Name der Funktion und RTYP definiert den Datentyp des Wertes RWERT, den die Funktion zurückliefert. Nach dem Funktionsnamen stehen innerhalb einer Klammer die Daten PARAMETER, die an die Funktion übergeben werden. D. h., eine Funktion kann mehrere Daten erhalten aber nur ein Datum zurückliefern.

 Die Angaben in den Klammern [...] sind optional. Nach der geschweiften Klammer am Ende einer Funktion steht kein Semikolon.

Funktionsdefinition

Der Name einer Funktion muss eindeutig sein, d.h., es darf keine weitere Funktion, Variable usw. mit dem gleichen Namen geben. Ausnahmen: Überladene Funktionen, Funktionen in einem anderen Namensraum oder inline-Funktionen, die in einem späteren Kapitel behandelt werden

Die von einer Funktion auszuführenden Anweisungen werden in einen Block {...} eingeschlossen. Innerhalb einer Funktion können, bis auf eine Ausnahme, beliebige Anweisungen stehen. Nicht erlaubt ist, innerhalb einer Funktion eine weitere Funktion zu definieren.

Der einfachste Fall einer Funktion ist eine Funktion, die keine Daten erhält und kein Ergebnis zurückliefert. Eine Funktion, die kein Ergebnis zurückliefert, besitzt den Returntyp void. Und da die Funktion keine Daten erhält, bleibt die Parameterklammer leer.

1: #include <iostream>
2:
3: // Definition der Funktion
4: void PrintHeader()
5: {
6:    std::cout << "Seitenüberschrift\n";
7: }
8:
9: // main() Funktion
10: int main()
11: {
12:    ...
13: }

Werden innerhalb einer Funktion Daten definiert, sind diese nur innerhalb der Funktion gültig.

1: void Func1()
2: {
3:    // Definition von Variablen/Konstanten die nur
4:    // innerhalb dieser Funktion gültig sind
5:    int temp;
6:    constexpr auto PI = 3.1416;
7:    ...
8: }

Funktionsaufruf

Fehlt noch die Anweisung um die Funktion aufzurufen. Der Aufruf der Funktion erfolgt durch Angabe des Funktionsnamens, gefolgt von einer Klammer () mit den optionalen Argumenten und einem abschließenden Semikolon.

1: #include <iostream>
2: // Definition der Funktion
3: void PrintHeader()
4: {
5:    std::cout << "Tabellenüberschrift\n";
6: }
7:
8: // main() Funktion
9: int main()
10: {
11:    // Funktion aufrufen
12:    PrintHeader();
13: }

Funktionsdeklaration (Signatur)

Bevor eine Funktion aufgerufen werden kann, muss sie entweder definiert oder deklariert sein. Der Unterschied zwischen einer Funktionsdefinition und einer Funktionsdeklaration ist folgender:

  • Die Funktionsdefinition ist die Codierung der Funktion, d.h., sie enthält die Anweisungen, die die Funktion ausführt.
  • Die Funktionsdeklaration enthält keinen Code, sondern lediglich den Funktionskopf, bestehend aus Returntyp, Funktionsname und den optionalen Parametern.
1: // Funktionsdeklarationen
2: void PrintHeader();
3: // Funktionstyp wird gleich noch erklaert
4: auto MinVal(short val1, short val2) ->decltype(val1);

Funktionsparameter

Parameter im Allgemeinen

Mithilfe von Parametern können Daten an eine Funktion übergeben werden.

Werden an Daten eine Funktion übergeben, sind bei der Deklaration der Funktion zumindest die Datentypen der Parameter anzugeben. Zusätzlich können (und sollten auch!) die Parameter nach dem Datentyp mit einem sinnvollen Namen versehen werden.

Benötigt eine Funktion mehrere Parameter, sind diese durch Komma voneinander zu trennen. Im nachfolgenden Beispiel erhält die Funktion PrintIt() im ersten Parameter die X-Position, im zweiten Parameter die Y-Position und im dritten Parameter den Text für die Ausgabe.

1: void CalcSqrt (double); // gleichbedeutend mit
2:                         // void CalcSqrt (double val);
3: void PrintIt (short xPos, short yPos, const char *pText);

Bei der Definition der Funktion sind, genauso wie bei der Deklaration, die Datentypen der Parameter anzugeben, nun jedoch zwingend gefolgt von Parameternamen. Innerhalb der Funktion wird über die Parameternamen auf die an die Funktion übergebenen Daten zugegriffen. Die Parameternamen bei der Deklaration und der Definition der Funktion müssen nicht zwingend übereinstimmen, der besseren Lesbarkeit wegen sollten sie es aber.

Im Beispiel ist eine Funktion für die Ausgabe eines Seitenkopfes aufgeführt.

1: // Funktionsdeklaration
2: void PrintHeader(int pageNum);
3:
4: // main() Funktion
5: int main ()
6: {
7:    // gleich noch Seitenüberschrift ausgeben
8:    ...
9: }
10: // Definition der PrintHeader-Funktion
11: // Erhaelt die Seitennummer als Parameter
12: void PrintHeader(int pageNum)
13: {
14:    std::println("Seite {}", pageNum);
15: }

Die Reihenfolge und Anzahl der Datentypen der Parameter bei der Funktionsdeklaration und -definition müssen übereinstimmen!

Prinzipiell gibt es zwei Arten von Parameter: call-by-value Parameter und Referenzparameter.

call-by-value Parameter

Bei einem Parameter vom Typ call-by-value erhält die aufgerufene Funktion eine Kopie des beim Funktionsaufruf angegebenen Datums. Daraus folgt, dass Änderungen des Parameterwerts sich nur auf die Kopie auswirken. Die Definition eines call-by-value Parameters erfolgt in der Art, dass innerhalb der Parameterklammer der Funktion lediglich der Datentyp und Name des Parameters angegeben wird.

1: // Funktionsdeklaration
2: void PrintHeader(int pageNum);
3:
4: // main() Funktion
5: int main ()
6: {
7:     auto page = 1; // Seitennummer initialisieren
8:     PrintHeader(page); // Funktion aufrufen
9:     // page hat hier immer noch den Wert 1
10:    PrintHeader(10); // Funktionsaufruf mit Konstante
11: }
12: // Definition der PrintHeader-Funktion
13: void PrintHeader(int pageNum)
14: {
15:    std::println("Seite {}", pageNum);
16:    pageNum = 99; // Parameter verändern
17: }

Beim Aufruf der Funktion kann für einen call-by-value Parameter entweder eine Variable (Zeile 8) oder eine Konstante (Zeile 10) an die Funktion übergeben werden.

Referenzparameter

Ein Referenzparameter ermöglicht Funktionen, ein übergebenes Datum dauerhaft zu verändern, sodass innerhalb der Funktion durchgeführte Änderungen an einem Datum auch nach dem Verlassen der Funktion gültig sind.

Um einen Parameter als Referenzparameter zu kennzeichnen, wird bei der Deklaration und der Definition der Funktion nach dem Datentyp des Parameters der Operator & angefügt. Beim Aufruf der Funktion wird ein Referenzparameter wie ein Parameter vom Typ call-by-value übergeben.

Nachfolgend eine Funktion Swap(), welche die Inhalte der übergebenen Daten vertauscht.

1: // Funktionsdeklaration
2: void Swap (short& val1, short& val2);
3:
4: // main() Funktion
5: int main ()
6: {
7:     short var1, var2;
8:     ...
9:     Swap (var1, var2);
10:    ...
11: }
12:
13: // Funktionsdefinition
14: // Vertauscht die Inhalte der übergebenen Variablen
15: void Swap (short& val1, short& val2)
16: {
17:    auto temp = val1;
18:    val1 = val2;
19:    val2 = temp;
20: }

Konstante Referenzparameter

In vielen Fällen ist es durchaus sinnvoll, Daten per Referenz zu übergeben (damit kein Kopiervorgang der Daten notwendig wird), aber innerhalb der Funktion keine Änderung dieser Daten zuzulassen. Um einen Referenzparameter als nicht veränderbar innerhalb einer Funktion zu kennzeichnen, wird dem Datentyp des Referenzparameters das Schlüsselwort const vorangestellt. Jeder Versuch, einen als const definierten Referenzparameter innerhalb der Funktion zu verändern, führt zu einem Übersetzungsfehler.

1: // Funktionsdeklaration
2: void PrintHeader(const int& pageNum);
3:
4: // main() Funktion
5: int main ()
6: {
7:    auto page = 1;        // Seitennummer initialisieren
8:    PrintHeader(page);    // Funktion aufrufen
9:    PrintHeader(10);      // Funktionsaufruf mit Konstante
10: }
11: // Definition der PrintHeader-Funktion
12: void PrintHeader(const int& pageNum)
13: {
14:    std::println("Seite {}", pageNum);
15: }

Außerdem können an einen konstanten Referenzparameter ebenfalls Konstanten übergeben werden. Dies ist im obigen Beispiel beim zweiten Aufruf der Funktion PrintHeader() dargestellt.

Übergabe von eindimensionalen Feldern

Um ein eindimensionales Feld an eine Funktion zu übergeben, ist dessen Anfangsadresse zu übergeben. Wie im Kapitel über Felder erwähnt, ist der Name eines eindimensionalen Feldes gleichbedeutend mit einem Zeiger auf den Beginn des Feldes. Und somit erhält die Funktion durch die Angabe des Feldnamens als Argument beim Aufruf einen Zeiger auf den Beginn des Feldes.

Der Zugriff innerhalb der Funktion auf die Feldelementekann entweder durch dereferenzieren des Parameters (Zeiger!) oder per Indizierung erfolgen.

1: // Funktionsdeklaration
2: void DoSomething(const short* const ptr);
3:
4: // main() Funktion
5: int main ()
6: {
7:     // Felddefinition und Initialisierung
8:     short data[] {10,20,30,40};
9:     // Übergabe des Feldes an die Funktion
10:    DoSomething(data);
11: }
12:
13: // Funktionsdefinition
14: void DoSomething (const short* const ptr)
15: {
16:    // Zugriff auf 1. Element durch dereferenzieren
17:    auto var = *ptr;
18:    // Zugriff auf 3. Element durch dereferenzieren
19:    var = *(ptr+2);
20:    // Zugriff auf 2. Element über Index
21:    var = ptr[1];
22: }

Und noch ein gut gemeinter Rat: Wird innerhalb der Funktion der Inhalt des Feldes nicht verändert, sollte das Feld als const übergeben werden. Im Beispiel oben erhält die Funktion DoSomething() einen konstanten Zeiger auf ein konstantes Feld. DoSomething() kann somit weder den Zeiger noch den Inhalt des durch den Zeiger adressierten Feldes verändern.

Alternativ kann bei der Deklaration und Definition der Funktion für den Parameter anstelle eines Zeigers auch Folgendes angeben:

void DoSomething (short arr[4]);
void DoSomething (short arr[]);

 

Übergabe von mehrdimensionalen Feldern

Bei der Deklaration und Definition der Funktion sind nun zwingend die Felddimensionen mit anzugeben. Lediglich die Angabe der ersten Dimension ist optional (siehe nachfolgendes Beispiel). Die Übergabe des Feldes beim Aufruf der Funktion bleibt gleich wie bei eindimensionalen Feldern, d.h. in der Parameterklammer der Funktion steht lediglich der Name des zu übergebenden Feldes.

1: // Feld definieren
2: const int ROWS=4;
3: const int COLUMNS=3;
4: short data[ROWS][COLUMNS];
5:
6: // Funktionsdeklaration
7: void PrintVal(short arr[][COLUMNS]);
8:
9: // main() Funktion
10: int main()
11: {
12:    // Funktion aufrufen
13:    PrintVal(data);
14: }
15: // Funktionsdefinition
16: void PrintVal(short arr[][COLUMNS])
17: {
18:    // Zugriff auf Feldelement
19:    auto var = arr[0][2];
20: }

Die oben angegebene Übergabe eines mehrdimensionalen Feldes an eine Funktion ist kürzeste Schreibweise. Alternativ kann die vollständige Dimension eines Feldes angegeben werden:

void PrintVal(short arr[ROWS][COLUMNS]);

 

deleted functions

Soll beim Aufruf einer Funktion eine implizite Typkonvertierung von Parametern verhindert werden, kann dafür eine sogenannte deleted function deklariert werden. Sehen wir uns dies anhand eines Beispiels an:

1: // Funktionsdefinition
2: void DoAnything (short val)
3: {
4:    ...
5: }
6:
7: int main()
8: {
9:    short sVal = 10;
10:   long lVal = 1234567L;
11:   DoAnything(sVal); // Aufruf mit short-Argument
12:   DoAnything(lVal); // Aufruf mit long-Argument
13:   ...
14: }

Die Funktion DoAnything() erwartet als Parameter einen short-Wert. Wird die Funktion z.B. mit einem mit einem long-Wert aufgerufen, erfolgt eine automatische Typkonvertierung des long-Werts in einen short-Wert, was zu einem fehlerhaften Verhalten des Programms führen kann. Um eine solche automatische Typkonvertierung beim Aufruf einer Funktion zu verhindern, wird eine Funktion als deleted function deklariert wie nachfolgend dargestellt.

1: // Funktionsdefinition
2: void DoAnything (short val)
3: {
4:    ...
5: }
6: // Deklaration einer deleted function
7: void DoAnything (long) = delete;
8:
9: int main()
10: {
11:    short sVal = 10;
12:    long lVal = 1234567L;
13:    DoAnything(sVal);     // Aufruf mit short-Parameter
14:    DoAnything(lVal);     // Führt zum Fehler beim Übersetzen
15:    DoAnything(10);       // auch das erzeugt einen Fehler
16: ...
17: }

Und auch der 3. Aufruf der Funktion führt zu einem Übersetzungsfehler, da der Funktionsaufruf nicht eindeutig ist. Ein int-Wert kann ja implizit sowohl in einen short- wie auch in einen long-Wert konvertiert werden und der Aufruf mit einem long Parameter wurde explizit unterbunden.

Funktionsrückgabewert

Außer dass Funktionen Daten per Parameter erhalten können, können Funktionen ein Datum zurückliefern, den sogenannten Returnwert.

Die Rückgabe des Wertes erfolgt mit einer return-Anweisung, wobei nach dem Schlüsselwort return das zurückzugebende Datum folgt. Innerhalb einer Funktion können, nicht schön aber erlaubt, mehrere return-Anweisungen stehen. 

Unabhängiger Datentyp des Rückgabewertes

Der Datentyp des Returnwerts wird vor dem Funktionsnamen angegeben, wobei der Datentyp des Returnwerts mit dem Returntyp der Funktion übereinstimmen sollte. Stimmen die Datentypen nicht überein, versucht der Compiler den Returnwert in den Returntyp der Funktion zu konvertiert (siehe Kapitel Typkonvertierungen).

1: // Funktionsdeklaration
2: float Deg2Rad (float deg);
3:
4: // main() Funktion
5: int main ()
6: {
7:    auto rad = Deg2Rad(90.0f);
8:    ...
9: }
10: // Funktionsdefinition
11: // Umrechnung von Grad in Bogenmass
12: // Liefert umgerechneten Wert
13: float Deg2Rad (float deg)
14: {
15:    constexpr decltype(deg) CONV = 3.1416*2.0 / 360.0;
16:    decltype(deg) result = deg * CONV;
17:    return result;
18: }

Die Funktion Deg2Rad() rechnet eine als float-Parameter übergebene Gradzahl ins Bogenmaß um (3600 = 2*Pi).

Besitzen alle Returnwerte denselben Datentyp, kann der Compiler den Returntyp der Funktion bestimmen. Dazu ist als Returntyp auto anzugeben. Der Returntyp entspricht dann dem Datentyp des Returnwertes.

auto FktName ([PARAMETER])
{
   ANWEISUNGEN
   return RETURNWERT;
}

Bei diesem Funktionstyp muss die Funktion vor ihrem Aufruf definiert sein, da eine Funktionsdeklaration ist nicht möglich.

Vorsicht ist geboten, wenn der Returntyp der Funktion mittels decltype(auto) bestimmt wird.

1: // Liefert Wert zurueck
2: auto ValFunc(int index)
3: {
4:    static int theData[] {1,2,3};
5:    return theData[index];
6: }
7: // Liefert Referenz zurueck
8: decltype(auto) RefFunc(int index)
9: {
10:    static int theData[] {10,20,30};
11:    return theData[index];
12: }

Die erste Funktion ValFunc() liefert einen Wert aus dem Feld theData zurück, während die zweite Funktion RefFunc() eine Referenz auf ein Feldelement zurückliefert. Dieses unterschiedliche Verhalten der beiden Funktionen liegt darin begründet, dass auto u.a. die Referenz entfernt.

Abhängiger Datentyp des Rückgabewertes

Hängt der Datentyp des Rückgabewertes vom Datentyp eines Parameters ab, kann die Funktion wie folgt definiert werden:

auto FktName ([PARAMETER]) ->RTYP
{
   ANWEISUNGEN
   return RETURNWERT;
}

Beispiel:

1: auto Deg2Rad (float deg) ->decltype(deg)
2: {
3:    constexpr decltype(deg) CONV = 3.1416*2.0 / 360.0;
4:    decltype(deg) result = deg * CONV;
5:    return result;
6: }

Ändert sich der Datentyp des Parameters deg, ändert sich automatisch auch der Datentyp des Returnwertes entsprechend.

Diese Art der Funktionsdefinition kann ebenfalls bei einem fixen Returntyp verwendet werden.

auto AnyFunc(short para1) -> float;

 

Predicates

Funktionen, die ein boolsches Ergebnis zurückliefern, werden als Predicate bezeichnet und spielen später bei der Behandlung der Container und Algorithmen der C++-Standardbibliothek eine wichtige Rolle.

Unäres Predicate: eine Funktion mit einem Parameter und einem bool-Wert als Rückgabewert.

1: bool IsZero(int val)
2: {
3:    return (val == 0);
4: }

Binäres Predicate: eine Funktion mit zwei Parameter und einen bool-Wert als Rückgabewert.

1: bool IsLesser(int val, int limit)
2: {
3:    return (val<limit);
4: }

Rekursive Funktionen

Da innerhalb einer Funktion (bis auf die eine erwähnte Ausnahme) alle Anweisungen erlaubt sind, können Funktionen wiederum Funktionen aufrufen. Ein Sonderfall stellt hierbei eine Funktion dar, die sich selbst aufruft. Solch eine Funktion wird als rekursive Funktion bezeichnet.

Rekursive Funktionen benötigen immer ein Abbruchkriterium, um eine Endlos-Schleife zu vermeiden.

1: #include <iostream>
2:
3: // Funktionsdeklaration
4: void PrintLine(const short count);
5:
6: // main() Funktion
7: int main ()
8: {
9:     // Funktionsaufruf
10:    PrintLine(4);
11: }
12: // Funktion PrintLine(), ruft sich selbst auf!
13: void PrintLine(const short count)
14: {
15:    // Sternchen und Zeilenvorschub ausgeben
16:    for (auto index=0; index < count; index++)
17:       std::cout << " *";
18:    std::cout << '\n';
19:    // Falls mehr als 1 Sternchen ausgegeben wurde
20:    if (count != 1)
21:       // Funktion erneut aufrufen, jetzt
22:       // jedoch mit einem Sternchen weniger
23:       PrintLine(count-1);
24:    // Nach dem letzten Sternchen fertig!
25:    return;
26: }

* * * *
* * *
* *
*

Die Funktion PrintLine() erhält von main() die Anzahl der in einer Reihe auszugebenden Sternchen. Nachdem die Sternchen in der for-Schleife ausgegeben sind, wird in der darauffolgenden if-Abfrage abgeprüft, ob mehr als 1 Sternchen ausgegeben wurde. Ist dies der Fall, ruft die Funktion sich erneut auf, jetzt mit einem Sternchen weniger. Dieses Spiel wiederholt sich so lange, bis nur ein Sternchen ausgegeben wird. In diesem Fall liefert die Bedingung der if-Abfrage false und die Funktion wird über die return-Anweisung beendet. Die return-Anweisung könnte hier entfallen, da sie die letzte Anweisung in der Funktion ist. Somit ergibt sich folgende Aufrufsequenz der Funktion:

PrintLine(4);
PrintLine(3);
PrintLine(2);
PrintLine(1);

 

constexpr und consteval Funktionen

Bei einer constexpr Funktionen versucht der Compiler den Returnwert der Funktion zu berechnen und diesen anstelle des Funktionsaufrufs einzusetzen. Kann der Returnwert nicht durch den Compiler berechnet werden, wird die Funktion wie gewöhnlich zur Laufzeit aufgerufen. Um eine Funktion als constexpr Funktion zu definieren, wird vor dem Returntyp das Schlüsselwort constexpr angegeben.

Doch was kann eine constexpr Funktion leisten, wenn deren Ergebnis zur Compilezeit berechnet werden soll? Sehen Sie sich das folgende Programm an, bei dem Compiler für beliebige int-Daten die Fakultät berechnet.

1: #include <print>
2:
3: // Fakultät berechnen
4: constexpr int fac(int val)
5: {
6:    return (val <= 1) ? 1: val*fac(val-1);
7: }
8:
9: // main() Funktion
10: int main()
11: {
12:    // Fakultaeten ausgeben berechnen (Compilezeit-Ausdruck!)
13:    std::println("Fakultaet von 5: {}",fac(5));
14:    std::println("Fakultaet von 3: {}",fac(3));
15: }

Die Funktion für die Fakultät ist eine rekursive Funktion, d.h., der Aufruf von fac(5) erzeugt folgende Aufrufe:

fac(5) = 5*fac(4)
fac(4) = 4*fac(3)
fac(3) = 3*fac(2)
fac(2) = 2*fac(1)
fac(1) = 1

Die consteval Funktion gleich prinzipiell der constexpr Funktion, nur mit der Einschränkung, dass das Ergebnis der Funktion nun zur Compilezeit berechenbar sein muss. Damit gelten für consteval Funktionen folgende Einschränkungen:

  • Jeder Parameter muss ein zur Compilezeit berechenbarer Literaltyp sein und
  • der Returnwert muss ebenfalls ein Literaltyp sein.
1: constexpr int exprFunc(int val)
2: {
3:    return val * val;
4: }
5: consteval int evalFunc(int val)
6: {
7:    return val * val;
8: }
9:
10: int main()
11: {
12:    auto var = 10;
13:    auto exprVar = exprFunc(var);    // ok
14:    auto evalVar1 = evalFunc(var);   // Fehler!
15:    auto evalVar2 = evalFunc(10);    // ok
16: }

Sonstiges zu Funktionen

Zum Schluss ein paar Hinweise auf Funktionseigenschaften, auf die später noch eingegangen wird:

  • Für Funktionsparameter können Defaultwerte vorgegeben werden.
  • Funktionen können als inline-Funktionen definiert werden.
  • Im Zusammenspiel mit Algorithmen der Standardbibliothek spielen Funktionsobjekte und Lambda-Ausdrücke eine wichtige Rolle.
  • Und jeder C++-Compiler kennt eine Unmenge an Standardfunktionen wie z.B. rand() oder time().

Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz