C++ Teil 10 – Funktionen I



Inhalt

Einleitung
Funktionen erstellen
Funktionsaufruf
Wertübergabe
Rückgabewert
Ein Beispiel
Übungsaufgaben

Einleitung

In diesem und in mindestens einem weiteren Teil dieser Artikelreihe werden wir uns mit Funktionen beschäftigen. Funktionen sind ein absolut grundlegendes Element einer (funktionaler) Programmiersprache. C++ ist zwar hauptsächlich eine objektorientierte Programmiersprache (dafür wurde sie schließlich entwickelt), aber im Gegensatz zu Java oder C# ist C++ auch gleichzeitig eine funktionale Programmiersprache und erlaubt einem Programmierer die Nutzung von Funktionen.
Das hier gewonnen Wissen wird uns durch alle nachfolgenden Kapitel begleiten und ist eine Grundvoraussetzung für das Verständnis der objektorientierten Programmierung.

Funktionen erstellen

Eine Funktion ist ein Programmblock bzw. eine Ansammlung von Anweisungen versehen mit einem Namen. Funktionen dienen in erster Linie dazu den Quellcode in möglichst unabhängige Teile aufzuspalten um doppelten Code zu vermeiden und die Übersichtlichkeit zu gewährleisten.

Schauen wir uns an, wie eine Funktion im Allgemeinen aussieht.

Rückgabedatentyp Funktionsname([Datentyp parameter1, Datentyp parameter2, Dat...])
{
  // hier stehen die Anweisungen
  
  [return Rückgabewert; falls Rückgabetyp nicht void]
}

Gehen wir die einzelnen Bestandteile einer Funktion durch.

Funktionsname: Der Name der Funktion. Für die Namensgebung gelten die gleichen Regeln wie für Variablen (sie dürfen nicht mit Zahlen anfangen ect.).

Parameter: Eine Funktion kann beim Aufruf Argumente bekommen. Das sind irgendwelche Werte, Variablen oder Zeiger, die in der Funktion verwendet werden. Zum Beispiel braucht eine Funktion, die die Länge eines Strings bestimmen logischerweise den String selbst als Argument.
Ein Parameter ist eine lokale Variable in der Funktion, die einen Argument beim Aufruf aufnimmt.
Wenn eine Funktion keine Parameter enthalten soll, dann lässt man die runden Klammern leer oder man schreibt dort das Schlüsselwort void rein.

Rückgabewert: Funktionen können nicht nur Werte bekommen, sie können auch selbst welche zurückgeben. Das kann beispielsweise ein Rechenergebnis sein. Hat man z.B. eine Funktion zur Berechnung der Kreisfläche, so wird der Radius aus Parameter übergeben und als Ergebnis bekommt man die Fläche.
Die Form der Rückgabeanweisung sieht immer folgendermaßen aus.

return Rückgabewert;

Der Rückgabewert kann wie schon bei den Parametern alles Mögliche sein, beispielsweise eine feste Zahl oder ein Zeiger.

Rückgabetyp: Der Rückgabetyp ist der Datentyp des Rückgabewertes. Er steht immer vor dem Funktionsnamen. Wenn man z.B. eine Funktion für die Kreisflächenberechnung hat, so sollte der Rückgabetyp float oder double sein, da die Fläche sicherlich eine Fließkommazahl ist.
Der Rückgabewert heißt void, falls die Funktion keinen Rückgabewert hat, also die return-Anweisung fehlt. Wenn also die Funktion nichts zurückgeben soll, dann den Datentyp auf void setzen und die return-Anweisung weglassen.

Parameterliste: Damit sind alle Parameter gemeint, also alles was zwischen den runden Klammern steht.

Funktionskopf: Als Funktionskopf wird der Funktionsteil bis zur geöffneter geschweifter Klammer { bezeichnet, also der Rückgabetyp, der Funktionsname und die Parameterliste.

Funktionsrumpf: Das ist der Anweisungsblock der Funktion. Er befindet sich zwischen den geschweiften Klammern.

Die nachfolgende Grafik fasst alle Begriffe anhand einer Funktion zur Addition von zwei Ganzzahlen zusammen.

Funktionsaufbau

Funktionsaufbau

Schauen wir uns einige einfache Funktionsbeispiele an.

// eine Funktion die überhaupt nichts macht
void macheNichts(void)
{
}

// sagt Hallo
void sageHallo()
{
	std::cout << "Hallo" << endl;
}

// gibt das Ergebnis in der Konsole aus
void coutErgebnis(int ergebnis)
{
	std::cout << "Das Ergebnis lautet " << ergebnis << endl;
}

// addiert zwei Zahlen
int addiere(int a, int b)
{
	int ergebnis = a + b;
	return ergebnis;
	// oder auch alles zusammen in einer Zeile als:
	// return a+b;
}

// gibt die kleinere der beiden übergeben zahlen zurück
int kleinsteZahl(int a, int b)
{
	if(a < b)
		return a;
	else
		return b;

	// oder auch in Kurzschreibweise:
	// return (a < b) ? a : b;
}

Funktionsaufruf

Ein Funktionsaufruf ist nicht weiter schwierig und hat folgende Form.

Funktionsname([Argumentübergabe]);

Als Beispiel werden bereits oben definierte Funktionen aufgerufen.

macheNichts();

sageHallo();

coutErgebnis(5);

addiere(4, 2);

kleinsteZahl(24, 16);

Wichtig ist das bei einem Funktionsaufruf keine Datentypen angegeben werden, nicht der Rückgabetyp und auch nicht die Datentypen der Parameter. Das ist der entscheidende Unterschied zu einer Funktionsdeklaration.

Schauen wir uns ein komplettes Programm mit einem Funktionsaufruf an.

#include <iostream>

using namespace std;

// Funktion wird definiert
void sageHallo()
{
	std::cout << "Hallo" << endl;
}

int main(void)
{
	cout << "Beispiel: Ein Funktionsaufruf." << endl;

	// Funktion aufrufen
	sageHallo();

	cout << "Zum beenden Enter-Taste druecken" << endl;
	cin.get();
	
	return 0;
}

Das Programm startet, wie auch jedes andere Programm, mit der main-Funktion und geht dann alle Befehle der Reihe nach durch. Wird eine Funktion aufgerufen, so springt das Programm in ihren Anweisungsblock und führt dort alle Befehle der Reihe nach durch bis keine Anweisungen mehr vorhanden sind oder man auf eine return-Anweisung trifft. Dann springt das Programm zurück an die Stelle, an der die Funktion aufgerufen wurde und führt ganz normal die restlichen Befehle der Reihe nach durch.
Die Funktionsweise wird schematisch an dem oberen Beispiel erläutert.

Funktionsaufruf. Die Reihenfolge der Ausführung.

Funktionsaufruf. Die Reihenfolge der Ausführung.

Beim Funktionsaufruf muss der Funktionsname dem Compiler bereits bekannt sein, d.h. der Funktionsname muss in der Anweisungsreihenfolge vor dem Funktionsaufruf stehen. Um dies zu erreichen gibt es zwei Möglichkeiten. Entweder man definiert die Funktion oberhalb des Funktionsaufrufs (was gleichzeitig auch einer Deklaration entspricht) oder aber man schreibt nur die Deklaration oberhalb der des Funktionsaufrufs hin und die Definition selbst kann dann irgendwo in der Datei stehen (Näheres zur Dateiverwaltung im nächsten Teil des Tutorials).

Alle oberen Funktionsbeispiele sind Funktionsdefinitionen. Eine Funktionsdeklaration besteht nur aus dem Funktionskopf abgeschlossen mit einem Semikolon. Ein paar Varianten zum besseren Verständnis.

// Das Beispiel funktioniert

// Funktion wird definiert
void sageHallo()
{
	std::cout << "Hallo" << endl;
}

int main(void)
{
	// Funktion aufrufen
	sageHallo();

	cout << "Zum beenden Enter-Taste druecken" << endl;
	cin.get();
	
	return 0;
}
// das Beispiel funktioniert nicht

int main(void)
{
	// Funktion aufrufen
	 sageHallo();	// Fehler, Funktion nicht bekannt

	cout << "Zum beenden Enter-Taste druecken" << endl;
	cin.get();
	
	return 0;
}

// Funktion wird definiert
void sageHallo()
{
	std::cout << "Hallo" << endl;
}
// das Beispiel funktioniert

// Funktion wird deklariert und ist ab hier für den Compiler bekannt
void sageHallo();

int main(void)
{
	// Funktion aufrufen
	sageHallo();

	cout << "Zum beenden Enter-Taste druecken" << endl;
	cin.get();
	
	return 0;
}

// Funktion wird definiert
void sageHallo()
{
	std::cout << "Hallo" << endl;
}
Funktioniert das Beispiel?

// Funktion wird deklariert und ist ab hier für den Compiler bekannt
void sageHallo();

int main(void)
{
	// Funktion aufrufen
	sageHallo();

	cout << "Zum beenden Enter-Taste druecken" << endl;
	cin.get();
	
	return 0;
}

// Funktion wird definiert
int sageHallo()
{
	std::cout << "Hallo" << endl;
}

Man sollte also immer auf die Reihenfolge achten und auch auf die korrekte Übereinstimmung der Funktionsköpfe.

Wie bereits aus den oberen Beispielen ersichtlich werden Funktionen außerhalb der main-Funktion in dem globalen Block deklariert und definiert. Allgemein lässt sich in einem Funktionsblock keine Funktion definieren und dabei ist die main-Funktion keine Ausnahme.

Werteübergabe

Schauen wir uns an, wie Werte an Funktionen übergeben werden. Dies soll nur eine kurze Erläuterung sein, denn wir werden auf die Wertübergabe im Zusammenhang mit Zeigern noch ausführlich zu sprechen kommen.

Die Parameter in der Parameterliste einer Funktion sind ganz normale lokale Variablen, die nur in der Funktion selbst deklariert sind. Diese Variablen werden mit den Werten, die man an eine Funktion übergibt, auch Argumente genannt, initialisiert.

Nehmen wir eine Additionsfunktion als Beispiel.

void addiere(int a, int b)
{
	int ergebnis = a + b;

	cout << "a+b="<< ergebnis << endl;
}

Die Parameterliste besteht aus zwei Parametern int a und int b. Das sind lokale Variablen dieser Funktion. Wenn wir diese Funktion aufrufen, dann müssen wir ihr solche Werte übergeben, die wir auch sonst einer int-Variable zuweisen würden.

Ein Beispiel:

addiere(3,5);

Wenn die Funktion addiere aufgerufen wird, dann stehen in der Funktion Variablen a und b zur Verfügung, die mit den Werten 3 bzw. 5 initialisiert werden. Wir können mit diesen Variablen ganz normal arbeiten.
Aus diesem Grund führt auch folgender Code zu einem Compilerfehler, weil a bereits in der Funktion deklariert ist.

void addiere(int a, int b)
{
	int a; // Fehler, weil a bereits deklariert ist

	int ergebnis = a + b;
	cout << "a+b="<< ergebnis << endl;
}

Wie bereits erwähnt können wir an die Funktion auch Variablenwerte übergeben.

int zahl1 = -12;
int zahl2 = 35;

addiere(zahl1, zahl2);

Dies ist immer dann nötig, wenn das Argument erst zur Laufzeit bekannt ist. Beispielsweise, wenn der Benutzer irgendeinen Wert eingeben soll, der dann für eine Berechnung benötigt wird.

Es sei an diese Stelle anzumerken, dass die Änderung des Variablenwertes a in der Funktion keine Auswirkung auf den Wert von zahl1 hat.

Rückgabewert

Der Rückgabewert dient dazu einen Wert aus einer Funktion zu erhalten.

Nehmen wir eine Funktion zur Berechnung der Kreisfläche.

float kreisflaeche(float radius)
{
	return 3.14*radius*radius;
}

Die Funktion bekommt als Argument den Kreisradius, rechnet die Kreisfläche aus und liefert sie gleichzeitig als Rückgabewert zurück.
Um den Rückgabewert beim Aufruf abzufangen, wird die Funktion als eine Konstante benutzt (wir haben es in einem früheren Artikelteil als R-Wert bezeichnet).

float ergebnis = kreisflaeche(5);

Die Funktion float kreisflaeche(int radius) wird mit dem Argument 5 aufgerufen und der Rückgabewert der Funktion wird in der Variable ergebnis abgelegt.

Da der Funktionswert als eine Konstante aufgefasst werden kann, so können wir damit auch Rechnen.

// Fläche von zwei unterschiedlichen Kreisen
float ergebnis = kreisflaeche(5) + kreisflaeche(8);

// Fläche von zwei gleichen Kreisen
float ergebnis = 3* kreisflaeche(8);

// Fläche sofort auf dem Bildschirm ausgeben
std::cout << "Kreisflaeche: " << kreisflaeche(4) << endl;

Was natürlich aber nicht geht, ist einer Konstante einen Wert zuzuweisen. Deswegen ist auch der folgende Ausdruck ungültig:

// Fehler: Linker Operand muss ein L-Wert sein.
kreisflaeche(3) = 2;

Dies waren die nötigsten Grundlagen um weitere Konzepte rund um Funktionen zu erlernen. Bevor es aber im nächsten Teil weitergeht, schauen wir uns noch ein größeres Beispiel an, um das Erlernte besser zu verstehen.

Ein Beispiel

Betrachten wir ein einfaches Programm zur Berechnung der Dreiecksfläche.

#include <iostream>
using namespace std;

int main(void)
{
	// Länge der Grundseit und Höhe des Dreiecks
	float grundseite = 0.0f;
	float hoehe = 0.0f;
	
	cout << "Dreiecksflaechenberechnung." << endl;

	// Grundseite eingeben (wir verzichten hier auf die Überprüfung der Eingabe)
	cout << "Geben Sie die Laenge der Grundseite ein." << endl;
	cin >> grundseite;

	// Grundseite eingeben (wir verzichten hier auf die Überprüfung der Eingabe)
	cout << "Geben Sie die Hoehe ein." << endl;
	cin >> hoehe;
	
	float flaeche = (grundseite * hoehe) / 2;

	cout << "Die Flaeche des Dreiecks betraegt: " << flaeche << endl;

	cout << "Zum beenden Enter-Taste druecken" << endl;
	cin.get();
	
	return 0;
}

Die Aufforderung zur Eingabe der Grundseite und der Höhe sieht in beiden Fällen fast identisch aus. Wir wollen dies vermeiden und lagern den Code aus der Main-Funktion in eine andere Funktion aus.


// Fordert den Benutzer auf, eine Fließkommazahl-Eingabe zu machen.
// Gibt die Eingabe zurück.
float floatEingabe(const char* text)
{
	float eingabe;

	cout << "Geben Sie " << text <<" ein." << endl;
	cin >> eingabe;

	return eingabe;
}

Die Funktion floatEingabe erwartet eine Zeichenkette, die die Bezeichnung für die Größe, die eingegeben soll, enthält. Die Eingabeaufforderung wird ausgegeben, der Benutzer gibt eine Zahl ein und die Funktion gibt diese Zahl zurück.

Wir verwenden diese Funktion um das ursprüngliche Programm zu vereinfachen.

int main(void)
{
	cout << "Dreiecksflaechenberechnung." << endl;

	// Länge der Grundseit und Höhe des Dreiecks
	float grundseite = floatEingabe("die Laenge der Grundseite");
	float hoehe = floatEingabe("die Hoehe");
	
	float flaeche = (grundseite * hoehe) / 2;

	cout << "Die Flaeche des Dreiecks betraegt: " << flaeche << endl;

	cout << "Zum beenden Enter-Taste druecken" << endl;
	cin.get();
	
	return 0;
}

Das Programm erscheint sofort etwas übersichtlicher und die Programmlogik ist deutlicher.
Müsste man zum Beispiel nicht nur zwei sondern zehn Werteeingaben machen, so kann man sich leicht vorstellen, dass bereits solch eine kleine Funktion einiges an Schreibarbeit erspart, aber nicht nur das. Möchte man fehlerhafte Eingaben abfangen und den Benutzer auffordern die Eingabe zu wiederholen, so muss den Code nur einmal in der Funktion abändern werden und nicht bei jeder Eingabe, wie es in dem ursprünglichen Beispiel der Fall wäre.

Oder ein anderes Beispiel. Man hat in einer Anweisungsblock einen Fehler entdeckt und muss jetzt diesen überall dort korrigieren, wo dieser Anweisungsblock in dem Programm verwendet wurde. Diese Prozedur ist fehleranfällig und kostet nur unnötige Zeit. Benutzt man aber eine Funktion, so muss der Fehler nur an einer einzigen Stelle korrigiert werden.

Wie man sieht, arbeitet man mit Funktionen nicht nur effizienter, sondern verhindert auch Programmfehler.

Rein aus demonstrativen Gründen möchte ich noch zeigen, dass man das obere Beispiel noch kürzer schreiben kann.

int main(void)
{
	cout << "Dreiecksflaechenberechnung." << endl;
	
	float flaeche = (floatEingabe("die Laenge der Grundseite") * floatEingabe("die Hoehe")) / 2;

	cout << "Die Flaeche des Dreiecks betraegt: " << flaeche << endl;

	cout << "Zum beenden Enter-Taste druecken" << endl;
	cin.get();
	
	return 0;
}
// noch etwas kürzer

int main(void)
{
	cout << "Dreiecksflaechenberechnung." << endl;

	cout << "Die Flaeche des Dreiecks betraegt: " 
		<< (floatEingabe("die Laenge der Grundseite") * floatEingabe("die Hoehe")) / 2 
		<< endl;

	cout << "Zum beenden Enter-Taste druecken" << endl;
	cin.get();
	
	return 0;
}

Man sollte mit der kurzen Schreibweise auch hier nicht übertreiben. Das Beispiel sollte nur verdeutlichen, dass Funktionsaufrufe sehr flexibel eingesetzt werden können.

Übungsaufgaben

  1. Was sind Funktionen und welchen Nutzen haben sie?
  2. Beschreiben Sie den Aufbau einer Funktion.
  3. Wo dürfen Funktionen deklariert werden?
  4. Wie greift man den Rückgabewert einer Funktion ab?
  5. Schreiben Sie eine Funktion, die als Argument eine Ganzzahl, eine Kommazahl und einen String annimmt und sie dann auf dem Bildschirm ausgibt.
  6. Schreiben Sie eine Funktion, die die Fläche eines Dreiecks berechnet. Überlegen Sie sich, welche Parameter und welchen Rückgabetyp sie haben soll. Die Funktion soll keine Bildschirmausgabe machen. Tipp: schauen Sie sich die Kreisflächenfunktion in einem Beispiel oben an.
  7. Erweitern Sie die Funktion float floatEingabe(const char* text) so, dass der Benutzer keine negativen Werte eingeben darf. Falls die Eingabe inkorrekt ist, soll sie wiederholt werden. Tipp: schauen Sie sich das Kapiter über Schleifen an.
    Überlegen Sie sich andere Schutzmechanismen gegen fehlerhafte Eingaben.
  8. Schreiben Sie ein Programm, das dem Benutzer erlaubt die Fläche von einem Kreis und einem Dreieck zu berechnen. Dazu soll der Benutzer eine Liste mit den Auswahlmöglichkeiten (z.B. 1 für Kreis, 2 für Dreieck) bekommen. Die dafür benötigten Funktionen können Sie aus dem Artikel und der Aufgabe 6 entnehmen (es sollten mindestens drei sein).
  9. Erweitern Sie das Programm aus der Aufgabe 8 so, dass die Fläche mindestens einer weiteren geometrischen Form berechnet werden kann.
  10. Schwierig, aber sehr lehrreich: Schreiben Sie ein „Drei gewinnt“-Programm. Die beiden Spieler sollten die Eingaben nacheinander machen. Die einzelnen Schritte können nacheinander auf dem Bildschirm ausgegeben werden. Überlegen Sie sich wie die Eingabe aussehen könnte und welche Funktionen man braucht. Arbeiten Sie zuerst mit einen Schreibblock und einen Bleistift. Viel Erfolg!




Trackback URI | Kommentare als RSS

Einen Kommentar schreiben

XHTML: Du kannst folgende Tags verwenden: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <sub> <sup>

Hinweis: Ich behalte mir das Recht vor solche Kommentare, die Beleidigungen oder rechtswidrige Inhalte beinhalten erst nach einer Editierung freizugeben oder kommentarlos zu löschen. Ähnliches gilt auch für Kommentare die offensichtlich nur der Suchmaschinenoptimierung dienen.