C++ Kurs

Funktionen

Die Themen:

Syntax

Eine Funktion ist, vereinfacht ausgedrückt, eine Zusammenfassung von mehreren Anweisungen unter einem bestimmten Namen. D.h. eine Funktion fungiert als eine Art (Unter-)Programm innerhalb eines beliebig komplexen Gesamtprogramms.

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

  1. Öfters benötigte Sequenzen von Anweisungen müssen nur einmal geschrieben werden und können dann von verschiedenen Stellen im Programm aus ausgeführt werden.
  2. Erst mit Hilfe von Funktionen ist es möglich, ein größeres Programm in kleinere, logische Teile zu unterteilen. Diese Unterteilung erleichtert den Überblick über das Gesamtprogramm und damit, auch ganz wichtig, die Wartbarkeit. Außerdem lassen sich einzelne Funktionen leichter testen als ein großes Gesamtprogramm.

Eine Funktion hat folgenden allgemeinen Aufbau:

1. Form

RETURNTYP FNAME ([PARAMETER])
{
   Anweisungen der Funktion
   [return RETURNWERT;]
}

2. Form 

auto FNAME ([PARAMETER]) ->RETURNTYP
{
   Anweisungen der Funktion
   [return RETURNWERT;]
}

3. Form

auto FNAME ([PARAMETER])
{
   Anweisungen der Funktion
   return RETURNWERT;
}

Angaben in den Klammern [...] sind optional.

Beachten Sie, dass nach der geschweiften Klammer am Ende der Funktion kein Semikolon steht.

Die zweite Form einer Funktion wird hauptsächlich dann eingesetzt, wenn sich der Returntyp der Funktion erst durch Auswerten des Datentyps eines der Parameter bestimmen lässt. In der Regel wird diesem Fall für die Definition des Returntyps der Operator decltype() verwendet.

Bei der dritten Form wird der Returntyp durch Auswerten des Datentyps des Returnwerts bestimmt. Enthält die Funktion mehrere return-Anweisung, so müssen die Datentypen des Returnwerts identisch sein.

HinweisIm Folgenden werden die Begriff Parameter und Argument wie folgt verwendet:

  • Parameter: Variable welche innerhalb der Klammer bei der Funktionsdefinition bzw. -deklaration steht
  • Argument: Datum welches beim Aufruf der Funktion an die Funktion übergeben wird
void Function(int val);    // val ist eine Parameter
...
int anyVal;
...
Function(anyVal);          // anyVal ist das Argument

Funktionsdeklaration (Signatur)

Bevor eine Funktion in einem Programm verwendet (aufgerufen) werden kann, muss die Funktion vorher definiert oder aber zumindest deklariert sein. Der Unterschied zwischen einer Funktionsdefinition und einer Funktionsdeklaration ist folgender:

Vielleicht fragen Sie sich nun, warum überhaupt die Funktionsdefinition bzw. Funktionsdeklaration vor dem Funktionsaufruf notwendig ist? Nun, der C++ Compiler ist in diesem Fall ein sehr strenger Compiler. Trifft er während des Übersetzungsvorgangs des Programms auf den Aufruf einer Funktion die er noch nicht kennt, so kann er ohne die vorherige Funktionsdeklaration bzw. -definition nicht prüfen, ob der Name der Funktion richtig geschrieben ist oder ob eventuelle Parameter der Funktion richtig angegeben wurden. D.h. erst durch die vorherige Funktionsdeklaration bzw. -definition kann der Compiler den Funktionsaufruf syntaktisch überprüfen und auch den notwendigen Aufruf-Code erstellen.

Doch wie sieht die Deklaration einer Funktion aus? Sie hat folgende Syntax:

RETURNTYP FNAME ([PARAMETER]);
 bzw.
auto FNAME ([PARAMETER]) -> RETURNTYP;

Wenn Sie nun diese Funktionsdeklaration mit dem im vorherigen Abschnitt aufgeführten Aufbau einer Funktion vergleichen, so werden Sie feststellen, dass sie genau dem Kopf der Funktion entspricht, jedoch mit einem abschließenden Semikolon. Solche Funktionsdeklarationen haben wir bisher auch schon, mehr oder weniger bewusst, angewandt. Das Einbinden von Header-Dateien mittels #include diente unter anderem genau diesem Zweck. Nachfolgend einige Beispiele für Funktionsdeklarationen.

PgmHeader
// Funktionsdeklarationen
void PrintHeader();
auto MinVal(short val1, short val2) ->decltype(val1);

// main() Funktion
int main()
{
   ...
}

Und noch ein weiterer Begriff wird später im Zusammenhang mit Funktionen auftauchen, der Begriff Funktions-Signatur. Die Signatur einer Funktion besteht aus dem Funktionsnamen und den Parametern, also ohne den Returntyp der Funktion. Sie spielt später beim Überladen von Funktionen noch eine wichtige Rolle.

Vielleicht haben Sie die Deklaration einer Funktion mittels
auto FNAME ([PARAMETER]);
vermisst. Eine solche Deklaration erzeugt im Allgemeinen einen Fehler beim Übersetzen des Programms, da der Returntyp der Funktion aus dem Datentyp des Rückgabewertes der Funktion (wird gleich noch behandelt) bestimmt wird. Und dazu benötigt der Compiler die Definition der Funktion, d.h. den Funktionscode. Ist aber eine Funktion vor ihrem Aufruf definiert, so ist die Deklaration redundant.

Funktionsdefinition

Sehen wir uns jetzt die Definition einer Funktion an. Beginnen wir mit dem Funktionsnamen. Der Name einer Funktion muss eindeutig sein, d.h. es darf keine weitere Funktion, Variable usw. mit dem gleichen Namen geben. Ausnahme: beim Überladen von Funktionen, das in einem späteren Kapitel noch behandelt wird.

PgmHeader
// Funktionsdeklarationen
void PrintHeader();     // PrintHeader() und
void Printheader();     // Printheader() sind unterschiedliche Funktionen
auto Min(short v1,short v2) ->decltype(v1);
// Variablendefinitionen
short Min;              // Nicht erlaubt, da Min() bereits als Funktion deklariert

Beachten Sie, dass C++ streng zwischen Groß-/Kleinschreibung unterscheidet. So deklarieren die ersten zwei Funktionsdeklarationen im obigen Beispiel zwei unterschiedliche Funktionen. Funktionsnamen, welche sich nur durch die Groß-/Kleinschreibung unterscheiden, sollten aber der besseren Lesbarkeit wegen vermieden werden. Auch die Definition der short-Variable Min im Beispiel würde einen Übersetzungsfehler erzeugen, da zuvor bereits eine Funktion mit dem Namen Min(...) deklariert wurde.

Die letztendlich von einer Funktion auszuführenden Anweisungen werden in einem Block {...} zusammengefasst. Innerhalb einer Funktion können bis auf eine Ausnahme alle C++ Anweisungen stehen. Nicht erlaubt ist es, innerhalb einer Funktion eine weitere Funktion zu definieren, so wie dies z.B. die Programmiersprache PASCAL zulässt. Und auch hier gibt es wiederum eine Ausnahme: innerhalb einer Funktion können sogenannte Lambda-Funktionen definiert werden. Was es mit diesen Lambda-Funktionen auf sich hat, das sehen wir uns später noch an.

PgmHeader
void Func1()
{
   ...
   void Func2()   // Das ist nicht erlaubt!
   {
      ...
   }
   ...
}

Außer auszuführende Anweisungen können innerhalb von Funktionen auch Variablen oder Konstanten definiert werden, denn auch die Definition von Variablen und Konstanten ist ja letztendlich eine Anweisung. Diese Variablen bzw. Konstanten sind dann aber nur innerhalb der Funktion gültig, in der sie definiert wurden.

PgmHeader
void Func1()
{
   int temp;                  // Definition von Variablen/Konstanten die nur
   const auto PI = 3.1416;    // innerhalb dieser Funktion gültig sind
   ...
}

Gehen wir jetzt zur Praxis über und sehen uns den einfachsten Fall einer Funktionsdefinition an: eine Funktion die keine Parameter besitzt und auch kein Ergebnis zurückliefert. Wir wollen jetzt eine einfache Funktion schreiben, die einen bestimmten Text, z.B. für eine Seitenüberschrift, ausgibt. Die Funktion soll den Namen PrintHeader erhalten.

Bevor im Programm PrintHeader aufgerufen werden kann, muss die Funktion, wie erwähnt, zumindest deklariert sein. Diese Deklaration ist wie unten angegeben vorzunehmen. Dabei legt der Returntyp void vor dem Funktionsnamen fest, dass die Funktion keinen Wert zurückliefert. Da die Funktion auch keine Parameter (Daten) benötigt um den Text auszugeben, bleibt die Parameterklammer einfach leer.

PgmHeader
#include <iostream>
// Funktionsdeklaration
void PrintHeader();
...
// main() Funktion
int main()
{
   ...
}

Bleibt noch die Definition der Funktion übrig. Dazu ist zunächst der Funktionskopf einzugeben. Er entspricht der Funktionsdeklaration, jedoch ohne das abschließende Semikolon. Nach dem Funktionskopf folgt der Funktionsblock {...}. Und innerhalb dieses Blocks stehen die entsprechenden Anweisungen der Funktion. In unserem Beispiel ist die Funktion relativ klein und gibt nur einen bestimmten Text aus. Beim Erreichen des Endes des Funktionsblocks wird automatisch zu der nach dem Funktionsaufruf (wird gleich behandelt) folgenden Anweisung zurückgekehrt.

PgmHeader
#include <iostream>
// Funktionsdeklaration
void PrintHeader();
...
// main() Funktion
int main()
{
   ...
}
// Definition der Funktion
void PrintHeader()
{
   cout << "Seitenüberschrift\n";
}
Wenn die Funktionsdefinition vor der main() Funktion erfolgt wäre, dann wäre keine Funktionsdeklaration notwendig, da dem Compiler beim Aufruf der Funktion aus main() heraus die Funktion dann schon bekannt ist.

Funktionsaufruf

Nach der Deklaration und Definition der Funktion fehlt jetzt nur noch die Anweisung um die Funktion auch aufzurufen, d.h. den Funktionscode auszuführen. Dazu wird an den Stellen im Programm, an denen die Funktion aufgerufen werden soll, einfach der Funktionsnamen, gefolgt von einer leeren Klammer ( ) und einem abschließenden Semikolon angegeben.

PgmHeader
#include <iostream>
// Funktionsdeklaration
void PrintHeader();
...
// main() Funktion
int main()
{
   ...
   // Funktion aufrufen
   PrintHeader();
}
// Definition der Funktion
void PrintHeader()
{
   cout << "Tabellenüberschrift\n";
}
Achtung Wird die dritte Funktionsform verwendet, so muss die Funktion vor ihrem Aufruf definiert sein, da der Compiler ansonsten den Returntyp der Funktion nicht bestimmen kann.

Funktionsparameter

Parameter im Allgemeinen

Nachdem wir uns die generelle Handhabung einer Funktion angesehen haben, folgt nun der nächste Schritt: die Parametrierung von Funktionen. War bisher die Klammer nach dem Funktionsnamen noch leer, so werden wir jetzt mit Hilfe dieser Klammer Daten an die Funktion übergeben.

Fangen wir auch hier wieder mit der Deklaration der Funktion an. Wenn an eine Funktion Daten übergeben werden sollen, so sind bei der Deklaration der Funktion zumindest die Datentypen der Parameter anzugeben. Damit kann der Compiler beim Aufruf der Funktion zum einen den richtigen Aufrufcode erzeugen und zum anderen auch eine Überprüfung der beim Aufruf angegebenen Argumente vornehmen. Zusätzlich kann (und sollte auch!) bei der Funktionsdeklaration nach dem Datentyp des Parameters auch ein beschreibender Parametername mit angegeben werden.

Benötigt eine Funktion mehrere Parameter, so sind diese durch Komma voneinander zu trennen. Besonders bei mehreren Parametern hilft die Angabe eines beschreibenden Parameternamens, der die Bedeutung des Parameters widerspiegelt. Die Anzahl der Parameter einer Funktion ist nicht begrenzt.

Im nachfolgenden Beispiel erhält die Funktion PrintIt() im ersten Parameter wahrscheinlich die X-Position und im zweiten Parameter die Y-Position für die Ausgabe.

PgmHeader
void CalcSqrt (double); // ist gleichbedeutend mit
                        // void CalcSqrt (double val);
void PrintIt (unsigned short xPos, unsigned short yPos);

Bei der Definition der Funktion sind, genauso wie bei der Deklaration, die Datentypen der Parameter anzugeben, nun jedoch zwingend gefolgt von den Parameternamen. Wenn schon bei der Deklaration die Parameter mit Namen versehen wurden, so braucht bei der Definition der Funktion deren Deklaration im Prinzip nur kopiert und das abschließende Semikolon entfernt werden. Innerhalb der Funktion kann dann über diese Parameternamen auf die an die Funktion übergebenen Daten zugegriffen werden, d.h. die Parameternamen wirken sozusagen als Platzhalter für die im Aufruf tatsächlich angegebenen Daten. Die Parameternamen bei der Deklaration und der Definition der Funktion müssen nicht zwingend übereinstimmen, aber der besseren Lesbarkeit wegen sollten sie es.

Im Beispiel ist eine Funktion für die Ausgabe eines Seitenkopfes aufgeführt. Die aktuelle Seitennummer erhält PrintHeader() dabei im Parameter übergeben.

PgmHeader
// Funktionsdeklaration
void PrintHeader(int pageNum);
...
// main() Funktion
int main ()
{
   // gleich noch Seitenüberschrift ausdrucken
   ...
}
// Definition der PrintHeader-Funktion
void PrintHeader(int pageNum)
{
   cout << "Dies ist Seite " << pageNum << endl;
}
AchtungAchten Sie immer darauf, dass die Reihenfolge und Anzahl der Datentypen der Parameter bei der Funktionsdeklaration und -definition übereinstimmen. Tun sie dies nicht, so meldet der Compiler einen Fehler!

Sehen wir uns nun die verschiedenen Parametertypen an und fangen mit dem einfachsten Typ an, dem call-by-value Parameter.

call-by-value Parameter

Bei einem Parameter vom Typ call-by-value erhält die aufgerufene Funktion nur eine Kopie der beim Funktionsaufruf übergebenen Daten. Daraus folgt, dass Änderungen des Parameterwerts zwar innerhalb der Funktion erlaubt sind, diese aber auch nur auf die Kopie wirken. Wird die Funktion wieder verlassen, so hat sich der Wert des übergebenen Datums nicht geändert. Die Definition eines call-by-value Parameters erfolgt in der Art, dass innerhalb der Parameterklammer der Funktion lediglich der Datentyp und Name des entsprechenden Parameters angegeben wird. Im Beispiel wird am Ende der Funktion der Wert des Parameters pageNum zwar immer auf 99 gesetzt, jedoch wirkt sich dies nicht weiter in main() aus.

PgmHeader
// Funktionsdeklaration
void PrintHeader(int pageNum);
...
// main() Funktion
int main ()
{
   auto page = 1;        // Seitennummer initialisieren
   PrintHeader(page);    // Funktion aufrufen
   ...                   // page hat hier immer noch den Wert 1
   PrintHeader(10);      // Funktionsaufruf mit Konstante
}
// Definition der PrintHeader-Funktion
void PrintHeader(int pageNum)
{
   cout << "Dies ist Seite " << pageNum << endl;
   pageNum = 99;          // Parameter verändern
}

Beim Aufruf der Funktion kann für einen call-by-value Parameter entweder eine Variable (erster Aufruf oben) oder aber eine Konstante (zweiter Aufruf) an die Funktion übergeben werden.

AchtungDa die Funktion eine Kopie des übergebenen Datums erhält, kann es bei dieser Parameterart unter Umständen notwendig werden, sehr viele Daten zu kopieren. Dies gilt insbesondere dann, wenn die Funktion später Objekte als Parameter erhält. Sie sollten diese Art der Parameterübergabe nach Möglichkeit auf die einfachen, bereits bekannten Datentypen wie int oder unsigned long beschränken

Referenzparameter

Referenzparameter ermöglichen Funktionen, die an sie übergebenen Daten dauerhaft zu verändern, sodass innerhalb der Funktion durchgeführte Änderungen an den Parametern auch nach dem Verlassen der Funktion noch gültig sind.

Wie schon bei der Einführung von Variablen erwähnt, ist eine Referenz letztendlich im Prinzip nichts anderes als ein Verweis auf ein bereits bestehendes Datum. Um einen Funktionsparameter 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 genauso wie ein Parameter vom Typ call-by-value übergeben.

Nachfolgend sehen Sie eine Funktion Swap(...), welche die Werte der beiden übergebenen Parameter tauscht.

PgmHeader
// Funktionsdeklaration
void Swap (short& val1, short& val2);

// main() Funktion
int main ()
{
   ...
   Swap (var1, var2);
   ...
}

// Funktionsdefinition
// Vertauscht die Inhalte der übergebenen Variablen

void Swap (short& val1, short& val2)
{
   auto temp = val1;
   val1 = val2;
   val2 = temp;
}

Was nicht so ohne weiteres als Referenzparameter übergeben werden kann sind Konstanten. D.h. die obige Funktion Swap(...) kann zum Beispiel nicht mit Swap(10,var); aufgerufen werden (was hier aber auch keinen Sinn machen würde).

Konstante Referenzparameter

In manchen Fällen kann es durchaus sinnvoll sein, dass Daten, die per Referenz übergeben wurden (damit kein Kopiervorgang der Daten notwendig wird) innerhalb einer Funktion nicht verändert werden sollen. Denken Sie an die vorherige Funktion PrintHeader(), die die aktuelle Seitennummer als Parameter erhalten hat. Niemand würde hier vermuten, dass die Seitennummer innerhalb der PrintHeader() Funktion verändert wird. Um nun 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.

PgmHeader
// Funktionsdeklaration
void PrintHeader(const int& pageNum);
...
// main() Funktion
int main ()
{
   auto page = 1;        // Seitennummer initialisieren
   PrintHeader(page);    // Funktion aufrufen
   ...                   // page hat hier immer noch den Wert 1
   PrintHeader(10);      // Funktionsaufruf mit Konstante
}
// Definition der PrintHeader-Funktion
void PrintHeader(const int& pageNum)
{
   cout << "Dies ist Seite " << pageNum << endl;
}

Außerdem können bei einem konstanten Referenzparameter jetzt auch Konstanten übergeben werden. Dies ist im obigen Beispiel beim zweiten Aufruf der Funktion PrintHeader() dargestellt.

HinweisKennzeichnen Sie grundsätzlich alle Parameter, die innerhalb einer Funktion nicht verändert werden und die nicht per call-by-value übergeben werden, immer als const. In der englisch-sprachigen Literatur wird dies auch als const-correctness bezeichnet.

Übergabe von eindimensionalen Feldern

Sehen wir uns zuerst wieder den einfacheren Fall an, dass ein eindimensionales Feld an eine Funktion übergeben werden soll.

Bei der Übergabe eines eindimensionalen Feldes an eine Funktion wird lediglich dessen Anfangsadresse übergeben. Wie im Kapitel über Felder erwähnt, ist der Name eines eindimensionalen Feldes gleichbedeutend mit dem Zeiger auf den Beginn des Feldes. Und somit erhält die Funktion durch die Angabe des Feldnamens als Parameter beim Aufruf einen entsprechenden Zeiger auf den Beginn des Feldes. Doch wie wird nun innerhalb der Funktion auf die Feldelemente zugegriffen? Nun, der erste Ansatz hierzu könnte wie folgt aussehen: man addiert zum Zeiger den Index des gewünschten Elements und dereferenziert diesen dann, so wie im Beispiel unten aufgeführt. Zu beachten ist dabei stets, dass das erste Element den Index 0 besitzt!

PgmHeader
// Funktionsdeklaration
void DoSomething(const short* const);

// main() Funktion
int main ()
{
   // Felddefinition und Initialisierung
   short array[] {10,20,30,40};
   ...
   // Übergabe des Feldes an die Funktion
   DoSomething(array);
   ...
}

// Funktionsdefinition, das Feld kann hier in
// der Funktion nicht verändert werden

void DoSomething (const short* const ptr)
{
   // Zugriff auf 1. Element
   auto var = *ptr;
   // Zugriff auf 3. Element
   var = *(ptr+2);
}

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

Der Zugriff auf die Feldelemente innerhalb der Funktion geht aber auch eleganter, wenigsten für den armen Programmierer, der mit Zeigern noch etwas auf Kriegsfuß steht. Anstelle nun irgendwelche Zeigerarithmetik durchführen zu müssen, kann der an die Funktion übergebene Zeiger auch als Feldname verwendet werden um so indiziert auf die Feldelemente zuzugreifen. Dass dies möglich ist rührt von der bereits erwähnten Tatsache her, dass der Name eines eindimensionalen Feldes gleichzusetzen ist mit der Anfangsadresse des Feldes. Und damit kann das vorherige Beispiel wie angegeben umgeschrieben werden.

PgmHeader
...
// Funktionsdefinition, das Feld kann hier in
// der Funktion nicht verändert werden

void DoSomething (const short* const ptr)
{
   // Zugriff auf 1. Element
   auto var = ptr[0];
   // Zugriff auf 3. Element
   var = ptr[2];
}
HinweisWenn Sie wollen, können Sie bei der Deklaration und der Definition der Funktion für den Parameter anstelle eines Zeigers auch Folgendes angeben:
void DoSomething (short array[4]);
void DoSomething (short array[]);

Wenn an eine Funktion mehrere konstante Daten des gleichen Datentyps übergeben werden sollen, so können statt eines entsprechenden Feldes auch die Elemente direkt an die Funktion übergeben werden, und zwar als sogenannte Initialisiererliste. Der Datentyp des Funktionsparameters ist dann vom Typ std::ininitializer_list<ETYP>, wobei ETYP den Datentyp der Elemente in der Liste beschreibt. Dies lässt sich am besten am folgenden Beispiel veranschaulichen:

PgmHeader
// Funktionsdeklaration
void Print(std::initializer_list<std::string> iListe);

int main()
{
    // Aufruf der Funktion mit einer Initialisiererliste
    Print({"eins","zwei","drei"});
}
// Funktionsdefinition
void Print(std::initializer_list<std::string> iListe)
{
   // Ausgabe der Werte in der Liste
   for (auto element: iListe)
      cout << element << endl;
   // Ausgabe des 2. Elements aus der Liste
   cout << iListe[1] << endl;     // So geht's leider nicht mehr!
   auto iter = iListe.begin();    // Iterator auf Listenanfang setzen
   cout << *(iter+1) << endl;     // Aber so geht's
}

Die Funktion Print(..) erhält drei Strings per Initialisiererliste und gibt diese per range-for Schleife aus. Für den Zugriff auf die einzelnen Elemente kann jetzt allerdings nicht mehr der Indexoperator verwendet werden, sondern hierfür muss einen Iterator eingesetzt werden. Iteratoren im Allgemeinen werden später noch genauer behandelt.

Übergabe von mehrdimensionalen Feldern

Ein klein wenig komplizierter sieht die Sache bei der Übergabe von mehrdimensionalen Feldern aus. Hier müssen wir dem Compiler etwas helfen damit, er die Feldelemente im Speicher findet. Bei der Deklaration und der Definition der Funktion muss die Feldgröße des übergebenen Feldes immer mit angegeben werden, denn nur so kann der Compiler innerhalb der Funktion die Position der einzelnen Elemente korrekt berechnen. Lediglich die Angabe der 'höchsten 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 auch hier lediglich der Name des zu übergebenden Feldes.

PgmHeader
// Feld definieren
const int ROWS=4;
const int COLUMNS=3;
short array[ROWS][COLUMNS];

// Funktionsdeklaration
void PrintVal(short arr[][COLUMNS]);

// main() Funktion
int main()
{
   // Funktion aufrufen
   PrintVal(array);
   ...
}

// Funktionsdefinition
void PrintVal(short arr[][COLUMNS])
{
   // Zugriff auf Feldelement
   auto var = arr[0][2];
}
HinweisDie oben angegebene Übergabe eines mehrdimensionalen Feldes an eine Funktion ist kürzeste Schreibweise. Alternativ kann auch die vollständige Dimension eines Feldes für die Übergabe angegeben werden:
void PrintVal(short arr[ROWS][COLUMNS]);

Funktionsrückgabewert

Außer dass Funktionen Daten als Parameter erhalten können, können sie auch einen, und nur einen, Wert zurückliefern. Dieser Wert wird auch als Returnwert bezeichnet. Dazu muss der Compiler aber wissen, welchen Datentyp der Returnwert besitzt. Dieser Datentyp wird entweder explizit vor dem Funktionsnamen oder nach der Parameterklammer angegeben oder aber implizit durch Angabe von auto vor dem Funktionsnamen durch den Compiler bestimmt.

Wird der Datentyp nach der Parameterklammer angegeben, muss vor dem Funktionsnamen das Schlüsselwort auto stehen und nach der Parameterklammer ->RETURNTYP (siehe auch nachfolgendes Beispiel).

Die Rückgabe des Wertes erfolgt dann mit der return-Anweisung innerhalb der Funktion, wobei nach dem Schlüsselwort return der zurückzugebende Wert anzugeben ist. Innerhalb einer Funktion können, nicht schön aber erlaubt, durchaus mehrere return-Anweisungen stehen. Der Datentyp des Returnwerts sollte dabei mit dem Returntyp der Funktion übereinstimmen. Stimmen die Datentypen nicht überein, so versucht der Compiler den Returnwert in den Returntyp der Funktion zu konvertiert (siehe dazu Automatische Typkonvertierung). Ausnahme: es wird die dritte Funktionsform verwendet. In diesem Fall müssen die Datentypen aller Returnwerte übereinstimmen oder Sie müssen eine explizite Typkonvertierung des Returnwerts einsetzen.

PgmHeader
// Funktionsdeklaration
auto Deg2Rad (float deg) -> decltype(deg);

// main() Funktion
int main ()
{
   ...
   auto rad = Deg2Rad(90.0f);
   ...
}
// Funktionsdefinition
// Umrechnung von Grad in Bogenmass
// Liefert umgerechneten Wert

auto Deg2Rad (float deg) -> decltype(deg)
{
   constexpr decltype(deg) PI = 3.1416;
   decltype(deg) result = deg/360.0 * 2.0 * PI;
   return result;
}

Die Funktion Deg2Rad(...) erhält als Parameter eine Gradzahl als float-Wert übergeben und rechnet diese dann in Bogenmaß um (3600 = 2*Pi). Beachten Sie den häufigen Gebrauch des Operators decltype(). Wird der Datentyp des Parameters deg geändert, so ändert sich zum einen auch der Returntyp der Funktion und zum anderen auch die Datentypen, welche für die Umrechnung verwendet werden.

Obacht geben müssen Sie, wenn der Returntyp der Funktion mittels decltype(auto) bestimmt werden soll.

PgmHeader
// Liefert Wert zurueck
auto ValFunc(int index)
{
   static int theData[] {1,2,3};
   return theData[index];
}
// Liefert Referenz zurueck
decltype(auto) RefFunc(int index)
{
   static int theData[] {10,20,30};
   return theData[index];
}

Die erste Funktion liefert den entsprechenden Wert des Feldes theData zurück während die zweite Funktion eine Referenz auf das Feldelement zurückliefert. D.h. wird der von der Funktion RefFunc(...) zurückgegebene Wert verändert, so ändert sich damit der Inhalt des Feldes theData. Dieses unterschiedliche Verhalten der beiden Funktionen liegt darin begründet, dass auto u.a. die Referenz entfernt.

Soll mehr als nur ein Wert aus der Funktion zurückliefert werden, so muss dies bis jetzt über entsprechende Referenzparameter erfolgen.

Rekursive Funktionen

Sehen wir uns jetzt einen 'Sonderfall' von Funktionen an. Da innerhalb einer Funktion (bis auf die eine erwähnte Ausnahme) alle Anweisungen erlaubt sind, können Funktionen selbstverständlich wiederum Funktionen aufrufen. Einen Sonderfall stellen hierbei solche Funktionen dar, die sich wieder selbst aufrufen. Solche Funktionen werden auch als rekursive Funktionen bezeichnet. Rekursive Funktionen benötigen aber immer ein Abbruchkriterium, in etwa der folgenden Form, um eine Endlos-Schleife zu vermeiden:

if (AUSDRUCK)
   return [WERT];

Die Angabe von Wert bei der return-Anweisung entfällt natürlich bei Funktionen mit dem Returntyp void.

Nachfolgend ein Beispiel für eine solche rekursive Funktion.

PgmHeader
#include <iostream>
using namespace std;

// Funktionsdeklaration
void PrintLine(const short);

// main() Funktion
int main ()
{
   // Funktionsaufruf
   PrintLine(4);
}
// Funktion PrintLine(), ruft sich selbst auf!
void PrintLine(const short count)
{
   // Sternchen und Zeilenvorschub ausgeben
   for (auto index=0; index < count; index++)
      cout << " *";
   cout << endl;
   // Falls mehr als 1 Sternchen ausgegeben wurde
   if (count != 1)
      // Funktion erneut aufrufen, jetzt
      // jedoch mit einem Sternchen weniger

      PrintLine(count-1);
   // Nach dem letzten Sternchen fertig!
   return;
}
Programmausgabe* * * *
 * * *
 * *
 *

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

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

Beachten Sie, dass die Funktion PrintLine(...) einen const Parameter erhält. Trotzdem kann die Funktion selbstverständlich mit diesem Parameter Operationen durchführen, wie etwa beim rekursiven Aufruf der Funktion:

PrintLine(nCount-1);

Das Einzige was die Funktion nicht tun darf ist, zu versuchen den Parameter zu verändern!

constexpr Funktionen

Wie vorhin bei den Konstanten schon erwähnt, können auch Funktionen vom Typ constexpr sein. Um eine Funktion als constexpr Funktion zu deklarieren bzw. zu definieren, wird vor dem Returntyp das Schlüsselwort constexpr angegeben. Das Besondere an constexpr Funktionen ist, dass der Compiler schon beim Aufruf der Funktion versucht, das Ergebnis der Funktion zu berechnen. D.h. er versucht den Aufruf der Funktion gleich durch deren Ergebnis zu ersetzen. Damit er das kann, unterliegt eine constexpr Funktion aber u.a. folgenden Einschränkungen:

Es gibt noch einige weitere Einschränkungen, die für uns an dieser Stelle nicht relevant sind.

Doch was kann so eine constexpr Funktion leisten, wenn deren Ergebnis schon zur Compilezeit berechenbar sein muss? Nehmen wir einmal an, Sie benötigen in Ihrer Anwendung den Sinuswert von verschiedenen Konstanten. Natürlich könnten Sie hierfür Ihren Taschenrechner verwenden und die Werte direkt eintragen. Wir wollen dies aber durch den Compiler berechnen lassen. Damit wir dies tun können, müssen wir den Sinus durch eine entsprechende trigonometrische Reihe ausdrücken. Die entsprechende Formel hierzu lautet:

sin(x) = x1/1! - x3/3! + x5/5! - x7/7! + ... usw.

D.h. wir benötigen 3 (drei!) Funktionen, um den Sinus näherungsweise zu berechnen. Eine Funktion für die Exponenten (xy), eine für die Fakultäten (x!) und eine für die Summe der Terme. Die vollständige Lösung hierfür könnte wie folgt aussehen:

PgmHeader
// Fakultät berechnen
constexpr int fac(int val)
{
   return (val <= 1) ? 1: val*fac(val-1);
}
// Exponentialfunktion
constexpr auto pow(int ex, float val) ->decltype(val)
{
   return (ex==1) ? val: val*pow(ex-1,val);
}
// Sinus Reihe
constexpr float mysin(float val)
{
   float sum = pow(1, val);
   sum -= pow(3,val) / fac(3);
   sum += pow(5,val) / fac(5);
   sum -= pow(7,val) / fac(7);
   return sum;
}
// main() Funktion
int main()
{
   ...
   auto var = mysin(0.8f);    // Sinus berechnen (Compilezeit-Ausdruck!)
}

Die Funktion für die Fakultät und die Exponentialfunktion sind rekursive Funktionen. D.h. der Aufruf von fac(5) erzeugt wiederum folgende Aufrufe:

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

Gleiches gilt auch für den Aufruf der Exponentialfunktion. Diese Art der Programmierung, die übrigens auch mit den später noch zu behandelnden Templates möglich ist, bezeichnet man als Meta-Programmierung. Bei der Meta-Programmierung wird Programm-Code durch einen anderen Programm-Code erzeugt. In unserem Fall erzeugt der Compiler z.B. durch den Aufruf fac(5) den Programm-Code für die Funktion fac(4).

Und das alles wird letztendlich durch die Sinusfunktion mysin(...) gesteuert. Sie erzeugt die notwendigen Fakultäts- und Exponentialfunktionen und fügt deren Ergebnisse zusammen.

HinweisEinen weiteren Einsatz einer constexpr Funktion finden Sie später bei der Behandlung von überladenen Operatoren.

Sonstiges zu Funktionen

Damit sind wir fast am Ende dieses Kapitels angelangt. Bevor wir zum Beispiel und der anschließenden Übung kommen, noch einige weitere 'Eigenschaften' von Funktionen:

DetailUnd zu guter Letzt gibt es noch Zeiger auf Funktionen. Was es damit auf sich hat, das erfahren Sie, wenn Sie das Symbol links anklicken. Funktionszeiger sind aber wirklich für etwas für Fortgeschrittene.

Beispiel und ÜbungUnd hier geht's zum Beispiel und zur Übung.