C++ Kurs

Funktionen

Die Themen:

Einleitung und Syntax
Funktionsdeklaration (Signatur)
Funktionsdefinition
Funktionsaufruf
Funktionsparameter
Funktionsrückgabewert
Rekursive Funktionen
Sonstiges zu Funktionen
Beispiel und Übung

Einleitung und Syntax

Eine Funktion ist, vereinfacht ausgedrückt, eine Zusammenfassung von mehreren Anweisungen unter einem bestimmten Namen. Sie können sich eine Funktion als eine Art (Unter-)Programm innerhalb eines beliebig komplexen Gesamtprogramms vorstellen.

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 auf relativ einfache Art von verschiedenen Stellen im Programm aus ausgeführt werden.
  2. Erst mithilfe von Funktionen ist es möglich, ein größeres Programm in kleine, 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 immer folgenden Aufbau:

RETURNTYP Funktionsname ([Parameter])
{
   [Definition von lokalen Variablen]
   Anweisungen der Funktion
   [return RETURNWERT]
}

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

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

Funktionsdeklaration (Signatur)

Bevor Sie eine Funktion in einem Programm verwenden (aufrufen) können, 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 eine Funktionsdeklaration vor einem Funktionsaufruf notwendig ist? Nun, der C++ Compiler ist ein 'sehr strenger' Compiler. Trifft er während des Übersetzungsvorgangs auf den Aufruf einer Funktion die noch nicht definiert ist, so kann er ohne die notwendige Funktionsdeklaration nicht prüfen, ob z.B. der Name der Funktion richtig geschrieben ist und auch die (optionalen) Parameter der Funktion richtig angegeben wurden. D.h. erst durch die Funktionsdeklaration 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 Funktionsname ([Parameter]);

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


// Funktionsdeklarationen
void  PrintHeader();
short MinVal(short, short);

// 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.

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 einer späteren Lektion noch behandelt wird.


// Funktionsdeklarationen
void PrintHeader();     // PrintHeader() und
void Printheader();     // Printheader() sind unterschiedliche Funktionen
short Min(short,short);
// Variablendefinitionen
short Min;              // Nicht erlaubt, da Min() als Funktion deklariert

Beachten Sie bitte, dass C++ streng zwischen Groß-/Kleinschreibung unterscheidet. So deklarieren die ersten zwei Funktionsdeklarationen im Beispiel oben zwei unterschiedliche Funktionen. Vermeiden Sie aber der besseren Lesbarkeit wegen solche Konstruktionen. Die Definition der short-Variable Min im Beispiel würde einen Übersetzungsfehler erzeugen, da bereits zuvor eine Funktion 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.


Nicht erlaubt!

void Func1()
{
   ...
   void Func2()   // Das geht nicht!
   {
      ...
   }
   ...
}

Außer Anweisungen im üblichen Sinne, können Sie innerhalb von Funktionen auch Variablen oder Konstanten definieren. Auch die Definition von Variablen/Konstanten ist ja letztendlich eine Anweisung. Diese Variablen/Konstanten sind dann aber nur innerhalb der Funktion gültig.


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

Doch gehen wir jetzt zur Praxis über und sehen uns den einfachsten Fall einer Funktionsdefinition an: eine Funktion die keine Parameter (Daten) benötigt und auch kein Ergebnis zurückliefern muss. Wir wollen jetzt eine einfache Funktion schreiben, die einen bestimmten Text, z.B. für die Überschrift auf einer Seite, 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. Das Schlüsselwort void vor dem Funktionsnamen gibt an, dass die Funktion keinen Wert zurückliefert. Da die Funktion auch keine Parameter (Daten) benötigt um den Text auszugeben, bleibt die Funktionsklammer einfach leer.


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

Nur muss die Funktion noch definiert werden. Dazu ist zunächst der Funktionskopf einzugeben. Er entspricht der Deklaration, 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.


#include <iostream>
// Funktionsdeklaration
void PrintHeader();
...
// main() Funktion
int main()
{
   ...
}
// Definition der Funktion
void PrintHeader()
{
   cout << "Tabellenüberschrift\n";
}

Funktionsaufruf

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


#include <iostream>
// Funktionsdeklaration
void PrintHeader();
...
// main() Funktion
int main()
{
   ...
   // Funktion aufrufen
   PrintHeader();
}
// Definition der Funktion
void PrintHeader()
{
   cout << "Tabellenüberschrift\n";
}

Funktionsparameter

Parameter im Allgemeinen

Nachdem die generelle Handhabung einer Funktion bekannt ist, folgt nun der nächste Schritt: die Parametrierung von Funktionen. War bisher die Klammer nach dem Funktionsnamen noch leer, so werden wir jetzt mithilfe dieser Klammer Parameter (Daten) an die Funktion übergeben.

Fangen wir auch hier wieder mit der Deklaration der Funktion an. Wenn an eine Funktion Parameter übergeben werden, so müssen bei der Deklaration der Funktion mindestens die Datentypen der Parameter angegeben sein. Damit kann der Compiler später beim Aufruf der Funktion zum einen den richtigen Aufrufcode erzeugen und zum anderen auch eine Überprüfung der beim Aufruf angegebenen Parameter vornehmen.

Zusätzlich zum Datentyp kann auch ein beschreibender Parametername bei der Funktionsdeklaration 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. Im letzten Beispiel erhält die Funktion PrintIt() im ersten Parameter wahrscheinlich die X-Position und im zweiten Parameter die Y-Position für die Ausgabe. Die Anzahl der Parameter einer Funktion ist nicht begrenzt.


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

Bei der Definition der Funktion geben Sie genauso wie bei der Deklaration die Datentypen der Parameter an, nun jedoch zwingend gefolgt von einem Parameternamen. Wenn Sie schon bei der Deklaration die Parameter mit Namen versehen haben, so brauchen Sie bei der Definition der Funktion die Deklaration im Prinzip nur kopieren und das abschließende Semikolon entfernen. In der Funktion können Sie dann über diese Parameternamen auf die an die Funktion übergebenen Werte zugreifen, d.h. die Parameternamen wirken sozusagen als Platzhalter für die im Aufruf tatsächlich angegebenen Werte. Die Parameternamen bei der Deklaration und der Definition der Funktion müssen nicht zwingend übereinstimmen, aber der besseren Lesbarkeit wegen sollten sie es. Wie Daten beim Aufruf an die Funktion übergeben werden, das kommt gleich noch. Im Beispiel sehen Sie eine Funktion zum Ausdruck einer Kopfzeile auf einer Seite. Die aktuelle Seitennummer wird dabei als Parameter an PrintHeader() übergeben.


// 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;
}

Achten 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 Ihnen einen Fehler!

called-by-value Parameter

Bei einem Parameter vom Typ called-by-value erhält die aufgerufene Funktion lediglich eine Kopie des übergebenen Wertes. Daraus folgt, dass Änderung des Parameters zwar innerhalb der Funktion erlaubt sind, diese aber auch nur auf die Kopie wirken. Wird die Funktion wieder verlassen, so hat sich am Wert des übergebenen Parameters nichts geändert. Die Definition eines called-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.


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

Beim Aufruf von Funktion können Sie für einen called-by-value Parameter entweder eine Variable (erster Aufruf oben) oder aber eine Konstante (zweiter Aufruf) an die Funktion übergeben.

Da die Funktion eine Kopie des Wertes erhält, kann es bei dieser Parameterart unter Umständen notwendig sein, sehr viele Daten zu kopieren. Dies gilt insbesondere dann, wenn die Funktion 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 Parameter dauerhaft zu verändern, so dass die innerhalb der Funktion durchgeführte Änderungen an den Parametern auch nach dem Verlassen der Funktion noch gültig sind.

Eine Referenz ist 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 called-by-value übergeben.

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


// 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)
{
   short temp = val1;
   val1 = val2;
   val2 = temp;
}

Was Sie nicht so ohne weiteres als Referenzparameter übergeben können, 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 Parameter die per Referenz übergeben wurden (da kein Kopiervorgang der Daten notwendig) 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.


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

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.

Kennzeichnen Sie grundsätzlich alle nicht veränderbaren Parameter, die nicht per called-by-value übergeben werden, immer als const. In der englisch-sprachigen Literatur wird dies auch als const-correctness bezeichnet.

Übergabe von eindimensionalen Feldern

Oftmals ist es notwendig, Felder an Funktionen zu übergeben. Sehen wir uns zuerst wieder den einfacheren Fall an, dass ein eindimensionales Feld an eine Funktion übergeben werden soll.

Bei der Übergabe ein eindimensionales Feld an eine Funktion wird lediglich dessen Anfangsadresse übergeben. Sie wissen doch noch hoffentlich aus der Lektion über Felder, dass der Name eines eindimensionalen Feldes nichts anderes ist als der Zeiger auf den Beginn des Feldes. Und damit erhält die Funktion durch die Angabe des Feldnamens als Parameter einen entsprechenden Zeiger von Datentyp des Feldes. Doch wie wird nun innerhalb der Funktion auf die einzelnen 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. Sie müssen dabei nur beachten, dass das erste Element den Index 0 besitzt!


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

// main() Funktion
int main ()
{
   // Felddefinition und Initialisierung
   short array[] = {10,20,30,40};
   ...
   DoSomething(array);
   ...
}

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

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

Und noch ein gut gemeinter Rat. Wenn Sie innerhalb der Funktion den Inhalt des Feldes nicht verändern, definieren Sie den Feldinhalt als const. 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 und somit indiziert auf die Feldelemente zugegriffen werden. 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. Damit kann das vorherige Beispiel auch wie angegeben umgeschrieben werden.


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

void DoSomething (const short* const ptr)
{
   // Zugriff auf 1. Element
   short var = ptr[0];
   // 3. Element verändern
   ptr[2] = 10;
}

Wenn 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[]);

Übergabe von mehrdimensionale Feldern

Ein klein wenig komplizierter sieht die Sache bei der Übergabe von mehrdimensionalen Feldern aus. Hier müssen Sie dem Compiler etwas unter die Arme greifen, damit er die Feldelemente im Speicher auch findet. Bei der Deklaration und der Definition der Funktion müssen Sie die Feldgröße des übergebenen Feldes mit angegeben, 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 gegenüber eindimensionalen Feldern unverändert, d.h. in der Parameterklammer der Funktion steht auch hier lediglich der Name des zu übergebenden Feldes.


// 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
   short var = arr[0][2];
}

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

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

Funktionsrückgabewert

Außer dass Funktionen Werte 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 (für eine eventuelle Typanpassung). Dieser Datentyp wird vor dem Funktionsnamen angegeben, und zwar sowohl bei der Deklaration wie auch bei der Definition der Funktion. Die Rückgabe des eigentlichen Wertes erfolgt dann mit einer return-Anweisung innerhalb der Funktion. Nach dem Schlüsselwort return folgt der zurückzugebende Wert, der natürlich mit dem Datentyp vor dem Funktionsnamen übereinstimmen muss. Innerhalb einer Funktion können, wenig notwendig, durchaus mehrere return-Anweisungen stehen.


// Funktionsdeklaration
float Deg2Rad (float deg);

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

float Deg2Rad (float deg)
{
   const float PI = 3.1416f;
   float result;
   result = deg/360.0f * 2.0f * PI;
   return result;
}

Die Funktion Deg2Rad(...) oben erhält als Parameter eine Gradzahl als float-Wert übergeben und rechnet diese dann ins Bogenmaß um (3600 = 2*Pi).

Die obige Funktion Deg2Rad(...) erhält den Wert als called-by-value Parameter übergeben. Als Alternative hätten Sie den Wert auch als const-Referenz übergeben können, also in folgender Form:

float Deg2Rad (const float& deg);

Wichtig ist hier die Angabe von const, da Deg2Rad(...) ansonsten den übergebenen Wert 'dauerhaft' verändern könnte und Sie auch keine Konstanten an die Funktion übergeben können.

Wollen Sie mehr als nur einen Wert aus der Funktion zurückliefern, so müssen Sie dies bis jetzt über entsprechende Referenzparameter tun.

Rekursive Funktionen

Sehen wir uns jetzt noch einen 'Spezialfall' 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. Diese Funktionen benötigen aber immer ein Abbruchkriterium, in etwa der folgenden Form um eine Endlos-Schleife zu vermeiden:

if (Bedingung)
   return Wert;

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


#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 (int 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;
}

 * * * *
 * * *
 * *
 *

Oben sehen Sie ein Beispiel für eine solche rekursive Funktion. Die Funktion PrintLine(...) erhält aus 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 sich die Funktion erneut selbst auf, jetzt doch mit einem Sternchen weniger. Dieses Spiel wiederholt sich so lange, bis nur noch ein Sternchen ausgegeben wird. In diesem Fall ist die Bedingung der if-Abfrage nicht mehr erfüllt 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!

Sonstiges zu Funktionen

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

Und 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 Freaks.

Beispiel und Übung

Beispiel:

In diesem Beispiel geht's mathematisch zu. Jeder kennt aus seiner Schulzeit noch die Winkelfunktion Sinus. Zur Berechnung des Sinus gibt es zwar auch eine fertige Bibliotheksfunktion, wir werden aber zur Berechnung des Sinus eine Annährungsformel verwenden und dann auch einmal nachschauen, wie genau diese Annäherung an die Bibliotheksfunktion ist.

Die Annäherungsformel zur Berechnung des Sinus lautet:

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

Hierbei entspricht z.B. x3 dem Ausdruck x*x*x. Der Ausdruck 3! ist die Fakultät von 3, d.h. 1*2*3. Die Berechnung des Näherungswerts erfolgt in der Funktion BerechneSin(...). Die Übergabe des Werts, aus dem der Sinus berechnet werden soll, erfolgt als called-by-value da die Funktion den Wert nicht verändern muss.

Zur Überprüfung der Rechengenauigkeit der Funktion BerechneSin(...) wird dann die Bibliotheksfunktion sin(...) aufgerufen, die folgende Funktionsdeklaration besitzt:

double sin(double value);

Sie berechnet aus dem übergebenen double-Wert value den Sinus und liefert diesen ebenfalls als double-Wert zurück. Die für den Aufruf der Funktion sin(...) notwendige Funktionsdeklaration ist in der Headerdatei cmath enthalten.

Wenn Sie in den Online-Hilfe nach der Funktion sin(...) suchen, so werden Sie dort anstelle der Header-Datei cmath teilweise die Header-Datei math.h vorfinden. Der Grund hierfür ist, dass sin(...) eigentlich eine C-Funktion ist und in C-Programme Header-Dateien die Extension .h besitzen.

In der main() Funktion werden dann für drei verschiedene Wert die Sinuswerte berechnet, und zwar einmal mit der Funktion BerechneSin(...) und einmal mit der Bibliotheksfunktion sin(...). Da die berechneten Werte nicht weiter ausgewertet werden, werden die Funktionsaufruf direkt innerhalb der cout-Anweisungen durchgeführt.

sin(0.5) : Berechnet = 0.479426, Bibl-Funktion = 0.479426
sin(PI)  : Berechnet = -0.0752295, Bibl-Funktion = -7.34641e-006
sin(PI/2): Berechnet = 0.999843, Bibl-Funktion = 1


// Beispiel zu Funktionen

// Dateien einbinden
#include <iostream>
#include <cmath>

using std::cout;
using std::endl;

// Funktionsdeklarationen
double BerechneSin(double wert);

// main() Funktion
int main()
{
   // Konstante PI definieren
   const double PI = 3.1416;

   // Berechne Sinus(0.5) mittels Reihe
   cout << "sin(0.5) : Berechnet = " << BerechneSin(0.5);
   // Und nun Sinus(0.5) mittels Bibliotheksfunktion
   cout << ", Bibl-Funktion = " << sin(0.5) << endl;
   // Das ganz nun für PI (3.1416)
   cout << "sin(PI) : Berechnet = " << BerechneSin(PI);
   cout << ", Bibl-Funktion = " << sin(PI) << endl;
   // und zum Schluss für PI/2
   cout << "sin(PI/2): Berechnet = " << BerechneSin(PI/2.0);
   cout << ", Bibl-Funktion = " << sin(PI/2.0) << endl;
}

// Funktion: BerechneSin
// Berechnet Sinus mittels Reihe
//       sin(x) = x - x^3/3! + x^5/5! - x^7/7!
// Parameter wert: Wert aus dem Sinus berechnet werden soll

double BerechneSin(double wert)
{
   // Ergebnis mit dem Wert vorinitialisieren
   // 1. Term in der Reihe

   double erg = wert;
   // Divident (Nenner) mit wert^3 vorbelegen
   double divident = wert*wert*wert;
   // Divisor (Teiler) 3! vorbelegen
   double divisor = 1.0*2.0*3.0;  // 3!
   // Zweiten Term vom Ergebnis subtrahieren
   erg -= divident/divisor;
   // Dritten Term zum Ergebnis addieren
   divident *= wert*wert;         // wert^5
   divisor *= 20.0;               // 3! * 4 * 5 = 5!
   erg += divident/divisor;
   // Vierten Term wieder vom Ergebnis subtrahieren
   divident *= wert*wert;         // wert^7
   divisor *= 42.0;               // 5! * 6 * 7 = 7!
   erg -= divident/divisor;
   // Ergebnis zurückliefern
   return erg;
}

Übung:

Nun zur Abwechslung mal etwas aus der Physik (aber nicht gleich erschrecken).

Für einen Wurf (schräger Wurf) sind die Wurfweite und die maximale Wurfhöhe zu berechnen. Für die Wurfweite gilt folgende Formel:

Weite = (Abwurfgeschwindigkeit2 * sin(2*Abwurfwinkel)) / G

Und für die Wurfhöhe gilt:

Höhe = Abwurfgeschwindigkeit2 * sin(Abwurfwinkel)2 / (2*G)

Die Funktionsdeklaration der Funktion sin(...) ist oben beim Beispiel angegeben. Beachten Sie, dass sin(...) den Winkel in Rad und nicht in Grad erwartet! Die Abwurfgeschwindigkeit ist in beiden Formeln in m/s und G ist die Gravitationskonstante 9.81m/s2. Beide Formeln liefern das Ergebnis in m zurück.

Schreiben Sie jetzt jeweils eine Funktion zur Berechnung der Wurfweite und eine Funktion zur Berechnung der Wurfhöhe bei vorgegebener Abwurfgeschwindigkeit und Abwurfwinkel.

In der main() Funktion ist nun zuerst bei einem fest vorgegebenem Abwurfwinkel von 45 Grad(!) die jeweilige Wurfweite und -höhe zu berechnen. Die Abwurfgeschwindigkeit ist im Bereich 10....20 m/s in 2er-Schritten zu durchlaufen. Die berechneten Daten sind dann in Tabellenform (wie unten angegeben) auszugeben.

Anschließend ist das ganze Spiel mit einer festen Abwurfgeschwindigkeit von 28m/s (entspricht ca. 100km/h) zu wiederholen, wobei jetzt der Abwurfwinkel im Bereich 30...60 Grad in 5er-Schritten zu variieren ist.

Beachten Sie bitte, dass die Winkelangaben zunächst in Grad erfolgen und die Funktion sin(...) als Einheit Rad erwartet. Sie wissen doch noch aus Ihrem Mathematik-Unterricht, dass 2*PI gleich 360 Grad sind? Damit lautet die Formel für die Umrechnung von Grad in Rad:

Rad = Grad / 360.0 * 2.0 * 3.1416

So, und nun viel Glück beim Lösen der Aufgabe. Bei richtiger Lösung sollten Sie die unten dargestellten Daten erhalten.

Schräger Wurf mit konst. Winkel von 45 Grad
Abwurfgeschw. (m/s):10, Wurfweit (m):10.19, Höhe (m): 2.55
Abwurfgeschw. (m/s):12, Wurfweit (m):14.68, Höhe (m): 3.67
Abwurfgeschw. (m/s):14, Wurfweit (m):19.98, Höhe (m): 4.99
Abwurfgeschw. (m/s):16, Wurfweit (m):26.10, Höhe (m): 6.52
Abwurfgeschw. (m/s):18, Wurfweit (m):33.03, Höhe (m): 8.26
Abwurfgeschw. (m/s):20, Wurfweit (m):40.77, Höhe (m):10.19

Schräger Wurf mit konst. Abwurfgeschw. 28 m/s
Abwurfwinkel (Grad):30, Wurfweit (m):69.21, Höhe (m): 9.99
Abwurfwinkel (Grad):35, Wurfweit (m):75.10, Höhe (m):13.15
Abwurfwinkel (Grad):40, Wurfweit (m):78.70, Höhe (m):16.51
Abwurfwinkel (Grad):45, Wurfweit (m):79.92, Höhe (m):19.98
Abwurfwinkel (Grad):50, Wurfweit (m):78.70, Höhe (m):23.45
Abwurfwinkel (Grad):55, Wurfweit (m):75.10, Höhe (m):26.81
Abwurfwinkel (Grad):60, Wurfweit (m):69.21, Höhe (m):29.97

Lösung ansehen!