C++/Tutorial: 5. Funktionen
Nun können wir bereits so ziemlich alle Algorithmen implementieren, Textprogramme ohne spezielles Wissen sollten für uns also kein Problem mehr darstellen.
Es sollte auch möglich sein ein textbasierendes Spiel zu entwickeln, nur wenn man sich jetzt vorstellt mit dem bisherigem Wissen und ein paar Befehlen zur Grafikanzeige Crysis zu entwickeln wird man sich schnell der Unmöglichkeit der Aufgabe bewusst. Aus diesem Grund beschäftigen sich Programmiersprachen hauptsächlich damit die Komplexität der Aufgabe zu kompensieren, wartbar, flexibel zu sein und sich dabei auch noch schnell zu entwickeln.
Ein früher Ansatz war die prozedurale Progammierung zu dessen bekanntesten Vertretern C und PASCAL gehören. Die heute noch meist verbreitete Objektorientierte Programmierung baut im Prinzip darauf auf, schauen wir uns also mal die Elemente dieser an.
Die verwendete Methode war es wiederkehrende Aufgaben zu eliminieren und zu einer sogenannten "Funktion" zusammenzufassen.
Solche Funktionen haben wir bereits exzessiv verwendet, denn niemanden hat es interessiert, dass cin.get() eigentlich jedes mal viele Dinge erledigt, wir kümmern uns nicht drum, wir verwenden die Funktion cin.get().
Die Vorteile von Funktionen lassen sich aus dem genannten Beispiel ableiten
- Lesbarkeit und damit Wartbarkeit: cin.get() ist deutlich klarer, als wenn wir jedes mal dutzende Befehle ausführen würden, die das gleiche bewirken.
- Wieder verwendbarkeit und damit Wartbarkeit und Codereduktion: Funktionen können mehrfach verwendet werden, dies führt offensichtlich zu weniger Code, allerdings hat es auch noch einen viel wichtigeren Nebeneffekt: Wenn wir etwas ändern wollen, müssen wir es nicht überall im Code tun(der ggf. nicht nur von uns geschrieben wurde), sondern müssen nur eine einzelne Funktion ändern.
Der Klarheit wegen, wieso heißt es prozedurale Programmierung und nicht funktionale Programmierung? In der Tat gibt es letzteres auch, doch ist dies etwas anderes und Funktionen im Sinne der prozeduralen Programmierung haben mehrere Namen: Funktion, Prozedur, Methode, Subroutine u.s.w.
Es gibt auch Unterscheidungen, so sehen manche(PASCAL-Programmierer z.B.) eine Prozedur als eine Funktion ohne Rückgabetyp(dazu später mehr) an und für manche ist eine Methode eine Memberfunktion. Eine klare Unterscheidung gibt es jedoch nicht, weswegen ich im folgendem auch keine klare Unterscheidung machen werde.
Zurück zum eigentlichen Thema. Wie ich bereits angewendet habe, verwenden wir Funktionen bereits exzessiv in verschiedensten Formen:
- cin.get(), cin.clear() etc. sind Memberfunktionen
- << und >> in Bezug auf Streams sind überladene Operatoren(ebenfalls Funktionen)
- numeric_limits<streamsize>::max ist eine Klassenfunktion
- Das umschließende cin.ignore ist ebenfalls eine Memberfunktion
- int main ist ein Sonderfall einer globalen Funktion
u.s.w.
Wie wir die verschiedenen Funktionen definieren werde ich im Laufe des Tutorials ansprechen, zunächst wollen wir uns aber erstmal mit den "normalen" globalen Funktionen beschäftigen.
Inhaltsverzeichnis
Eine einfache Funktion
Also, wie bereits erwähnt sollen Funktionen wiederkehrende Aufgaben übernehmen, beschäftigen wir uns einmal mit einer ziemlich primitiven Funktion, nennen wir sie "greet", greet soll den Benutzer begrüßen.
Dieser Ansatz an Erklärung sollte dem Benutzer der Funktion schon reichen, es hat für ihn keinen Nutzen zu wissen, wie greet den Benutzer grüßt, hauptsache die Funktion tut dies, dies hält den Programmierer davon ab das ganze Programm kennen zu müssen, was in Fällen größerer Software unmöglich ist(schon bei ein paar tausend Zeilen ist es unmöglich, die Software, die an die ich bei großer Software denke hat mehrere Millionen). Dass wir die Funktion benannt haben lässt schon durchscheinen, dass jede Funktion einen Identifier hat, mit dem man auf die Funktion verweisen kann. Ein Funktionsaufruf wäre also z.B.:
greet();
und das wars schon. greet(); ist ein vollständiger Funktionsaufruf, der Funktion mit dem Bezeichner "greet". Dass die Klammern nicht immer leer stehen, hat uns nicht zu kümmern, in C++ existiert für Funktionen, die keine Operatoren sind immer ein Klammerpaar.
Wann immer wir in unserem Code greet(); ausführen begrüßen wir also den Benutzer, so einfach ist das für den Anwender.
Natürlich müssen diese Funktionen zuvor implementiert worden sein, dem Compiler muss also gesagt werden was greet überhaupt tut, dazu werde ich erstmal ein bisschen weiter ausholen:
Man unterscheidet in C++ zwischen einer Funktionsdeklaration und einer Funktionsdefinition, denn in C++ sind Deklaration und Definition üblicherweise getrennt. Die Deklaration teilt dem Compiler für den folgenden Code mit, dass irgendwo eine Funktion, z.B. greet(); existiert und das reicht dem Compiler um den Code fehlerfrei zu übersetzen. Der Linker assoziiert die Funktion dann erst mit ihrer Definition, in der gesagt wird was die Funktion eigentlich tut. Notwendig ist diese Trennung nur, wenn die Funktion später im Code, oder in einer anderen Übersetzungseinheit(später mehr dazu) definiert wird.
Eine Funktionsdeklaration besteht aus einem Prototypen, der nur aus dem Funktionskopf besteht, also der Signatur der Funktion, gefolgt von einem Semikolon. Die Definition besteht aus dem Funktionskopf und dem Funktionskörper.
Der Funktionskopf gibt viele Informationen an, für uns relevant ist zur Zeit nur der Identifier, auf die anderen Elemente kommen wir später in diesem Tutorial zu sprechen.
Der Rohbau des Funktionskopfes wäre void identifier(), in unserem Fall also void greet(). Der Funktionskörper besteht schlicht aus einem Block.
Die Deklaration greets könnte folgendermaßen aussehen
void greet();
Die Definition hingegen so
void greet() { cout << "Guten Tag" << endl; }
Für uns neu ist allerdings wo diese Codestücke zu finden sind, denn sie befinden sich nicht mehr in main, welche ja selbst eine Funktion ist, sondern im globalen Scope(wo sich auch z.B. using namespace std; befindet), demnach sind auch Variablen aus main in den Funktionen nicht gültig, sie sind eben völlig unabhängig.
Nun, soviel zur Theorie, nun erst einmal ein voll funktionsfähiges Beispiel:
#include <iostream> using namespace std; void greet(); int main() { greet(); } void greet() { cout << "Guten Tag" << endl; }
Oder eine Alternative, in der die Definition ihre eigene Deklaration ist:
#include <iostream> using namespace std; //muss nun vor dem Aufruf zu finden sein void greet() { cout << "Guten Tag" << endl; } int main() { greet(); }
Parameter
Für simplen Code wie diesen mag es möglich sein, die Funktion ohne jegliche Zusatzinformationen aufzurufen, aber dies ist nicht immer der Fall.
Wenn wir zum Beispiel daran denken, dass greet etwas persönlicher sein sollte, wir also zunächst den Namen einlesen wollen und den Benutzer dann damit begrüßen stoßen wir schon auf ein Problem: greet muss den Namen kennen.
Man könnte nun auf die Idee kommen, den Namen in greet einlesen zu lassen, aber macht das Sinn? Der Benutzer wird bei greet nicht erwarten, dass gleichzeitig auch noch der Name eingelesen wird, also vielleicht ein read_name_and_greet()? Was aber wenn wir einmal grüßen wollen, ohne den Namen einzulesen, vielleicht eine bestimmte Person - daher macht es wenig Sinn den Namen in greet einzulesen. In der Tat ist es eine gute Richtlinie, wenn eine Funktion nur eine Aufgabe hat(wenn diese so primitiv wie greet ist, mag man keine Funktion brauchen, aber es handelt sich ja nur um ein Beispiel).
Wir wollen der Funktion also irgendwie den Namen mitteilen, egal wie wir nun an den Namen gekommen sind. Was wir benötigen ist eine Parameter. Ein Parameter lässt sich innerhalb der Funktion, wie eine Variable benutzen und wird der Funktion beim Aufruf als Wert(das "Argument", das natürlich auch eine Variable sein kann) "übergeben".
Wenn wir nun also die Funktion aufrufen, sollen wir sie folgendermaßen aufrufen können
greet("Herr Schmidt");
aber auch folgendermaßen
string name; cin >> name; greet(name);
Wie man sehen kann, werden die Parameter in dem Klammerpaar übergeben, ebenso sieht es auch bei der Deklaration aus:
void greet(string name);
string name ist hier der Parameter, das ganze ähnelt einer Variablendeklaration nicht nur, sondern gleicht ihr sogar. name ist innerhalb der Funktion eine ganz normale Variable, die mit dem übergebenem Argument initialisiert wird.
In der Implementation könnte das nun folgendermaßen aussehen
void greet(string name) { cout << "Guten Tag, " << name << endl; }
Wie man an der Formulierung erahnen kann, ist name eine neue Variable, der Übergabewert wurde kopiert und auch wenn es sich um eine Variable handelt, die übergeben wird, kann man name innerhalb der Funktion beliebig verändern, ohne dass es irgendeinen Einfluss auf den aufrufenden Code hat(dort wird nichts verändert!)
Und wie der bisher verwendete Plural schon vermuten lässt ist die Zahl der Parameter nicht auf 0 oder 1 beschränkt, in der Tat kann man beliebig viele Parameter definieren, diese werden dann mit Kommata getrennt. Es wäre z.B. möglich die Anzahl der bisherigen Log-Ins mit anzugeben:
void greet(string name, int number_of_logins) { cout << "Guten Tag, " << name << "dies ist ihr " << number_of_logins << "er Log-In" << endl; }
der Aufruf wäre entsprechend z.B.
greet("Herr Schmidt", 4);
Überladen von Funktionen
Was wenn wir nun beides wollen? Wenn wir nun eine Funktion greet wollen, die allgemein begrüßt und eine Funktion greet, die einen Parameter nimmt?
Sicher wäre es einfach möglich, indem wir einfach zwei verschiedene Namen wählen, aber noch einfacher ist es mit dem selben Namen, denn dies ist in der Tat möglich.
Zur Signatur einer Funktion gehört nämlich nicht nur der Identifier, sondern auch die Parameter. Wir können also problemlos 2 Funktionen mit dem selben Namen definieren, sofern sie unterschiedliche Parameterlisten haben. Wichtig ist, dass hierbei die Anzahl und die Typen der Parameter eine Rolle spielen, nicht aber deren Identifier(man kann diese in der Deklaration sogar weglassen), denn der Compiler kann die Funktionen beim Aufruf nicht aufgrund der Identifier unterscheiden, schließlich kommen diese beim Aufruf gar nicht vor.
Folgendes wäre also möglich:
void greet(string name) { cout << "Guten Tag, " << name << endl; } void greet() { cout << "Guten Tag" << endl; }
Bei solchen Funktionen kann man nun von einer "überladenen Funktion" sprechen.
Default Parameter
Nun stellen wir uns vor, wir wollen greet im Falle eines parameterlosen Aufrufes nicht eine allgemeine Begrüßung aussprechen lassen. Stellen wir uns vor, wir hätten einen Standardanwender und wenn kein spezieller Name angegeben wird, wird immer diese Person begrüßt. Nun wäre es natürlich problemlos möglich die Phrase "Guten Tag" einfach dementsprechend abzuändern und für diese geringe Komplexität wäre es auch absolut in Ordnung, aber in der Regel sind unsere Funktionen etwas komplexer und wir wollen wohl kaum die ganze Funktion kopieren, nur um den Parameter durch unseren Defaultwert zu ersetzen. Ein logischerer Ansatz wäre es die parameterbehaftete Funktion aus der parameterlosen Funktion aufzurufen, allerdings gibt es auch eine einfacherere Möglichkeit. Es ist möglich Parameter(am Ende der Parameterliste) mit Defaultwerten auszustatten, sodass die Parameter diese Werte annehmen, wenn die Funktion ohne Parameter aufgerufen wird.
Konkret wird dies bewerkstelligt, indem man einen Parameter mit = "initialisiert"(Eine kleine Inkonsistenz in der Sprachsyntax ist es, dass die Klammersyntax für Defaultparameter nicht möglich ist). Diesen Defaultparameter muss man allerdings nur in der Deklaration angeben, in der Definition muss er nicht genannt werden.
Wenn wir nun zum Beispiel "Herr Schmidt" als Defaultwert für den name-Parameter angeben ist es möglich die Funktion entweder mit einem Übergabewert aufzurufen, dann hat name den Wert des Argumentes, oder die Funktion ohne Parameter aufzurufen, dann hat name den Wert "Herr Schmidt".
Das ganze könnte zum Beispiel so aussehen
void greet(string name = "Herr Schmidt") { cout << "Guten Tag, " << name << endl; }
greet("Herr Müller"); //Guten Tag, Herr Müller greet(); //Guten Tag, Herr Schmidt
Variadic Functions
Nehmen wir nunmal an, wir haben eine Gruppe von Menschen und greet soll sie alle begrüßen, wir wissen also vorher nicht die Anzahl der Parameter. An dieser Stelle haben wir zwei Möglichkeiten, entweder wir benutzen einen Container, oder eine Variadic Function. Auch wenn hier zweitere Variante vorgestellt werden soll, ist die erste Variante eindeutig vorzuziehen. Syntaktisch ist sie beim Aufrufer zwar nicht so schön, aber sie ist im Gegensatz zu variadic functions Typsicher, denn bei ihnen ist der Compiler nicht in der Lage zur Compilezeit die Typen zu überprüfen und bei einem falschen Typ kann es zu unerwarteten und schwer zu findenden Fehlern kommen. Variadic Functions sind eigentlich auch Relikte aus der C Zeit, im nächsten C++ Standard wird es eine sauberere, typsichere Variante geben, sie zu definieren.
Nun aber, was ist eigentlich eine Variadic Function? Eine Variadic Function ist einfach eine Funktion mit einer unbestimmten Anzahl an Parametern.
Um eine Variadic Function zu deklarieren ist der letzte Parameter einfach ..., es ist dabei notwendig, dass zuvor mindestens ein Parameter vorkommt.
Es ist auch notwendig, dass die Anzahl an Parametern zum Ausführungszeitpunkt der Funktion bekannt ist, also zum Beispiel als Parameter übergeben wird.
Unser Funktionskopf wäre also
void greet(int number_of_names, ...)
... indiziert hier die Namen, unser Aufruf könnte zum Beispiel folgendermaßen aussehen
greet(3, "Herr Müller", "Herr Meier", "Herr Schmidt");
Die Frage ist nun, wie wir auf innerhalb der Funktion auf die Parameter zugreifen können und das ist seltsamerweise durch die Standardbibliothek und nicht durch Sprachmittel gelöst, und es ist notwendig eine #include <cstdarg> Anweisung oben in der Datei stehen zu haben. Zunächst einmal ist es notwendig eine Variable vom Typ va_list in der Funktion zu haben.
cout << "Guten Tag, " << endl; va_list vl;
Dann ist es notwendig va_start mit der Anzahl der Parameter aufzurufen.
cout << "Guten Tag, " << endl; va_list vl; va_start(vl, number_of_names);
nun können wir mit dem Aufruf von va_arg auf die Parameter zugreifen, jeder Aufruf liefert den jeweils nächsten Parameter. Außerdem muss der Typ angegeben werden, dabei können nur Primtive Typen und PODs verwendet werden, was PODs sind braucht uns zunächst nicht zu interessieren, nur dass es mit string, vector etc. nicht geht. Da wir wissen, dass der Typ von Stringliteralen eigentlich char const* ist können wir diesen Typ hier verwenden. Zum Schluss noch ein Aufruf va_ends um aufzuräumen und das ganze sieht folgendermaßen aus:
void greet(int number_of_names, ...) { va_list vl; cout << "Guten Tag, "; va_start(vl, number_of_names); for(int i(0); i < number_of_names; i++) cout << va_arg(vl, const char*) << ", "; va_end(vl); cout << endl; }
Rückgabe
Nun stellen wir uns eine andere Funktion vor, als greet(), eine die nicht nur etwas tun soll, sondern auch noch ein Resultat bringen soll, wie es zum Beispiel cin.get() tut.
Wir brauchen also ein Werkzeug um nicht nur Werte an eine Funktion zu übergeben, sondern auch Werte zurückzugeben.
Eine Möglichkeit wäre der Rückgabemechanismus von Funktionen (ein anderer Outputparameter, s.u.). Gehen wir von unserem Beispiel greet weg und implementieren eine einfache add_square Funktion, die die Summe der Quadrate zweier Zahlen berechnen soll. Wenn wir nun an mathematische Funktionen denken, sinus und cosinus zum Beispiel, so wissen wir, dass die Funktionen einfach im Term eingesetzt werden können. Ähnlich ist es auch bei Funktionen, eine Funktion die einen Rückgabewert hat, steht im aufrufendem Code für eben diesen Rückgabewert. Wenn wir nun also die Summe zweier Quadratzahlen ausgeben wollen, so rufen wir die Funktion mit den entsprechenden Parametern auf und setzen die Funktion an die
Stelle, an der wir den Wert haben wollen.
cout << add_squares(2, 3) << endl; //13
add_squares(2, 3) entspricht also dem Wert 13, 2 und 3 sind dabei die beiden Parameter, also die beiden Zahlen, die wir übergeben wollen(quadriert und addiert haben wollen).
Nur wie definiert man eine solche Funktion?
Zunächst einmal wie man so eine Funktion deklariert, denn auch wenn man mit dem Rückgabetyp nicht überladen kann(nur mit Parametern und CV-Quafizierern), muss der Compiler, wie bei jedem anderen Ausdruck, doch wissen, welchen Typ der Rückgabewert der Funktion nun eigentlich hat.
Der relevante Teil ist das void, denn dieses ist nicht etwa ein spezielles Schlüsselwort für Funktionsdeklarationen, sondern gibt den Rückgabetyp an. Oder besser gesagt, gibt ihn nicht an, denn void bedeuted, dass die Funktion keinen Rückgabewert hat(manche würden jetzt sagen, eine Prozedur ist). Für unsere Funktion wollen, wir dass der Rückgabewert der selbe ist, wie der der beiden Parameter, zum Beispiel
double add_squares(double a, double b);
Die Funktion nimmt 2 doubles und gibt schlussendlich double zurück.
Nun müssen wir nur noch wissen, wie wir denn den Rückgabewert bestimmen und dafür brauchen wir die return-Anweisung.
Die return Anweisung beendet die Funktion, springt also an den Aufruf zurück und ermöglicht es den Rückgabewert anzugeben. Das ganze geschieht in der Form return Wert;
Bei Funktionen ohne Rückgabewert ist es übrigens auch möglich nur return; zu verwenden, einfach um aus der Funktion zu springen.
Unsere Funktion könnte nun also folgendermaßen aussehen:
double add_squares(double a, double b) { return a*a + b*b; }
Wenn wir nun einen Blick auf die Mainfunktion werfen, sehen wir, dass es sich um eine Funktion handelt, die keine Parameter nimmt(es gibt noch eine Variante mit Parametern, die wir später kennen lernen werden) und ein int zurückgibt. Wenn wir dann allerdings einen Blick auf unsere Programme werfen, werden wir feststellen, dass wir nie einen Integerwert zurückgegeben haben, wie das? Für jede andere Funktion wäre das ein Fehler, aber int main ist wie bereits erwähnt ein Sonderfall einer Funktion. Es ist durchaus möglich in der main-Methode eine Zahl zurückzugeben, die dem Betriebssystem einen Statusbericht liefert, es ist aber auch erlaubt diese Rückgabe wegzulassen, dann wird automatisch 0 zurückgegeben - was normalerweise bedeuted, dass das Programm ordnungsgemäß beendet wurde.
Der globale Raum
Nachdem wir nun die Grundlagen von Funktionen kennen, wollen wir uns mal mit dem Ort beschäftigen, an dem diese definiert werden, nämlich den großen Weiten außerhalb der Mainfunktion
Bisher war das Innere der Mainfunktion der Scope, der am weiteresten außerhalb war. Variablen die dort definiert wurden waren überall(da wir ja in der Mainfunktion geblieben sind) verfügbar etc. Da wir nun aber gleichberechtigte Funktionen haben, ist dem nicht mehr so. Variablen innerhalb der Mainfunktion und innerhalb von Funktionen generell nennt man lokale Variablen und ebenso verhalten sie sich auch, sie sind lokal, das heißt nur in der Funktion verfügbar in der sie deklariert worden sind.
In Wahrheit ist der äußerste Bereich der globale Bereich, der Bereich der von nichts anderem umschlossen wird.
Dieser Bereich verhält sich anders als das innere von Funktionen, so ist es nicht möglich global einfach so Anweisungen auszuführen(wann sollten sie auch ausgeführt werden?), wie es auch nicht möglich ist Funktionen lokal zu definieren.
Globale Variablen
Neben Funktionen wollen wir noch kurz die globalen Variablen ansprechen. Es ist nämlich durchaus möglich Variablen global zu deklarieren, sodass sie von überall zugreifbar sind.
Nun stellt sich die Frage, warum es überhaupt lokale Variablen und Parameter, Rückgabewerte und den ganzen Kram gibt, wenn man doch einfach alles global deklarieren könnte.
Die Antwort ist einfach:
Variablen sollten immer im engst möglichen Scope deklariert werden. Der Grund für diese stillistische Regel liegt in der Fehlersicherheit. Wenn sämtlicher Code Zugriff auf eine Variable hat und sie verändern kann, kann es leicht vorkommen, dass irgendein Codebereich der Variable einen falschen Wert zuweist(übrigens resultieren Fehler in imperativen Programmen sehr häufig aus falschem State, also einer Variable die nicht den Wert hat die sie haben sollte) und dann ist der Wurm drin, denn Fehlersuche wird hier sehr schwer, schließlich kann jeder Code der auf die Variable zugreift die Variable verändert haben.
Aus diesem Grund sollte man globale Variablen genau so meiden, wie man goto meiden sollte, wie aber auch bei goto kann es durchaus gute Gründe für die Verwendung einer globalen Variable geben. Dazu kommt, dass wir mit unseren bisherigen Mitteln wesentlich leichter um goto herum kommen, als um globale Variablen.
Wie also definieren wir eine globale Variable? Nunja, genau wie eine lokale Variable, nur dass sie eben im globalen Raum deklariert wird.
#include <iostream> using namespace std; int global; void foo() { cout << global << endl; } int main() { global = 2; foo(); }
An diesem Beispiel kann man auch die Problematik globaler Variablen gut erkennen: Wenn der Aufrufer von foo global nun keinen gültigen Wert zuweist, verhält sich foo bestimmt nicht wie es sollte.
Konstanten
An dieser Stelle halte ich es auch für eine gute Idee auf Konstanten zu sprechen zu kommen.
Konstanten sind Variablen deren Wert nicht verändert werden kann.
Manche werden sich jetzt fragen, wozu man Konstanten denn überhaupt braucht, wenn Variablen doch eben dazu da sind, sich zu verändern.
Die Vorteile sind denen von Funktionen nicht unähnlich, stellen wir uns vor, wir haben einen Pfad zu einer Datei(als String) und diesen brauchen wir an mehreren Stellen im Code. Nun zum einen ist es unleserlich den String direkt im Code zu schreiben, "./012.jpg" sagt nicht so viel wie grassland_path o.ä, zum anderen kann es vorkommen, dass wir den Pfad nun verändern wollen, dann ist es besser wir können den Pfad an einer Stelle im Code verändern, als dass wir mit Suchen&Ersetzen über unseren gesamten Code gehen müssen, nur um den Pfad zu ändern.
Also, wie deklariert man eine Konstante? Wie eine Variable, nur zusätzlich mit dem const modifier neben dem Typ(ob links oder rechts vom Typ ist egal).
string const grassland_path("12.jpg");
grassland_path kann nun im Code ähnlich einer Variablen verwendet werden(manchmal sind nur Literale und Konstanten erlaubt, manchmal auch nur Variablen, Konstanten können immer dann verwendet werden, wenn sie nicht verändert werden und im Gegensatz zu Variablen auch dann, wenn der Wert bereits zur Compilezeit feststehen muss) Auch ist es kein Problem die Konstante global zu deklarieren, da sie sowieso keinen anderen, also auch keinen falschen Wert annehmen wird.
Statische Variablen
Lokale Variablen werden üblicherweise bei jedem Funktionsaufruf neu angelegt(wenn sie deklariert werden) und am Ende des Funktionsaufrufes wieder gelöscht(wenn der Scope verlassen wird), was ja auch logisch ist, aber manchmal will man das eben nicht. Nehmen wir an, wir wollen eine Funktion haben, die aus irgendeinem Grund mitzählt wie oft sie aufgerufen wurde.
Eine Lösung wäre eine globale Variable zu verwenden, doch eigentlich wollen wir ja nicht von überall darauf zugreifen können, uns reicht es von der Funktion aus darauf zugreifen zu können, wir wollen lediglich, dass die Variable über den Funktionsaufruf hinaus gültig bleibt und beim nächsten Funtkionsaufruf noch gültig ist.
Was wir brauchen ist eine sogenannte statische Variable (C++ verwendet Schlüsselwörter gerne Kontextabhängig wieder, es gibt 3 verschiedene Arten von statischen Variablen, statisch für Funktion, statisch für Klasse und statisch für Übersetzungseinheit, hier beschäftigen wir uns zunächst mit der ersten Variante).
Eine statische Variable ist durch das Schlüsselwort static gekennzeichnet und wird ansonsten wie jede andere lokale Variable deklariert und initialisiert. Wichtig ist, dass sie nur beim ersten Aufruf der Funktion initialisiert wird, für jeden weiteren Aufruf bleibt der Wert der, den sie vorher hatte. Eine Funktion die lediglich zählt, wie oft sie aufgerufen wurde könnte also folgendermaßen aussehen:
#include <iostream> using namespace std; void foo() { static int call_counter(1); cout << "dies ist der " << call_counter++ << ".e Aufruf dieser Funktion" << endl; } int main() { foo(); //...1.e... foo(); //...2.e... foo(); //...3.e... }
Referenzen
Zum Schluss will ich noch auf Referenzen zu sprechen kommen. Wer Erfahrung in anderen Programmiersprachen hat, wie zum Beispiel Java, oder Ruby wird den Begriff zwar bereits kennen, doch sind Referenzen in Java und Ruby eher mit Pointern zu vergleichen, wenn auch nicht das Selbe. Referenzen in C++ sind jedenfalls etwas anderes.
Referenzen sind im Prinzip andere Namen für Variablen (auch ein klein wenig mehr, wie z.B. Speicherbereiche, auf die bereits ein Pointer zeigt, aber das ist nichts was wir jetzt schon verstehen können). Eine Referenz ist ein spezieller Variablentyp, genauergesagt wäre eine Referenz auf ein int, eine int& Variable. Also wenn dem Typ ein & folgt, handelt es sich um eine Referenz.
Die Referenz kann nun synonym für die andere Variable verwendet werden, um den Unterschied zu verdeutlichen, folgendes Programm
#include <iostream> using namespace std; int main() { int a(2); int b(a); //a wird kopiert, b hat den Wert 2 b = 4; //b wird verändert und hat nun den Wert 4, a hat immernoch den Wert 2 cout << a << " - " << b << endl; //2 - 4 int c(2); int& d(c); //d ist eine Referenz auf c d = 4; //da d eine Referenz auf c ist, wird c verändert und hat nun den Wert 4 cout << c << " - " << d << endl; //4 - 4 }
Konstante Referenzen
Eine Referenz muss immer auf eine Variable zeigen, es ist nicht möglich einer Referenz eine Konstante oder ein Literal zuzuweisen, dazu benötigt es konstante Referenzen.
Eigentlich ist der Begriff irreführend, denn Referenzen sind immer Konstant, es handelt sich eigentlich um Referenzen auf Konstanten.
Natürlich können derartige Referenzen auch auf herkömmliche Variablen zeigen, sie können über diese Referenz dann nur nicht mehr verändert werden. Das const steht in dem Fall entweder links oder rechts vom Typ auf den die Referenz zeigt, aber niemals rechts vom &.
int x(2); int& a(x); //erlaubt a = 3; //erlaubt, x verändert sich int& b(2); //nicht erlaubt int const& c(2); //erlaubt c = 3; //nicht erlaubt int y(2); int const& d(y); //erlaubt d = 3; //nicht erlaubt, auch wenn y keine Konstante ist
Call-By-Reference
Vielleicht fragen sie sich schon, wozu das ganze, schließlich kann ich ja auch die Variable direkt modifizieren. In der Tat gibt es Anwendungsfälle für Referenzen außer Call-By-Reference, allerdings kommen diese eher selten vor, Call-By-Reference ist also der Hauptanwendungsfall für Referenzen.
Wie bereits erwähnt können wir die Parameter innerhalb der Funktion verändern, ohne dass sich die Variablen verändern, mit denen die Funktion aufgerufen wird, was aber wenn wir das gar nicht wollen? Was wenn wir der Funktion variablen übergeben wollen, die verändert werden sollen? Das können wir den Parameter zu einer Referenz machen, Änderungen an der Referenz innerhalb der Funktion wirken sich wie alle Referenzen auf die eigentliche Variable aus. Also unterscheiden wir:
//call-by-value void foo1(int x) { x++; } //call-by-reference void foo2(int& x) { x++; } int main() { int x(0); foo1(x); //nichts geschieht, x wird kopiert und die Veränderung innerhalb der Funktion lässt uns kalt foo2(x); //x wird verändert und enthält nun den Wert 1! }
Logischerweise ist es nicht möglich foo2 ein Literal oder eine Konstante zu übergeben.
Wann sollte man für Resultate also Rückgabewerte verwenden und wann Call-By-Reference? Rückgabewerte sind auf jeden Fall klarer, man sieht sofort, was passiert, aber es ist ebenso unsinnig eine Variable zu übergeben, die kopiert wird, verändert wird und dann wieder zurückkopiert wird, wenn wir eine Variable also auf Basis ihres bisherigen Wertes verändern wollen ist Call-By-Reference angebracht.
Call-By-Const-Reference
Call-By-Reference hat schon seine Anwendung, aber es gibt einen Sonderfall, der besonders häufig vorkommt: Eine konstante Referenz als Parameter.
Warum sollte man so etwas wollen? Referenzen haben hier doch den Vorteil, dass wir die Variable mit der die Funktion aufgerufen wird verändern können, bei einer konstanten Referenz geht das doch nicht. Es handelt sich im Prinzip doch nur um einen normalen Parameter, mit der zusätzlichen Einschränkung, dass wir ihn nicht einmal lokal verändern dürfen, oder?
Nein, natürlich nicht ganz. Der Unterschied liegt interessanterweise in der Performance. Es ist unsinnig einen int, oder ein double als konstante Referenz zu übergeben, anders sieht es schon bei Containern wie string oder vector aus. C++ folgt einer Value-Semantik, das heißt, wenn wir etwas übergeben wird es üblicherweise auch wirklich kopiert, der gesamte String wird Buchstabe für Buchstabe kopiert und der gesamte vector wird Element für Element kopiert. Wenn wir nun einen besonders großen vector oder anderen Typ haben wird immer alles kopiert, was viel Zeit in Anspruch nehmen kann. Deswegen ist es üblich alle nicht-primitiven Typen(nicht bool, char, short, int, long, double, float etc..) als konstante Referenz zu übergeben, denn meistens ist es innerhalb der Funktion nicht notwendig den Parameter zu verändern, also reicht es wenn wir lesenden Zugriff haben und dann können wir auch auf das teure Kopieren verzichten.
Um auf unser greet Beispiel zurückzukommen, besser wäre es folgendermaßen:
#include <iostream> using namespace std; void greet(string const& name) { cout << "Guten Tag, " << name << endl;; } int main() { greet("Herr Schmidt"); }
Funktionen der Standardbibliothek
Nun, da wir wissen wie wir Funktionen erstellen und verwenden können, will ich anmerken, wo man globale Funktionen in der Standardbibliothek finden kann.
Mit einem #include <cmath> macht man sich eine ganze Reihe von mathematischen Funktionen verfügbar, auf diese einzeln einzugehen würde hier den Rahmen sprengen. Wann immer eine mathematische Aufgabe ansteht, sollte man zunächst mal hier schauen, welche Funktionen es in cmath alles gibt: C++-Referenz
Zufälle
Auf ein Beispiel möchte ich dennoch bereits an dieser Stelle eingehen. Zum Beispiel Zufallszahlen! Das ist nichts mathematisches und <cmath> tuts dafür auch nicht, viel eher benötigen wir dazu ein #include <cstdlib> und ein #include <ctime>.
Wollen wir also einmal 10 Zufallszahlen erzeugen und ausgeben, nur wie kann ein Computer Zufälle angeben, wenn er selbst komplett vorherbestimmt ist und alles berechnet? Nun.. gar nicht, stattdessen wird die Abfolge von Zufallszahlen von einem Startwert aus errechnet, ist der Startwert gleich, ist auch die Abfolge von Zufallszahlen gleich. Wir brauchen also etwas, das sich immer verändert und das ist die Zeit(deswegen auch #include <ctime>). Mit der Funktion time, der wir den, uns noch unbekannten, Parameter NULL übergeben kriegen wir die Anzahl an Sekunden, die seit dem 1. Januar 1970 vergangen ist. Wir initialisieren also genau einmal einen Startwert(nicht öfter!) mit der aktuellen Zeit in Sekunden(Der erste Wert ist nicht diese Zeit, sondern wird daraus errechnet), dazu verwenden wir die Funktion srand. Okay, nun brauchen wir noch die Zufallszahlen selbst und die bekommen wir mit der Funktion rand, die wir ohne Parameter aufrufen und das ganze sieht so aus:
#include <iostream> #include <cstdlib> #include <ctime> using namespace std; int main() { srand(time(NULL)); for(int i(0); i < 10; ++i) cout << rand() << endl; }
und die Ausgabe könnte z.B. lauten:
1257569704 1096180647 1125378322 1134723543 635802469 179718075 599647792 1498371159 1332505092 540341585
Wirkt ziemlich willkürlich, nicht wahr? Es handelt sich um Zahlen zwischen 0 und den Wert von RAND_MAX, der abhängig vom Compiler ist. Somit bringt es uns eigentlich in der Form nicht viel, aber wir können uns daraus unseren Zufallswert errechnen. Wollen wir Werte zwischen 0 und 100, ist das einfach, einfach den Modulooperator verwenden:
cout << rand() % 100 << endl;
und die Ausgabe könnte z.B. folgendermaßen lauten:
18 81 78 30 94 46 81 9 30 85
Wollen wir Werte zwischen einem Bereich, der nicht bei 0 beginnt, addieren wir einfach einen Wert dazu. z.B. 10 Zufallszahlen in einem Bereich von 100 bis 200 wären:
cout << 100 + rand() % 100 << endl;
Damit können wir nun beliebig viele Zufallszahlen in einem beliebigen Bereich erzeugen!