C++ Funktionen sind kleine Unterprogramme, die Teilprobleme lösen. Funktionen sind ein wichtiges Werkzeug um den Quelltext eines Programms zu ordnen und wesentliche Algorithmen und zusammenhängende Anweisungsblöcke der main()-Hauptfunktion in einer zusammenhängenden Form auszulagern. C++ Funktionen werden ausserhalb der main()-Hauptfunktion definiert und vereinfachen somit das Verständnis und die Lesbarkeit des Quelltextes.
Im Grunde bedeutet die Definition einer Funktion nichts Anderes, als eine Code-Block (Anweisungsblock) mit einem Funktionsnamen zu verbinden. Die Definition einer C++ Funktion besteht aus einer Deklaration und einem Anweisungsblock.
Für die Deklaration einer C++ Funktion sind gewisse Angaben erforderlich (siehe nebenstehendes Frame) und gewisse zusätzliche Spezifikationen möglich (z.B. 'inline' oder 'virtual'). Um eine Funktion schliesslich zu definieren, muss man zusätzlich noch die zu erledigenden Anweisungen in einem Anweisungsblock zusammenfassen. Die allgemeine Definition einer C++ Funktion ist der formalen Definition einer mathematischen Funktion nicht unähnlich und man könnte sie auch wie folgt definieren: "Eine C++ Funktion ist eine Abbildung von dem Datenraum der Argumentenliste in den Datenraum des Rückgabetyps. Die dabei benutzte Abbildungsvorschrift findet sich in ihrem Anweisungsblock. Formal besitzt sie somit die folgende Struktur:"
Der 'Rückgabe Typ' kann hierbei eine der schon besprochenen Datentypen (z.B. int oder doube) oder ein Daten-Array (siehe nächste Vorlesung) sein. Falls der 'Rückgabe Typ' als void gekennzeichnet wird, gibt die Funktion keine Daten an das Hauptprogramm zurück, und führt nur den 'Block von Anweisungen' aus. Die 'Argumentenliste' setzt sich aus einer Liste von Datentypen der formalen Argumente (Parameter) der Funktion zusammen, die jeweils mit einem Komma voneinander getrennt sind. Betrachten wir z.B. den Quelltext des Beispielprogramms Switch_0.cpp aus dem vorigen Unterkapitel C++ Anweisungen: Auswahlanweisungen mit if und switch.
#include <iostream> // Ein- und Ausgabebibliothek #include <cmath> // Bibliothek für mathematisches (e-Funktion, Betrag, ...) using namespace std; // Benutze den Namensraum std int main(){ // Hauptfunktion double zahl; // Deklaration der Double Variable 'zahl' int auswahl; // Deklaration der Integer Variable 'auswahl' für switch cout << "Geben Sie bitte eine Gleitkommazahl ein: "; // Ausgabe eines Textes cin >> zahl; // Einlesen der Zahl mittels der Tastatur cout << "Möchten Sie die Zahl ..." << endl; ; // Ausgabe eines Textes cout << "... mal 25 nehmen, dann geben Sie 1 ein." << endl; // Ausgabe eines Textes cout << "... die Zahl hoch 5 nehmen, dann geben Sie 2 ein." << endl; // Ausgabe eines Textes cout << "... den Sinus dieser Zahl berechnen, dann geben Sie 3 ein." << endl; // Ausgabe eines Textes cin >> auswahl; // Einlesen der Variable 'auswahl' switch( auswahl ){ // switch-Anweisung Anfang case 1: // switch-Fall Nr.1 cout << "Der berechnete Wert beträgt: " << 25*zahl << endl; // Anweisung Nr.1 break; // switch-Fall Nr.1 Ende case 2: // switch-Fall Nr.2 cout << "Der berechnete Wert beträgt: " << pow(zahl,5) << endl; // Anweisung Nr.2 break; // switch-Fall Nr.2 Ende case 3: // switch-Fall Nr.3 cout << "Der berechnete Wert beträgt: " << sin(zahl) << endl; // Anweisung Nr.3 break; // switch-Fall Nr.3 Ende default: // switch-default cout << "Leider haben Sie die falsche Auswahl getroffen :-(" << endl; // Anweisung default break; // switch-default Ende } // switch-Anweisung Ende cout << "Vielen Dank für Ihre Eingabe." << endl; // Ausgabe eines Textes }
#include <iostream> // Ein- und Ausgabebibliothek #include <cmath> // Bibliothek für mathematisches (e-Funktion, Betrag, ...) using namespace std; // Benutze den Namensraum std void print_wert (double arg_1, int arg_2){ // Definition der Funktion 'print_wert' switch( arg_2 ){ // switch-Anweisung Anfang case 1: // switch-Fall Nr.1 cout << "Der berechnete Wert beträgt: " << 25*arg_1 << endl; // Anweisung Nr.1 break; // switch-Fall Nr.1 Ende case 2: // switch-Fall Nr.2 cout << "Der berechnete Wert beträgt: " << pow(arg_1,5) << endl; // Anweisung Nr.2 break; // switch-Fall Nr.2 Ende case 3: // switch-Fall Nr.3 cout << "Der berechnete Wert beträgt: " << sin(arg_1) << endl; // Anweisung Nr.3 break; // switch-Fall Nr.3 Ende default: // switch-default cout << "Leider haben Sie die falsche Auswahl getroffen :-(" << endl; // Anweisung default break; // switch-default Ende } // switch-Anweisung Ende cout << "Vielen Dank für Ihre Eingabe." << endl; // Ausgabe eines Textes } // Ende des Anweisungsblocks der Funktion 'print_wert' int main(){ // Hauptfunktion double zahl; // Deklaration der Double Variable 'zahl' int auswahl; // Deklaration der Integer Variable 'auswahl' für switch cout << "Geben Sie bitte eine Gleitkommazahl ein: "; // Ausgabe eines Textes cin >> zahl; // Einlesen der Zahl mittels der Tastatur cout << "Möchten Sie die Zahl ..." << endl; ; // Ausgabe eines Textes cout << "... mal 25 nehmen, dann geben Sie 1 ein." << endl; // Ausgabe eines Textes cout << "... die Zahl hoch 5 nehmen, dann geben Sie 2 ein." << endl; // Ausgabe eines Textes cout << "... den Sinus dieser Zahl berechnen, dann geben Sie 3 ein." << endl; // Ausgabe eines Textes cin >> auswahl; // Einlesen der Variable 'auswahl' print_wert(zahl, auswahl); // Aufruf der oben definierten Funktion }
Es ist ein wenig unschön, dass in den einzelnen case-Marken die Textausgaben separat stehen und auch im Prinzip immer der gleiche Text ausgegeben wird, lediglich mit anderem Ergebnis. Eine wiederkehrende Textausgabe von gleicher qualitativer Struktur. Diese Textausgabe hätte man z.B in eine Funktion auslagern können. Man könnte jedoch auch die gesamte switch-Anweisung inklussive der Textausgaben in einer Funktion zusammen fassen. Wir wählen als Funktionsname z.B. 'print_wert'. Die 'Argumentenliste' der Funktion besteht aus den Parameter von dem die Terminal Ausgabe abhängig ist: Die vom Benutzer eingegebene double-Zahl und der int-Wert der Auswahl. Obwohl die Funktion dann eine Terminalausgabe erzeugt, hat sie formal keinen Rückgabe-Wert, sodass wir als 'Rückgabe Typ' void deklarieren. Eine reine Deklaration einer solchen Funktion hätte somit das folgende Aussehen:
void print_wert (double , int );
Die Listenanordnung der 'Argumentenliste' ( double , int ) impliziert dabei, dass das erste Argument die eingegebene Zahl und das zweite, die eingegebene Auswahl ist.
Das nebenstehende untere Programm (Switch_0_Funktion.cpp) zeigt den Quelltext mit ausgelagerter 'print_wert' Funktion. Bei der Definition der Funktion wurden in der 'Argumentenliste' die Variablen 'arg_1' und 'arg_2' verwendet und als double- bzw. int-Datentyp deklariert. Im Anweisungsblock der Funktion wurde nun der gesamte Code-Block der switch-Anweisung mit den Textausgaben eingefügt und die Variablen auf 'arg_1' und 'arg_2' umbenannt (man hätte hier auch bei den Variablen 'zahl' und 'auswahl' bleiben können). Die Variablen 'arg_1' und 'arg_2' werden auch als Lokale Variablen bezeichnet, da ihr Gültigkeitsbereich auf die Funktion beschränkt ist.
In der main()-Hauptfunktion wird dann leiglich noch die definierte Funktion aufgerufen:
print_wert(zahl, auswahl);
Hierbei wird beim Aufruf der Funktion, der Aufrufoperator '(...)' angewendet und die formalen Argumente der Funktion ('arg_1' und 'arg_2') werden mit den aktuellen Werten der korrespondierenden Variablen ('zahl' und 'auswahl') initialisiert.
Dieses Beispiel sollte lediglich die Verwendung von Funktionen illustrieren und es ist wohl kein besonderns gelungenes Beispiel, da der gesamte Quelltext durch die Auslagerung der switch-Anweisung nur marginal besser lesbar und sicherlich nicht kürzer ist. Bei der Konstruktion von Funktionen sollte man beachten, dass man sinnvolle, zusammenhängende Anweisungen in sorgfältig benannten Funktionen verpackt, die am besten immer nur eine logische Operation ausführen. Man sollte den 'Block von Anweisungen' von C++ Funktionen nicht zu lang werden lassen und eine Definition einer Funktion mit mehreren 100 Zeilen sollte vermieden werden. Besteht die 'Argumentenliste' aus großen Datenarrays, dann ist die Verwendung einer Referenz auf diese Datenwerte oft sinnvoller (siehe nächste Vorlesung).
Auf die Verwendung von Funktionen und ihre Einbettung in eine Objekt-orientierte Klassenstruktur wird in einer späteren Vorlesung noch genauer eingegangen. Im Folgenden werden wir jedoch noch auf eine wichtige Untermenge der C++ Funktionen eingehen, die sogennanten mathematischen C++ Funktionen.
In C++ hat das Wort Funktion eine allgemeinere Bedeutung als im Bereich der Mathematik und die in der Mathematik und Physik definierte Funktionen stellen eine echte Teilmenge der C++ Funktionen dar. In diesem Teilkapitel möchten wir weiter in den Themenbereich der C++ Funktionen einführen und uns hauptsächlich mit der Definition von mathematischen Funktionen in C++ befassen. Betrachten wir uns z.B. die Funktion $f(x)=x^2$. In der Mathematik definiert man diese Funktion als eine Abbildung von der Menge der reellen Zahlen ℝ in die Menge der positiv reellen Zahlen ℝ$^+$ und man schreibt formal: $$ \begin{equation} f: \underbrace{ℝ}_{\hbox{Definitionsmenge}} \rightarrow \underbrace{ℝ^+}_{\hbox{Bildmenge}} \quad \hbox{mit:} \quad f(x)=x^2 \,\,\, , \,\,\, x \in ℝ \end{equation} $$ Wir möchten nun diese mathematische Funktion in C++ formulieren und z.B. ihre Funktionswerte in einem Teilintervall ihrer Definitionsmenge $[a,b] \in ℝ$ ausgeben lassen. Die einfachste C++ Version der oben definierten mathematischen Funktion lautet:
/* Definition der Funktion f(x)=x^2 * und Ausgabe ihrer Funktionswerte im Telintervall [a,b]=[-3,3] * Ausgabe zum Plotten (Gnuplot oder Python) mittels: "./a.out > Funktion_Math_0.dat" */ #include <iostream> // Ein- und Ausgabebibliothek double f (double x) { // Deklaration und Definition der Funktion f(x) double wert; // Lokale double-Variable (nur im Bereich der Funktion gültig) wert = x*x; // Eigentliche Definition der Funktion return wert; // Rueckgabewert der Funktion f(x) } // Ende der Funktion f(x) int main(){ // Hauptfunktion int i; // Deklaration des Laufindex als ganze Zahl const double a = -3; // Untergrenze des x-Intervalls [a,b] const double b = 3; // Obergrenze des x-Intervalls [a,b] const int N_xp=300; // Anzahl der Punkte in die das x-Intervall aufgeteilt wird const double dx = (b - a)/N_xp; // Abstand dx zwischen den aequidistanten Punkten des x-Intervalls double p, fp; // Deklaration der Variablen des x- und f(x)-Wertes printf("# 0: Index i \n# 1: x-Wert \n# 2: y-Wert\n"); // Beschreibung der ausgegebenen Groessen for(i=0; i<=N_xp; ++i){ // Schleifen Anfang p = a + i*dx; // Festlegung des aktuellen x-Wertes fp = f(p); // Berechnung des f(x)-Wertes printf("%4d %14.10f %14.10f \n",i, p, fp); // Ausgabe der berechneten Werte } // Ende der for-Schleife }
Die Definitionsmenge der Funktion spiegelt sich in dem Datentyp der 'Argumentenliste' (hier nur ein Argument: (double x)) wider. Die Bildmenge wird durch den 'Rückgabe Typ' der Funktion ausgedrückt (double f, eigentlich unsigned double, das gibt es aber nicht). Die Rückgabe des y-Wertes der Funktion wird mittels der Anweisung 'return x*x;' im Anweisungensblock der Funktion erreicht. Bei komplizierteren Funktionen ist die Verwendung einer zusätzlichen lokalen Variable sinnvoll.
Im nebenstehenden Programm wird die gleiche Funktion definiert, der Allgemeinheit halber ist jedoch im Anweisungensblock der Funktion die lokale double Variable 'wert' verwendet worden, der dann der aktuelle Funktionswert übergeben wird. Das Programm gibt dann mittels einer for-Schleife 300 äquidistante Funktionswerte des Teilintervalls $[-3,3]$ aus, die man sich dann z.B. mit einem Python Skript darstellen kann.
In gleicher Weise können auch mathematische Funktionen dargestellt werden, die eine kompliziertere Abbildungsstruktur aufweisen. Nehmen wir z.B. eine Funktion, die eine Abbildung von dem ℝ$^3$ in den reelwertigen Raum ℝ darstellt: $$ \begin{equation} f: \underbrace{ℝ^3}_{\hbox{Argumentenliste}} \rightarrow \underbrace{ℝ}_{\hbox{Rückgabetyp}} \quad \hbox{mit:} \quad f(x,y,z)=x \cdot y^2 \cdot z^2 \,\,\, , \,\,\, x, y, z \in ℝ \end{equation} $$ Die entsprechende C++ Funktion hätte dann das folgende Aussehen:
Wir werden im Unterpunkt ... auf die allgemeinere Verwendung von Funktionen noch genauer eingehen und gerade in den Unterpunkten, die sich mit der Objekt-orientierten Programmierung befassen, sind Klassen-Funktionen, Konstruktoren und das Überladen von Funktionen ein wichtiges Thema.