====== Funktionen ======
Unter einer **Funktion** (function, in anderen Programmiersprachen auch **Prozedur** oder **Subroutine**
genannt) versteht man ein **Unterprogramm**, das eine **bestimmte Aufgabe** erfüllt.
Funktionen sind unter anderem sinnvoll, um sich oft **wiederholende Befehle zu kapseln**, so dass diese nicht jedes Mal neu geschrieben werden müssen. Zudem verbessert es die **Übersichtlichkeit**.
{{:inf:inf5ai_202324:05_algorithmik:5_08:pasted:20240122-063720.png}}
===== Parameter und Rückgabewert =====
Die spezielle Funktion main() ist uns schon mehrfach begegnet. In C+ + lassen sich Funktionen nach folgenden Kriterien unterscheiden:
* Eine Funktion kann Parameter besitzen, oder nicht.
* Eine Funktion kann einen Wert zurückgeben, oder nicht.
{{:inf:inf5ai_202324:05_algorithmik:5_08:pasted:20240122-062936.png?350}}
Dem Funktionsbegriff der Mathematik entsprechen diejenigen C+ + -Funktionen, die sowohl Parameter haben als auch einen Wert zurückgeben. Dieser Wert kann im Programm weiter genutzt werden, um ihn z.B. einer Variablen zuzuweisen.
int sum = add(3,5); // Aufruf einer Funktion
Damit diese Anweisung fehlerfrei kompiliert wird, muss vorher die **Funktion add() deklariert** worden sein. Bei einer Funktion bedeutet **Deklaration** die **Angabe des Funktionsprototyps**. Das heißt, der **Typ von Parametern und Rückgabewert** muss angegeben werden. Das folgende Beispiel deklariert bspw. eine Funktion, die einen **Parameter vom Typ int** besitzt und als **Rückgabewert einen int-Wert** zurückgibt.
{{:inf:inf5ai_202324:05_algorithmik:5_08:pasted:20240122-064204.png?350}}
int add (int a, int b); // Funktionsprototyp == Deklaration
Soll eine Funktion keinen Wert zurückliefern, lautet der **Rückgabetyp** formal **void**. \\
Nach dem Compilieren ist das Linken der entstanden Objektdateien zu einem ausführbaren Programm
nötig. Der Linker benötigt die Definition der aufzurufenden Funktion. Eine **Funktionsdefinition**
umfasst auch die **Implementation der Funktion**, d.h. den Code, der beim Aufruf der Funktion
ausgeführt werden soll. In unserem Fall wäre das:
{{:inf:inf5ai_202324:05_algorithmik:5_08:pasted:20240122-063446.png?350}}
int add(int a, int b); // Funktionsdeklaration
int main(){
int sum = add(3, 6); // Funktionsaufruf
// a hat jetzt den Wert 9
return 0;
}
int add(int a, int b){ // Funktionsdefinition
return a+b;
}
Der Compiler muss die Deklaration kennen, um eventuelle Typ-Unverträglichkeiten abzufangen.
Würden Sie die obige Funktion z.B. als int a = add(2.5, 3); aufrufen, käme die Warnung, dass add() ein ganzzahliges Argument erwartet und keine Fließkommazahl. Eine Definition ist für den Compiler auch immer eine Deklaration, das heißt Sie müssen nicht explizit eine Deklaration einfügen um eine Funktion aufzurufen, die zuvor definiert wurde.
{{:inf:inf5ai_202324:05_algorithmik:5_08:pasted:20240122-065315.png}}
Die **Trennung von Deklaration und Definition** kann zu **übersichtlicher Code-Strukturierung** bei größeren Projekten genutzt werden. Insbesondere ist es sinnvoll, Deklarationen und Definitionen in verschiedene Dateien zu schreiben. Oft will man, wenn man fremden oder alten Code benutzt, nicht die Details der Implementierung einer Funktion sehen, sondern nur das Format der Parameter o.ä. und kann so in der Deklarationsdatei (header file, üblicherweise mit der Endung .hpp, z.T. auch
.h oder .hh) nachsehen, ohne durch die Funktionsrümpfe abgelenkt zu werden. (Bei proprietärem Fremdcode bekommt man die Implementation in der Regel gar nicht zu Gesicht!)
Bei einer Deklaration ist es nicht nötig, die Parameternamen mit anzugeben, denn diese sind für den Aufruf der Funktion nicht relevant. Es ist allerdings üblich die Namen dennoch mit anzugeben um zu verdeutlichen was der Parameter darstellt. Der Compiler ignoriert die Namen in diesem Fall einfach, weshalb es auch möglich ist den Parametern in der Deklaration und der Definition unterschiedliche Namen zu geben. Allerdings wird davon aus Gründen der Übersichtlichkeit abgeraten.
Mit der Anweisung **return** gibt die **Funktion einen Wert zurück**, in unserem Beispiel x * x, wobei die Variable x als Parameter bezeichnet wird. Als Argument bezeichnet man eine Variable oder einen Wert, mit denen eine Funktion aufgerufen wird. Bei Funktionen mit dem Rückgabetyp void schreiben Sie einfach return; oder lassen die return-Anweisung ganz weg.
Nach einem **return** wird die **Funktion sofort verlassen**, d.h. alle nachfolgenden Anweisungen des Funktionsrumpfs werden ignoriert.
Erwartet eine Funktion mehrere Argumente, so werden die Parameter durch Kommas getrennt. Eine mit
int g(int x, double y);
deklarierte Funktion könnte z.B. so aufgerufen werden:
int a = g(123, -4.44);
Für eine leere Parameterliste schreiben Sie hinter dem Funktionsnamen einfach ().
int h(){ // Deklaration von h()
// Quellcode ...
}
int a = h(); // Aufruf von h()
===== Übergabe der Parameter (=Argumente) =====
In C+ + gibt es mehrere Varianten, wie einer Funktion die Argumente übergeben werden können:
- call-by-value
- call-by-reference
- call-by-pointer (siehe Kapitel Zeiger)
==== call-by-value (Übergabe per Wert) ====
Bei **call-by-value (Wertübergabe)** wird der **Wert des Arguments in einen Speicherbereich kopiert**, auf den die Funktion **mittels Parametername zugreifen** kann. Ein **Werteparameter** verhält sich wie eine **lokale Variable**, die „automatisch“ mit dem richtigen Wert initialisiert wird. Der Kopiervorgang kann bei Klassen (Thema eines späteren Kapitels) einen erheblichen Zeit- und Speicheraufwand bedeuten!
#include
void f1(int x) {
x = 3 * x;
std::cout << x << std::endl;
}
int main()
{
int a = 7;
f1(a); // Ausgabe: 21
f1(5); // Ausgabe: 15
std::cout << x; // Fehler! x ist hier nicht definiert
std::cout << a; // a hat immer noch den Wert 7
return 0;
}
=== Weiteres Beispiel ===
#include
#include
using namespace std;
//Prototyp, Deklaration
int sumW(int x,int y); //Parameterübergabe per Wert (Value)
int main(int argc, char** argv) {
cout << "Hello World" << endl;
int a=5,b=6,erg=0;
cout << "Parameteruebergabe per Wert" << endl;
erg=sumW(a, b);
cout << "a: " << a <
===Beispielhafte Speicherbelegung des Programms im Hauptspeicher:===
^Variable^Adresse^Wert^
|a|0x0001|5|
|b|0x0005|6|
|…|.|.|
|…|.|.|
|x|0x00a1|5 -> 6|
|y|0x00a5|6 -> 7|
===Die Ausgabe des Programms ist===
Hello World
Parameteruebergabe per Wert
x: 6
y: 7
a: 5
b: 6
==== call-by-reference (Übergabe per Referenz) ====
Die Sprache C kennt nur call-by-value. Sollen die von einer Funktion vorgenommen Änderungen auch für das Hauptprogramm sichtbar sein, müssen sogenannte Zeiger verwendet werden.
C+ + hingegen stellt Prozeduren und Funktionen ebenfalls **Zeiger (pointer)** zur Verfügung. C+ + gibt Ihnen aber auch die Möglichkeit, diese Zeiger mittels **Referenzen** zu umgehen. Beide sind jedoch noch Thema eines späteren Kapitels.
Im Gegensatz zu call-by-value wird bei **call-by-reference** die **Speicheradresse des Arguments** übergeben, also **der Wert nicht kopiert**. **Änderungen der (Referenz-)Variable betreffen** zwangsläufig auch die **übergebene Variable vom Hauptprogramm selbst** und bleiben nach dem Funktionsaufruf erhalten.
Um call-by-reference anzuzeigen, wird der **Operator &** verwendet, wie Sie gleich im Beispiel sehen werden. Wird keine Änderung des Inhalts gewünscht, sollten Sie den Referenzparameter als const deklarieren um so den Speicherbereich vor Änderungen zu schützen.
Fehler, die sich aus der ungewollten Änderung des Inhaltes einer übergebenen Referenz ergeben, sind in der Regel schwer zu finden.
Die im folgenden Beispiel definierte Funktion swap() vertauscht ihre beiden Argumente. Weil diese als Referenzen übergeben werden, überträgt sich das auf die Variablen mit denen die Funktion aufgerufen wurde:
#include
void swap(int &a, int &b)
{
int tmp = a; // "temporärer" Variable den Wert von Variable a zuweisen
a = b; // a mit dem Wert von Variable b überschreiben
b = tmp; // b den Wert der "temporären" Variable zuweisen (Anfangswert von Variable a)
}
int main()
{
int x = 5, y = 10;
swap(x, y);
std::cout << "x=" << x << " y=" << y << std::endl;
return 0;
}
**Ausgabe**
x=10 y=5