Funktionen

Einführung

Ein Programm wird beim Ausführen Zeile für Zeile durchlaufen und die dort befindlichen  Befehle werden einzeln ausgeführt. Bisher haben wir uns, bis auf eine Ausnahme (nämlich den Anweisungen 'cout' und 'cin'), auf reines C/C++ gestützt. Das beschränkt sich bisher auf die vier Grundrechenarten, die logischen Operatoren, das Deklarieren von Variablen und das Zuweisen von Werten. Funktionen wie die Ein- und Ausgabe sind in dieser Basis nicht inbegriffen. Mit der Programmiersprache C/C++ werden weiterhin diverse Bibliotheken in der u.a. die Ein- und Ausgabe implementiert sind, ausgeliefert. Die bedeutendste ist  wohl die STL (Standard Template Library). Mit ihrer Hilfe werden weitere Funktionalitäten (neben der Ein- und Ausgabe auch mathematische Operationen wie Quadratwurzel, trigononometrische Funktionen etc.) ermöglicht. Es lassen sich durchaus auch eigene Funktionen schreiben. Eine bereits bekannte ist die Funktion 'main'. Sie wird beim Programmstart aufgerufen und durchlaufen. Alle Anweisungen werden Schritt für Schritt abgearbeitet. Nun wäre es doch sinnvoll, wenn diese Funktion nicht die gesamte Funktionalität des Programms beinhaltet, sondern in der Lage ist Teile ihrer Aufgaben von anderen Funktionen bewältigen zu lassen. Dies bietet sich vor allem dann an, wenn einige Aufgaben mehrmals erfüllt werden müssen. Wird diese Aufgabe nämlich von einer Funktion bewältigt, so brauch nur diese aufgerufen werden. Soll der Aufgabenbereich dann einmal geändert werden, so brauch nur die Funktion geändert werden, der Aufruf bleibt allerdings bestehen. So kann eine Funktion beliebig verbessert werden, ohne gleichzeitig die aufrufende Methode ändern zu müssen. Dies ist ein wesentlicher aber nicht der einzige Vorteil.

Implementierung einer Funktion

Angenommen Sie programmieren ein Programm, welches einige mathematische Berechnungen durchführen soll. Darunter soll auch eine Funktion enthalten sein, welche die Summe der ersten 'n' Zahlen ermittelt, d.h. das Programm ruft diese Funktion auf übergibt dabei die Zahl 'n' und bekommt daraufhin dann den gewünschten Wert zurück. In C wird die Deklaration der Funktion wie folgt realisiert:

unsigned int Sum(unsigned int n)
{
	//Variable, welche die Summe erhält
	unsigned int nSum = 0;

	//Summe berechnen
	nSum = (n + 1) * n / 2;

	//Den Wert an die aufrufende Funktion übergeben
	return nSum;
}

Allgemein ließe sich dies so formulieren:

<Rückgabetyp> <Funktionsname>(<Datentyp1> <Parameter1>, <Datentyp2> <Parameter2>, ... ,<Datentypn> <Parametern>)
{
	<Rückgabetyp> <Variablenname>;
	
	/* --- Wert bestimmen ---
	...
	*/

	//Wert zurückgeben
	return <Variablenname>
	
}

Der Rückgabetyp gibt an, welchen Datentyp die Variable besitzen soll, die mit der 'return'-Anweisung zurückgegeben werden soll.  Der Funktionsname ist wie der Name einer Variablen eine eindeutige Bezeichnung dieser Funktion. Bei der Wahl des Namens ist der Programmierer nur durch die Regeln gebunden, die schon bei der Wahl von Variablennamen besprochen wurden. Nachdem Funktionsliste folgt (in Klammern) die sogenannte Argumenten- oder Parameterliste, in der sämtliche Variablen, die übergeben werden sollen, für die Funktion deklariert werden um dann in ihrem Gültigkeitsbereich zur Verfügung zu stehen. Darauf folgt dann der Funktionsrumpf, der den eigentlichen Quellcode enthält um das gewünschte Ergebnis zu erreichen. Zum Schluss wird mit der 'return'-Anweisung eine entsprechende Variable zurückgegeben dessen Datentyp dem des Rückgabetyps der Funktion entspricht. 

Auf unsere Funktion übertragen bedeutet dies, dass in der Parameterliste die Variable 'n' deklariert wird, welche dann innerhalb der Funktion zum Berechnen der Summe verwendet wird.  Diese wird dann am Ende zurückgegeben und steht der aufrufenden Funktion für weitere Berechnungen zur Verfügung.

Deklaration einer Funktion

Soll eine Funktion an irgendeiner Stelle aufgerufen werden, dann muss diese zuvor bereits bekannt sein, d.h. die Implementierung der aufzurufenden Funktion müsste vor der aufrufenden stehen. Es ist allerdings auch möglich die Funktion an geeigneter Stelle nur zu deklarieren und die Implementierung an beliebiger Stelle vorzunehmen. Das bietet sich vor allem dann an, wenn ein Projekt aus mehreren Dateien besteht. Die Deklarationen könnten dann in irgendwelchen Headerdateien (*.h) stehen, während die tatsächlichen Implementierungen in einer Quellcodedatei (*.cpp) stehen können. Wird die Funktion innerhalb einer anderen Datei aufgerufen, muss in dieser nur die entsprechende Headerdatei eingebunden werden.

Eine Deklaration besteht nur aus dem Funktionskopf, d.h. für die obige Funktion 'Sum' sähe die Deklaration wie folgt aus:

unsigned int Sum(unsigned int n);

oder

unsigned int Sum(unsigned int);

Nicht zu vergessen ist das Semikolon am Ende der Deklaration. Bei der Deklaration kann man die Variablennamen der Parameter einfach weglassen. Die allgemeine Deklaration sähe in etwa wie folgt aus:

<Rückgabetyp> <Funktionsname>(<Datentyp1> <Parameter1>, <Datentyp2> <Parameter2>, ... ,<Datentypn> <Parametern>);

oder

<Rückgabetyp> <Funktionsname>(<Datentyp1>, <Datentyp2>, ... ,<Datentypn>);

So ließe sich in unserem Beispiel die Deklaration der Funktion 'Sum' in der Headerdatei "functions.h", die Implementierung in der Datei "functions.cpp" vornehmen. In der Datei "main.cpp" könnte dann die Implementierung der Funktion 'main' stehen, welche die Funktion 'Sum' aufruft. Die Datei könnte in etwa so aussehen:

//cout und cin
#include <iostream.h>

//Einbinden der Funktion Sum
#include "functions.h"

//Implementierung der Funktion main
void main()
{
	unsigned int n;

	cout << "Berechnet die Summe der ersten n Zahlen" << endl;
	cout << "Geben Sie n ein: "; cin >> n;

	//Summe berechnen
	cout << "Die Summe der ersten " << n << " Zahlen ist: " << Sum(n) << endl;
}

Das Programm berechnet dann die Summe der ersten 'n' Zahlen, wobei 'n' vom Benutzer eingegeben wird.

Hier noch einmal der komplette Quelltext dieses Programms:

functions_1_src.exe (Quelltext, gepackt)

Der Rückgabetyp void

Soll eine Funktion keinen Wert zurückgeben, sondern nur irgendetwas ausführen so wählt man als Rückgabetyp 'void'. Innerhalb der Funktion muss dann auch kein 'return' mehr stehen um einen Wert an das aufrufende Programm zurückzugeben. Lediglich, wenn eine Funktion vorzeitig beendet werden soll, dann kann man einen 'return'-Befehl einfügen. Dies ließe sich in etwa mit einem Aufruf von 'break' innerhalb einer Schleife vergleichen. Im allgemeinen sieht die Implementierung einer solchen Funktion wie folgt aus:

void <Funktionsname>(Datentyp1> <Parameter1>, <Datentyp2> <Parameter2>, ... ,<Datentypn> <Parametern>)
{
	/*
	...
	*/

	//evtl. ein einzelner return-Aufruf
	return;

}

Eine Funktion, die Beispielsweise irgend etwas auf dem Bildschirm anzeigt oder etwas aktualisiert, könnte z.B.  vom Datentyp 'void' sein. Oft geben solche Funktionen auch Werte (z.B vom Datentyp 'bool') zurück, die angeben ob die Aktualisierung erfolgreich war oder nicht.

Werden keine Parameter benötigt, kann man in die Argumentliste anstatt der fehlenden Parameter einfach 'void' eintragen. Die Deklaration einer Funktion 'Actualize' könnte dann in etwa so aussehen:

void Actualize();

oder

void Actualize(void);

Referenzen oder Zeiger als Parameter

Parameter, welche einer Funktion übergeben werden,  werden kopiert und stehen dann als Variablen mit eigenem Speicherbereich innerhalb der Funktion zur Verfügung. Änderungen dieser Variablen innerhalb der Funktion haben außerhalb der Funktion keine  Auswirkung. Unter Umständen ist dies jedoch erwünscht. Beispielweise könnte man auf diese Weise erreichen, dass eine Funktion praktisch mehrere Werte zurückgibt. Angenommen eine Funktion 'exchange' soll die Werte zweier Ganzzahlen (Datentyp 'int') vertauschen. Mit Hilfe von Referenzen, ließe sich dies ganz einfach so implementieren:

void exchange(int& a, int& b)
{
	int c = b;
	b = a;
	a = c;
}

Zuerst wird also der Wert von 'b' in einer lokalen Variablen 'c' gespeichert. Anschließend bekommt 'b' den Wert von 'a' zugewiesen. Wird die Funktion, dann aufgerufen, dann könnte dies wie folgt aussehen:

int var1 = 1, var2 = 2;
exchange(var1, var2);		//var1 = 2, var2 = 1

Mit Hilfe von Zeigern ließe sich die Funktion wie folgt umschreiben: 

void exchange(int* pa, int* pb)
{
	if (!pa || !pb) return;

	int c = *pb;
	*pb = *pa;
	*pa = c;
}

Der Aufruf der Funktion könnte dann in etwa so aussehen:

int var1 = 1, var2 = 2;
exchange(&var1, &var2);		//var1 = 2, var2 = 1

Beide Varianten sind möglich und liefern das selbe Ergebnis. Allerdings haben beide ihre Vor- und Nachteile. Bei Zeigern beispielsweise muss zunächst überprüft werden, ob sie gültig sind. Referenzen hingegen sind immer gültig, da sie schon bei der Deklaration initialisiert werden müssen und nicht mehr geändert werden können. Allerdings kann das wiederum auch ein Vorteil sein. Angenommen man ruft eine Funktion auf, mit der man die gegenwärtige Zeit bestimmen kann. Es sollen, Jahr, Monat, Tag, etc. abfragbar sein. Übergibt man die Parameter per Zeiger so könnte man den Jeweiligen Parameter für die Informationen, die man nicht benötigt (z.B. die Sekunden) auf '0' (also auf einen ungültigen Wert) setzen. Die Funktion brauch dann diese Information gar nicht mehr zu berücksichtigen. Da Referenzen aber immer gültig sein müssen, ließe sich dies mit Referenzen nicht ermöglichen. 

Ein weiterer Punkt ist die Art des Aufrufes. Wenn man einer Funktion Variablen in Form von Referenzen übergibt, sieht man es dem Aufruf nicht an. Übergibt man sie hingegen mit Zeigern, dann muss der Adressoperator vorangestellt werden und der Aufruf unterscheidet sich deutlich von einem Aufruf bei dem einfach nur Werte übergeben werden.

Ein weiterer wesentlicher Vorteil von Zeigern ist, dass immer (egal welcher Datentyp) immer 4 Byte kopiert werden. Kopieren von Daten bedeutet immer Rechenzeit, je mehr kopiert wird, desto länger ist die Rechenzeit. Bei größeren Datentypen (z.B. in Form von Klassen) sollte man möglichst Zeiger verwenden, wenn man Daten übergibt. Man kann durch ein vorangestelltes 'const' auch noch verhindern, dass dann der Wert innerhalb der Funktion geändert werden kann.

Überladungen von Funktionen

In C++ ist es möglich Funktionen zu überladen, d.h. man kann eine Funktion in beliebig vielen Varianten implementieren. Die einzige Voraussetzung ist, dass sich alle Varianten in ihrer Argumentliste unterscheiden. So ist es z.B. möglich, die Funktion 'exchange' für jeden anderen Datentyp auch noch mal zu implementieren. So wäre folgende beiden nebeneinander existierenden Implementierungen durchaus möglich:

void exchange(int& a, int& b)
{
	int c = b;
	b = a;
	a = c;
}
void exchange(double& a, double& b)
{
	double c = b;
	b = a;
	a = c;
}

Die Argumentlisten können sich aber auch einfach nur in der Anzahl ihrer Parameter unterscheiden. Denkbar wäre z.B. auch dass der Funktion einfach ein Array mit zwei Elementen übergeben  wird. Also in etwa wie folgt:

void exchange(int Arr[])
{
	int c = Arr[1];
	Arr[1] = Arr[0];
	Arr[0] = c;
}

Bei dieser Variante muss natürlich vorausgesetzt werde, dass 'Arr' mindestens zwei Elemente besitzt. Sonst kommt es im harmlosesten Fall zu falschen Ergebnissen. 

Template-Funktionen

Die Funktion 'exchange' ließe sich theoretisch für jeden Datentyp implementieren. Es wäre allerdings mühselig, dies auch tatsächlich für jeden Datentyp einzeln zu implementieren, zumal es praktisch jedes Mal die gleiche Implementierung mit nur jeweils unterschiedlichen Datentypen ist. Zu diesem kann man in C++ einmalig eine Vorlagen-Funktion implementieren. Für unser Beispiel sähe sie dann in etwa so aus:

template <typename T>
void exchange(T& a, T& b)
{
	T c = b;
	b = a;
	a = c;

}

Man definiert die Funktion 'exchange' so wie oben nur mit einem unbekannten Datentyp (hier 'T'). Davor fügt man die Zeile 'template <typename T>' ein und schon ist die Funktion für alle Datentypen definiert. Die allgemeine Definition sieht so aus:

template <typename <T1>, typename <T2>, ... ,typename <Tn>>
<Rückgabetyp> <Funktionsname>(<Argumentliste>)
{
	Rückgabetyp> <Variablenname>;
	
	/* --- Wert bestimmen ---
	...
	*/

	//Wert zurückgeben
	return <Variablenname>
}

Die Argumentliste kann aus Parametern aller Datentypen (einschließlich '<T1>', '<T2>',  etc.) bestehen. Entscheidend ist, dass man Deklaration und Implementierung bei Vorlagenfunktionen nicht trennen darf. So muss der Implementierung der Funktion  die Deklaration vorausgegangen sein. Es ist also nicht möglich die Implementierung der Funktion in einer Quelcodedatei (.cpp) vorzunehmen, wenn nicht in der gleichen die Deklaration der Funktion  (zumindest durch Inkludieren einer Header-Datei) vorausgegangen ist. Am besten implementiert man die Funktion einfach in einer Headerdatei. 

Standardwerte

In C++ kann man bei einer Funktionsdeklaration Standardwerte für die einzelnen Parameter mitgeben. Werden bei diesen Parametern beim Funktionsaufruf keine Werte mehr übergeben, dann werden einfach die Standardwerte verwendet. Wenn man das kurz erwähnte Beispiel mit der Zeitfunktion noch einmal aufgreift so könnte die Deklaration in etwa so aussehen:

void GetTime(int *pYear, int *pMonth, int *pDay, int *pHour, int *pMinute, int *pSecond);

oder

void GetTime(int*, int*, int*, int*, int*, int*);

Wenn man jetzt bei allen als Standardwert '0' eintragen will, dann geht dies wie folgt:

void GetTime(int *pYear = 0, int *pMonth  = 0, int *pDay = 0, int *pHour = 0, 
		int *pMinute = 0, int *pSecond = 0);

oder

void GetTime(int* = 0, int* = 0, int* = 0, int* = 0, int* = 0, int* = 0);

Wenn die Funktion die Werte für Jahr, Monat, Tag, etc einfach nicht setzt wenn die Parameter auf '0' gesetzt sind und die aufrufende Methode nur die ersten zwei Parameter setzt, dann werden auch nur die ersten zwei Werte (Jahr und Monat) ermittelt. Der Aufruf sähe dann wie folgt aus:

//Variablen deklarieren
int Year, Month;

//Zeit ermitteln
GetTime(&Year, &Month);

//den Tag ermitteln
int Day;
GetTime(0, 0, &Day);

Für die nächsten Lektionen

Es sollte nun klar sein, wie Funktionen, deklariert, implementiert und überladen werden, da dies vor allem in Bezug auf Klassen immer wieder auftaucht.

Zurück Nach oben Weiter