C++: Die Basics - eps1.15_Funktionen
Hallo und herzlich willkommen. Bisher haben wir in der Beitragsserie C++: Die Basics einfache Programme gesehen oder selbst entwickelt. Sie bestanden aus einer main()
Funktion, in der sich alles abgespielt hat.
Das funktioniert für kleine und nicht allzu umfangreiche Programme ganz gut. Doch je größer und komplexer unser Code wird, desto länger wird der Inhalt von main()
.
Hast du nicht auch das Gefühl, mit wachsender Codelänge den Überblick zu verlieren? Es wird immer anstrengender, bestimmte Teile zu finden oder Zusammenhänge zu sehen.
Die Lösung: Strukturiere deinen Quellcode mit Hilfe von weiteren Funktionen!
Was sind Funktionen?
Du kennst bereits die main()
Funktion. Sie ist der Startpunkt und somit essentiell für jedes Programm. Dort muss alles rein, was deine Anwendung ausführen soll. Doch du musst nicht jede Anweisung, Verzweigung und Schleife zeilenweise untereinander dort hineinschreiben.
Mit zusätzlichen Funktionen ist es dir möglich, deinen Code in kleinere, logische Blöcke aufzuteilen und zu organisieren.
Du kannst Funktionen als Unterprogramme verstehen. Sie haben die Fähigkeiten, Parameter entgegenzunehmen, eigenständig eine Aufgabe zu erfüllen und einen Wert zurückzuliefern.
Mit einem Beispiel erkennst du die Notwendigkeit der Untergliederung in Funktionen sicherlich besser. Dazu nehmen wir ein Thema aus der Geometrie (der schon wieder mit seinem Mathe…).
Wir wollen den Durchmesser, die Fläche und den Umfang eines Kreises mit beliebigem Radius von unserer Anwendung berechnen lassen. Dazu müssen wir die Kreiszahl Pi
und die benötigten mathematischen Formeln implementieren.
// listing1: computing the area and circumference of a circle with given radius
#include <iostream>
const double Pi = 3.14159265;
// functions
double diameter(double radius)
{
return 2 * radius;
}
double area(double radius)
{
return Pi * radius * radius;
}
double circumference(double radius)
{
return 2 * Pi * radius;
};
// main function
int main()
{
double radius = 0.0;
std::cout << "Enter radius: ";
std::cin >> radius;
// call function "diameter"
std::cout << "diameter is: " << diameter(radius) << std::endl;
// call function "area"
std::cout << "area is: " << area(radius) << std::endl;
// call function "circumference"
std::cout << "circumference is: " << circumference(radius) << std::endl;
return 0;
}
Unsere main()
Funktion ist in listing1 schön schlank und delegiert alle Aufgaben an andere Funktionen weiter. Der Quellcode ist damit nicht nur viel übersichtlicher, wir haben ihn zusätzlich auch Teile wiederverwendbar gemacht. Die einzelnen Funktionen können, so oft du es für sinnvoll hältst, nochmals aufgerufen werden.
Dir ist sicherlich aufgefallen, dass die Konstanten und die Funktionen vor der main()
Funktion stehen. Das ist keinesfalls wahllos oder zufällig. Wenn du eine Funktion in einer anderen Funktion verwenden möchtest, dann muss diese vorher dem Compiler bekannt sein. Trifft er während des Build Prozesses auf eine Funktion, die er nicht kennt, so schmeißt er eine Fehlermeldung und bricht den Vorgang ab. Dies gilt auch für Variablen und Konstanten.
Aber du musst nicht alles vollständig vor die main()
Funktion setzen. Kannst du dich noch an den Unterschied zwischen Deklaration und Definition erinnern?
Von Prototyp, Definition, Function Call und Argumenten
Lass uns kurz überlegen, was wir alles für die Verwendung von Funktionen benötigen.
Damit dem Compiler die Funktion bekannt ist und eingesetzt werden kann, müssen wir diese zunächst einmal deklarieren. Was sie eigentlich macht, ist dann aber noch nicht klar. Dazu benötigen wir ihre Definition. Einsetzen können wir sie schließlich mit einem Funktionsaufruf.
Daraus ergeben sich die drei Bestandteile:
- Declaration
- Definition
- Function Call
Die Deklaration einer Funktion wird auch Funktionsprototyp genannt. Ein Prototyp besteht aus dem Return value type, Funktionsnamen und darauf folgen in Klammern ()
die Funktionsparameter. Der Prototyp wird mit einem Semikolon ;
abgeschlossen (siehe listing2).
// listing2: function prototype
// |Return value |Function parameters
double diameter (double radius);
// |Function name
Ein Prototyp zeigt also dem Compiler den Namen der Funktion, welche Parameter sie akzeptiert und welchen Datentyp sie zurückgeben wird. Erst jetzt kann er mit ihr in einer Anweisung etwas anfangen.
Das reicht dem Compiler vollkommen. Mehr muss er nicht wissen. Die Verbindung zwischen dem Aufruf und der Implementierung einer Funktion übernimmt anschließend der Linker.
Deshalb kann die Definition auch an einer beliebig anderen Stelle im Quellcode stehen. Sie ist die eigentliche Implementierung der Funktion und besteht immer aus einem Anweisungsblock in gescheiften Klammern {}
(siehe listing3).
// listing3: function definition
double diameter (double radius)
{
return 2 * radius;
}
Solange eine Funktion nicht den Return-Typ void
hat, muss es eine return
Anweisung geben. In listing3 hat die Funktion double
als Rückgabewert. Also wird von return
auch eine double
Variable erwartet.
Nach dem Funktionsnamen folgt in Klammern ()
der Funktionsparameter radius
. Dieser Parameter wird als Argument
beim Funktionsaufruf genutzt. Argumente sind demnach die Werte, die eine Funktion in ihrer Parameterliste beim Aufruf fordert.
// listing4: function call
diameter (3.0);
Der Funktionsaufruf ist ganz simpel. Du brauchst nur den Funktionsnamen zu schreiben und daran anschließend in Klammern ()
das geforderte Argument mitgeben. Natürlich folgt auch hier am Ende ein Semikolon ;
.
Würde unser Beispiel aus listing1 mit dem neuen Wissen anders aussehen? Ganz klar ja!
// listing5: function prototypes and implementations
#include <iostream>
const double Pi = 3.14159265;
// prototypes: function declarations
double diameter(double radius);
double area(double radius);
double circumference(double radius);
int main()
{
double radius = 0.0;
std::cout << "Enter radius: ";
std::cin >> radius;
// call function "diameter"
std::cout << "diameter is: " << diameter(radius) << std::endl;
// call function "area"
std::cout << "area is: " << area(radius) << std::endl;
// call function "circumference"
std::cout << "circumference is: " << circumference(radius) << std::endl;
return 0;
}
// implementation: function definitions
double diameter(double radius)
{
return 2 * radius;
}
double area(double radius)
{
return Pi * radius * radius;
}
double circumference(double radius)
{
return 2 * Pi * radius;
}
Wir würden erst die Prototypen deklarieren und darauf sofort die main()
Funktion folgen lassen. Um den Code gut lesen zu können, wollen wir, genau wie der Compiler, zunächst nur wissen, welche Funktionen es gibt und was ihre Parameter sind.
Denn eigentlich interessiert uns, was das Programm am Ende ausgibt. Tiefere Details über die Umsetzung der einzelnen Funktionen sind dazu im ersten Schritt noch nicht wichtig und erschweren den Überblick. Deshalb können die Definitionen gerne nach der main()
Funktion stehen.
Wenn du jetzt damit argumentierst, dass der Quellcode doch nur zur Kommunikation mit der Maschine dient und dieser eine übersichtlichere Strukturierung egal ist, dann ist das nur die halbe Wahrheit.
Die Programmiersprache und dein Code dienen als Schnittstelle zwischen Mensch und Maschine.
Natürlich muss dein Code für den Compiler in Maschinecode übersetzbar sein. Doch auch andere Menschen sollen verstehen können, was du mit deinen Zeilen erreichen möchtest. Ich weiß, das macht die Aufgabe noch schwerer, da du zwei total verschiedene Zielgruppen mit deinem Code erreichen musst. Aber genau das ist auch die große Kunst am Programmieren.
Funktionen mit mehreren Parametern
Bisher haben unsere Funktionen ein Argument haben wollen und dir einen Wert zurückgeliefert. Doch es gibt Aufgaben, die mehr als einen Parameter benötigen. Bleiben wir bei der Geometrie. Was, wenn wir statt einer Fläche die Oberfläche eines dreidimensionalen Körpers berechnen lassen wollen?
Zum Beispiel die Oberfläche eines Zylinders. Diese besteht aus dem kreisförmigen Deckel, dem kresiförmigen Boden und der Fläche der Seite.
Eine Funktion, die uns die Oberfläche eines Zylinders berechnet, braucht demnach die zwei Parameter double radius
und double height
. Und das geht ganz einfach. Zusätzliche Parameter werden im Protoypen einfach mit in die Klammer ()
geschrieben. Jeweils mit einem Komma ,
getrennt.
// listing6: function with multiple parameters
// prototype: function declaration
double cylinderSurface(double radius, double height);
// implementation: function definition
double cylinderSurface(double radius, double height)
{
return 2 * radius * radius + 2 * radius * radius + 2 * radius * height;
}
Mmh, die Formel in der Definition ist ziemlich lang und lässt sich schlecht überblicken. Wir könnten doch die Funktionen aus unseren anderen Beispielen wiederverwenden und den Code modularer gestalten. Denn wenn du genau hinschaust, dann besteht die Oberfläche eines Zylinders aus zwei Kreisflächen und dem Kreisumfang multipliziert mit der Höhe des Körpers.
// listing7: surface of a cylinder
#include <iostream>
const double Pi = 3.14159265;
// prototypes: function declarations
double area(double radius);
double circumference(double radius);
double cylinderSurface(double radius, double height);
int main()
{
double radius = 0.0;
double height = 0.0;
std::cout << "Enter radius: ";
std::cin >> radius;
std::cout << "Enter height: ";
std::cin >> height;
// call function "cylinderSurface"
std::cout << "The surface of the cylinder is: " << cylinderSurface(radius, height) << std::endl;
return 0;
}
// implementation: function definition
double area(double radius)
{
return Pi * radius * radius;
}
double circumference(double radius)
{
return 2 * Pi * radius;
}
double cylinderSurface(double radius, double height)
{
// call functions "area" and "circumference"
return 2 * area(radius) + circumference(radius) * height;
}
Weder Parameter, noch Return Value
Es gibt auch den Fall, dass eine Funktion keine Parameter hat und auch keinen Wert zurückgibt. Ist dir der Dialog unseres Programms zu unfreundlich oder zu kalt, dann möchtest du vielleicht zu Beginn eine Funktion aufrufen, die den Nutzer begrüßt.
Mehr soll die Funktion nicht können. Sie tut nur diese eine Ausgabe auf die Kommandozeile.
// listing8: function with no parameters and no retrun values
#include <iostream>
// prototype: function declaration
void welcome();
int main()
{
welcome();
return 0;
}
// implementation: function definition
void welcome()
{
std::cout << "Welcome!" << std::endl;
}
Der Datentyp void
zeigt an, dass nichts zurückgegeben werden soll. Deshalb wird in der Definition auch keine return
Anweisung verlangt. Es ist dir aber freigestellt, ob du die Anweisung weglässt oder symbolisch eine leere return
Anweisung schreibst.
// listing9: empty return statement
#include <iostream>
void welcome()
{
std::cout << "Welcome!" << std::endl;
return; // empty return statement
}
Funktionsparameter mit Default Value
Eine weitere Sache zu Funktionen möchte ich dir gerne noch zeigen.
“Einen noch… dann ist aber Schluss!” - Frau Hansen
In unserem Beispiel haben wir den Wert von Pi
als eine Konstante fest vorgegeben. Vielleicht möchtest aber du dem Nutzer die Chance geben, selbst zu wählen, mit welcher Genauigkeit und wie vielen Nachkommastellen er gerne Pi
verwenden möchte. Alles kein Problem. Wir können Funktionen mit zwei Parametern erstellen.
Doch was, wenn der Nutzer mit unserem Pi
einverstanden ist oder gar keine Ahnung hat, welchen Wert er eingeben soll?
Die Lösung ist nicht kompliziert. Wir können jedem Parameter einer Funktion im Prototypen einen Default Value geben. Für Pi
in unserer Kreisflächenfunktion area
sieht das folgendermaßen aus:
//listing10: function parameter default value
double area(double radius, double Pi = 3.14159265);
Dadurch, dass der zweite Parameter einen Default Value besitzt, ist er optional und muss nicht beim Funktionsaufruf mit einem Argument besetzt werden.
// listing11: function parameter with default value
#include <iostream>
// prototype: function declarations
double area(double radius, double Pi = 3.14159265);
int main()
{
double radius = 0.0;
std::cout << "Enter radius: ";
std::cin >> radius;
std::cout << "Do you want to have a different value of pi? (y/n) ";
char charPi;
std::cin >> charPi;
if( charPi == 'y')
{
std::cout << "Enter new Pi value: ";
double newPi;
std::cin >> newPi;
// call function "area with second argument"
std::cout << "The area is: " << area(radius, newPi) << std::endl;
}
else
{
// call function "area without second argument"
std::cout << "The area is: " << area(radius) << std::endl;
}
return 0;
}
// implementation: function definition
double area(double radius, double Pi)
{
return Pi * radius * radius;
}
Ob listing11 das beste und schönste Quellcodebeispiel ist, das soll nicht diskutiert werden. Aber ich denke, du siehst daran, wie Default Values funktionieren.
Das bleibt hängen
Wir haben Funktionen kennengelernt und gesehen, wie wir damit unseren Code modularisieren. Dadurch wurden nicht nur die Lesbarkeit und die Übersicht erhöht, sondern auch Teile des Quellcodes wiederverwendbar gemacht.
Du kannst nun Prototypen, Definitionen und Funktion Aufrufe erstellen. Dabei hast du völlig Gestaltungsfrei, wenn es um den Return Value oder die Parameter geht.
Funktionen sind ein mächtiges Werkzeug, mit dem du größere Programme geschickter und übersichtlicher umsetzen kannst. Definitiv eine Technik, die es sich zu meistern lohnt.
Ich wünsche dir maximalen Erfolg!
Quellen
- [1] B. Stroustrup, A Tour of C++. Pearson Education, 2. Auflage, 29. Juni 2018.
- [2] B. Stroustrup, Programming: Principles and Practice Using C++. Addison Wesley, 2. Auflage, 15. Mai 2014.
- [3] U. Breymann, Der C++ Programmierer. C++ lernen – professionell anwenden – Lösungen nutzen. Aktuell zu C++17. München: Carl Hanser Verlag GmbH & Co. KG; 5. Auflage, 6. November 2017