Strukturierte Programmierung und Funktionen Um ein Programm übersichtlicher zu gestalten, zerlegt man seine Funktionalität meist in mehrere (oder auch ganz viele) Teile. Dadurch lässt sich das Programm übersichtlicher darstellen und manchmal lassen sich die Einzelteile auch in anderen Programmen sinnvoll einsetzen(wiederverwertbarer Code). C besitzt für diese Aufteilung nur die Funktion, während objektorientierte Sprachen auch Objekte (Klassen) dafür einsetzen können. Wichtig: C (und C++)-Programme bestehen aus einer oder mehrerer Funktionen. Es muss mindestens die main()-funktion vorhanden sein. Mit dieser startet und endet das Programm im Normalfall. Der Programm-Code kann auch auf mehrere Quellcodedateien aufgeteilt werden, die aber vollständig sein sollten (d.h. jede sollte sich auch alleine ohne die übrigen übersetzen lassen). Eine Funktion wird nur ausgeführt, wenn sie im Programmcode aufgerufen wird (die einzige Ausnahme ist die main()-funktion, die automatisch beim Programmstart vom Betriebssystem aufgerufen wird). Ist der Code einer Funktion abgearbeitet, springt die Programmausführung zurück zum Aufrufer und setzt dort fort. double quadrat(double x) return x*x; // die Funktion wird definiert // hier wird das Ergebnis der Funktion festgelegt... quadrat; // kein Aufruf der Funktion: Klammern fehlen quadrat(1.0); // Aufruf der Funktion: Ergebnis nicht verwendet double u = quadrat(4.0); // 1 Aufruf der Funktion quadrat() double v = quadrat(u) + quadrat(3.0) // 2 Aufrufe der Funktion Funktionsargumente und Rückgabewert Viele Funktionen brauchen Eingabewerte vom Aufrufer, aus denen sie das Resultat berechnen. Es gibt daher einen Übergabe-Mechanismus, der es dem Aufrufer erlaubt, beliebig viele Daten an die Funktion zu übergeben (= Funktionsargumente) und der es der Funktion gestattet, das Ergebnis (es ist nur 1 Wert gestattet!) an den Aufrufer zurückzugeben (= Funktions-Rückgabewert). Sowohl die Funktion als auch der/die Aufrufer definieren die Funktionsargumente. Die Funktion definiert dabei die sog. formalen Funktionsargumente (oben: double x), die innerhalb der Funktion wie lokale Objekte benützt werden dürfen, und den Rückgabewert-Typ (oben double). Der Aufrufer spezifiziert im Aufruf die aktuellen Funktionsargumente (oben bei den 3 Aufrufen: 4.0, u, 3.0). C und C++ sind stark typisierte Sprachen, d.h. der Compiler überwacht
genauestens, ob die Anzahl der formalen Argumente mit der Anzahl der aktuellen Argumente übereinstimmt (oben: jedes Mal genau 1 Argument) und ob die Typen der formalen Argumente mit den Typen der aktuellen Argumente übereinstimmen oder zumindest kompatibel sind (oben: alle sind double).... int i = quadrat(1) + quadrat('a'); Bei diesem Aufruf sieht man mehrere Typ-Ungleichheiten: der erste Aufruf übergibt den int 1, der 2. den char 'a' als aktuelles Argument (statt double), der Compiler wandelt beide Argumente stillschweigend in double um, d.h. er führt aus: int i = quadrat(1.0) + quadrat(97.); // 'a' ist 97 Die rechte Seite ist vom Typ double (,da das der Rückgabewert-Typ der Funktionsaufrufe und der Addition ist). Dieser Wert wird sodann in einen int konvertiert und die Variable i damit initialisiert. Syntax der Funktionsdefinition Ergebnistyp Funktionsname(formales Argument 1, f. Argument 2,...) Anweisung 1; // Anweisungen enden mit Strichpunkt Anweisung 2;... // kein Strichpunkt! Jedes formale Funktionsargument besteht aus einer Typangabe und einem Namen (und in C++ optional einem Defaultwert). Die Funktionsdefinition bestimmt sowohl die Signatur der Funktion (Ergebnistyp, Name, formale Argumenttypen, Zusätze wie noexcept, constexpr etc.) als auch deren Code. Die Namen der Argumente gehören nicht zur Signatur (nur deren Typ). Beispiel: constexpr int summe(int a, int b) noexcept return a+b; Ergebnistyp: int
Name der Funktion: summe 1. Argument: int a, kein Defaultwert 2. Argument: int b, kein Defaultwert Zusätze: constexpr (Funktionsergebnis darf u.u. schon vom Compiler berechnet werden) noexcept (Funktion erzeugt keine Exceptions = Laufzeitfehler) Signatur: constexpr int summe(int, int) noexcept Der Compiler prüft beim Aufruf von Funktionen (d.h. der Name der Funktion gefolgt vom Aufruf-Operator () steht irgendwo im Programm), ob die richtige Anzahl von aktuellen Argumenten und die richtigen Typen vorhanden sind: summe(1) // Fehler: nur 1 aktuelles Argument vorhanden summe(1, 2) // korrekt: das Ergebnis ist 3 (1+2) // der Compiler wird statt des Funktionsaufrufs // dessen Ergebnis 3 verwenden (constexpr) summe(1.5, 2.1) // C/C++: korrekt: Wert ist 3 (1+2), Java: Fehler! summe("1", 2) // Fehler oder Warnung: das 1. Argument ist ein String // das Ergebnis ist nicht vorhersagbar!! Man sieht hier, dass C (und C++, Java) beim Programmaufruf automatisch einige Typumwandlungen vornehmen können, wenn solche definiert sind (z.b. double -> int in C/C++ aber nicht in Java). Es wird immer das aktuelle Argument beim Aufruf in den Typ des formalen Arguments der Funktions-Definition gewandelt. Manchmal gibt es trotzdem eine Compilerwarnung dafür. Falls es keine (gute) Typumwandlung gibt (wie hier Zeichenkette -> int beim letzten Beispiel) erfolgt eine Fehlermeldung oder auch nur eine Warnung. Die formalen Argumente der Funktionsdefinition (hier a,b) stehen innerhalb der Funktion wie lokale Variable zur Verfügung. Ihre Anfangswerte erhalten sie vom Aufrufer der Funktion (entweder als Kopie oder als Referenz etc.!). Der Rückgabetyp auto ist seit C++14 für Funktionen erlaubt. C++ bestimmt in diesem Fall den Ergebnistyp aus der (den) return-anweisung(en). constexpr auto summe(int a, int b) noexcept return a+b;
Syntax des Funktionsaufrufs Funktionsname(aktuelles Argument 1, aktuelles Argument 2,...) Insbesondere müssen die Klammern geschrieben werden, auch wenn keine Argumente vorhanden sind! sin(x) sin() sin // Aufruf der Funktion sin mit dem aktuellen Argument x // Aufruf der Funktion sin ohne Argument: Fehler // KEIN Aufruf der Funktion, sondern das Funktionsobjekt Verwendung von void C/C++/Java verfügen nicht, wie etwa Pascal, über sogenannte Routinen oder Prozeduren (Unterprogramme ohne Ergebnis) sondern nur über Funktionen, die immer ein Ergebnis und deshalb einen Rückgabetyp haben müssen! Aus Kompatibilitätsgründen wäre früher ein fehlender Rückgabetyp als int interpretiert worden, moderne C++-Versionen gestatten solche Konstruktionen nicht mehr. Um nichts zurückzugeben, existiert seit einiger Zeit der Datentyp void. Ein expliziter Rückgabetyp void besagt, dass die Funktion kein Ergebnis besitzt. In diesem Fall darf sie im (in den ) return-statements auch keinen Rückgabewert spezifizieren. Hat die Funktion auch keine Aufruf-Parameter, so sollte man in C (nicht nötig in C++ oder Java, aber erlaubt) auch hier explizit void in die Klammern schreiben, weil aus Kompatibilitätsgründen eine leere Liste für C nicht bedeutet, dass keine Argumente erlaubt sind. Für C++ und Java gilt: Hat die Funktion in der Definition keine Argumente, darf sie auch nur so aufgerufen werden. void f() // diese Funktion darf in C mit Argumenten aufgerufen // werden, hat kein Resultat // darf in C++ und Java nicht mit Argumenten aufgerufen werden void f(void) // die Funktion darf KEINE Aufrufargumente haben, hat kein Resultat Die return-anweisung
Mit der return-anweisung wird eine Funktion beendet. Folgt dem Schlüsselwort return ein Ausdruck, legt dieser den Rückgabewert fest. Nur auf diese Art können Rückgabewerte für Funktionen vereinbart werden. Eine Funktion kann mehrere return-statements haben (die aber dann meistens in if-blöcken stehen). Die erste ausgeführte return- Anweisung beendet die Funktion (und vereinbart, wenn vorhanden, das Ergebnis). Falls nötig, wird auch eine automatische Typkonvertierung des Ausdrucks in den Rückgabewert- Typ der Funktion gemacht. Auch mit der schließenden geschwungenen Klammer endet die Funktion, es wird hier KEIN Rückgabewert vereinbart. Alle return-anweisungen in einer Funktion sollten natürlich konsistent sein, d.h. alle sollten alle einen Rückgabewert vereinbaren oder keinen. Ist der Ergebnistyp nicht void, sollte zudem als letzte Anweisung vor der schließenden Klammer eine return-anweisung mit einem passenden Rückgabewert sein (verpflichtend in Java!). Das ist sogar erforderlich, wenn die Programmlogik es unmöglich macht, an diese Stelle zu gelangen! Die Funktionsdeklaration Durch eine Funktionsdeklaration wird nur die Signatur der Funktion bekannt gemacht, nicht aber ihr Code. Sie endet mit einem Strichpunkt ohne die Klammern. Man darf/sollte die Namen der Argumente weglassen, da der Compiler diese immer ignoriert. Ergebnistyp Funktionsname(Argument1, Argument 2,...); // Strichpunkt!! constexpr int summe(int, int) noexcept; // so ist es richtig constexpr int summe(int, int = 1) noexcept; // mit einem Defaultwert constexpr int summe(int = 5, int = 1) noexcept; // mit zwei Defaultwerten constexpr int summe(int a, int b) noexcept; // geht auch, gefällt mir aber nicht so gut void f(); // gut in C++, weniger gut in C s.o. Funktionsdeklarationen benötigt man oft, wenn das Programm aus mehreren Quellcode- Dateien bestehen soll (d.h. bei größeren Programmen). Wann immer ein Aufruf einer Funktion erfolgt, es muss vorher diese Funktion entweder definiert (also ihr ganzer Code wird programmiert) oder deklariert werden. Eine Funktion darf in einem Programm nur einmal (eigentlich muss sie genau einmal) definiert werden, kann aber beliebig oft konsistent deklariert werden. Sehr oft erfolgen diese Deklarationen in Headerdateien, die dann per #include eingefügt werden. Dies gilt insbesondere auch für die Bibliotheksfunktionen von C/C++:
std::cin ist deklariert in iostream (auch std::cout, << usw.) sin() ist deklariert in cmath (auch exp(),log() usw.) exit(), EXIT_SUCCESS ist deklariert in cstdlib Die main-funktion Die main()-funktion in C/C++ hat immer den Rückgabetyp int. Der Rückgabewert ist für das Betriebssystem ein Fehlercode, der aussagt, ob das Programm erfolgreich ohne Fehler ablaufen konnte oder ob es zu einem Fehler kam. Der Wert 0 steht für Erfolg und jeder andere Wert als Anzeichen für einen Fehler. Lesbarer wird das Programm aber durch die Verwendung der Makros EXIT_SUCCESS (ist logischerweise 0) und EXIT_FAILURE (ist 1) aus cstdlib Die main()-funktion in C/C++ hat entweder kein Argument oder aber meist genau 2 oder 3. Wir werden vorderhand nur die Version ohne Argument verwenden, somit lautet die Definition bis auf Weiteres: int main() // in C: int main(void)... return EXIT_SUCCESS; Jede Rückkehr aus der main()-funktion beendet das Programm. Überladen von Funktionen In C++ (aber nicht in C!) ist es erlaubt, denselben Funktionsnamen im gleichen Scope für verschiedene Funktionen zu verwenden. Diese müssen sich dann in der Argumentliste unterscheiden (entweder haben sie eine andere Anzahl von Argumenten oder wenigstens ein Argument hat einen unterschiedlichen Typ). Ein unterschiedlicher Rückgabetyp reicht nicht aus, natürlich auch nicht ein anderer Name eines formalen Arguments: Erlaubt sind z.b. folgenden Paare von Funktionen: void read(int& a) void read(double& x) // jeweils 1 Argument, aber unterschiedlicher Typ int f(int a) int f(int a, int b)
// 1 Argument vs. 2 Argumente int f(int a) int f(int& a) // jeweils 1 Argument, aber unterschiedlicher Typ!! Falsch: int f(int a) double f(int b) // jeweils 1 Argument mit gleichem Typ!! // Rückgabetyp und Name des Arguments werden NICHT verglichen Überladene Funktionen werden als absolut eigenständige Objekte behandelt, d.h. sie müssen keineswegs eine ähnliche Aufgabe haben (obwohl das meist der Fall ist). Beim Aufruf von überladenen Funktionen muss es für den Compiler eindeutig sein, welche der überladenen Funktionen aufgerufen werden soll, sonst ist es ein Fehler! 1) int f(int a, double b)... 2) int f(double a, int b)... 3) int f(double a, double b)... f(1, 1.0) f(1.0, 1) f(1.0, 2.0) f(1, 2) // Aufruf von 1), da exakte Übereinstimmung // Aufruf von 2), da exakte Übereinstimmung // Aufruf von 3), da exakte Übereinstimmung // Fehler: keine exakte Übereinstimmung, alle Varianten gleichwertig Findet der Compiler keine exakt passende Funktion mit diesem Namen, untersucht er auch alle Funktionen mit dem gleichen Namen, die er mit einer (oder mehrerer) erlaubten Typumwandlung(en) aufrufen kann. Gibt es hier mehr als eine Möglichkeit, ist das ein Fehler! Es ist dabei egal, wie viele Typumwandlungen für den Aufruf nötig wären: f(1, 2): könnte 1) aufrufen und 2 in 2.0 umwandeln könnte 2) aufrufen und 1 in 1.0 umwandeln könnte 3) aufrufen und 1 in 1.0 und 2 in 2.0 umwandeln Das Überladen von Funktionen in der Standardbibliothek C++ macht von diesen Möglichkeiten auch in den Standardbibliotheken (z.b. <cmath>) regen Gebrauch, während C hier passen muss: Alle Funktionen der Mathematikbibliothek werden in C++ für die Typen float, double und long double extra implementiert: float x;
sin(x); double a; std::abs(a); // -> float sin(float) // -> double abs(double) Aber Achtung: Auch in <cstdlib> ist eine abs()-funktionen definiert, nämlich: long abs(long). Diese Funktion sollte man nicht aufrufen, wenn das Argument ein Gleitkommatyp ist. Daher IMMER #include <cmath> verwenden, wenn man die abs() Funktion für Gleitkommazahlen (float, double) braucht. C muss für die unterschiedlichen Varianten unterschiedliche Namen definieren: int abs(int); long labs(long); double fabs(double);