====== Funktionen ====== Nach dem aufwendigen [[c:tutorial:loops|Kapitel mit den Schleifen]], schauen wir uns nun ein einfacheres Thema an. Nicht einfacher, weil es einfacher wäre, sondern weil wir bereits aus den vorherigen Lektionen Wissen mitbringen, das wir nun in ähnlicher Form wiederfinden werden. Und letztendlich, weil wir ja bereits die ganze Zeit mit einer Funktion namens ''main'' arbeiten. Und auch [[c:lib:stdio:printf()]] ist kein Schlüsselwort von C, sondern nur eine Funktion, die bei C aber grundsätzlich mitgeliefert wird. ===== Aufruf einer Funktion ===== Wie eine Funktion aufgerufen wird, kennen wir durch [[c:lib:stdio:printf()]] bereits: Wir schreiben den Funktionsnamen und übergeben in Klammern die Argumente (auch Parameter genannt): printf( "Hallo Welt\n" ); [[c:lib:stdio:printf()]] ist eine besondere Funktion, denn sie akzeptiert beliebig viele Argumente. Das ist aber eher die Ausnahme und für den Anfang nicht wichtig, daher wird diese Eigenschaft später in einem eigenen [[c:article:stdarg|Artikel]] erklärt. Wir werden uns hier erstmal um ganz normale Funktionen kümmern. Funktionen haben in der Regel eine Rückgabe, denn oftmals sollen sie ja etwas ausrechnen - wie in der Mathematik. [[c:lib:stdio:printf()]] druckt Zeichen auf den Bildschirm und liefert zurück, wieviele Zeichen es gedruckt hat. Wenn man weiß, wieviele Zeichen der Bildschirm breit ist, weiß man so entsprechend, wieviel Platz man noch hat. int printedChars; printedChars = printf( "Hallo Welt\n" ); Nehmen wir an eine Funktion soll zwei Zahlen addieren - das ließe sich natürlich einfach mit dem +-Operator machen, aber darum geht es ja nicht. In jedem Fall würde man eine solche Funktion wohl so aufrufen: int result; result = add( 1, 2 ); und anschließend erwarten, dass ''result'' den Wert 3 besitzt. Nun wissen wir, wie wir eine Funktion rufen würden, wenn wir sie schreiben könnten. Und wie das funktioniert schauen wir uns nun an. ===== Deklaration einer Funktion ===== Wie wir bereits zu Beginn des Tutorials gelernt haben, unterscheidet man zwischen Deklaration und Definition. Die Deklaration enthält nur die Information, wie die Funktion verwendet werden kann. Also wie sie heißt, welchen Datentyp sie zurück liefert und welche Parameter sie bekommt: ( ); Diesen grammatikalischen Aufbau nennt man auch **Signatur** einer Funktion. Eine Deklaration benötigt man, um dem Compiler den Identifier (den Namen der Funktion) und dessen Bedeutung (also, dass es sich um eine Funktion handelt) bekannt zu geben. Für die Deklaration einer Funktion wird auch oft der Begriff "Prototyp" verwendet. Prototypen sind erforderlich, wenn eine Funktion noch nicht definiert wurde, aber bereits gerufen werden soll. Es galt lange Zeit zum guten Stil, dass alle Funktionen zunächst zu Beginn (bzw. in einer eigenen Headerdatei) im Quelltext deklariert wurden, so dass man alle Funktionen sofort verwenden kann. Heute hat sich diese Sicht etwas geändert - je weniger Funktionen man kennt, desto weniger kann man damit verkehrt machen. Deklarieren wir mal eine Funktion, die zwei Integer-Werte addieren und entsprechend die Summe als Integer zurückliefern soll. Hierfür schreiben wir die Signatur einer solchen Funktion und fügen ihr ein Semikolon an: int add( int left, int right ); Wo ist die Addition? Egal, eine Deklaration behauptet ja auch nur, dass es eine solche Funktion gibt, damit der Compiler weiß, dass der Identifier ''add'' zwei Integer-Parameter bekommt und ein Integer zurückliefert. Wichtig ist, dass nach einer Funktionsdeklaration ein Semikolon (";") folgt. Das beendet die Anweisung und gibt dem Compiler zu verstehen, dass der Programmierer jetzt noch nicht beschreiben möchte, was diese Funktion genau machen soll. ===== Definition einer Funktion ===== Um zu beschreiben, was die Funktion tun soll, definieren wir die Funktion. Dazu wiederholen wir die Signatur und statt eines Semikolons öffnen wir nun einen Anweisungsblock mit einer geschweiften Klammer. In diesem Anweisungsblock stehen die Anweisungen, die die Funktion ausführen soll: int add( int left, int right ) { /* Anweisungen */ } Die Signatur unserer Funktion sagt aus, dass ein Integerwert zurückgegeben wird. Wann immer ein Wert zurück gegeben wird, muss man klar aussagen, welchen Wert man zurück gibt. Wir kennen hier bereits den Befehl ''return'', dem der zurück zu gebende Wert folgt. int add( int left, int right ) { /* Anweisungen */ return 0; } Nun wollen wir aber nicht grundsätzlich 0 zurückliefern. Stattdessen wollen wir die beiden Werte, die wir als Parameter erhalten (''left'' und ''right'') miteinander addieren und das Ergebnis zurückliefern. int add( int left, int right ) { int sum; sum = left + right; return sum; } Die Parameter ''left'' und ''right'' sind ganz normale Variablen, wie es ''sum'' auch ist. Sie können gelesen und beschrieben werden, der einzige Unterschied ist, dass sie durch den Aufrufer der Funktion bereits Werte zugewiesen bekommen haben. Die Aufgabe dieser Funktion ist es, diese beiden vom Aufrufer zugewiesenen Werte miteinander zu verrechnen. Wir haben ja bereits gelernt, dass C mit Expressions arbeitet. ''left'' und ''right'' sind Expressions vom Datentyp ''int''. ''left + right'' ist eine Expression, die ebenfalls vom Datentyp ''int'' ist. Auch ''sum'' ist eine Expression vom Datentyp ''int'', daher dürfen wir ''left + right'' der Variablen ''sum'' zuweisen. Die Funktion ''add'' liefert ebenfalls einen ''int'' zurück, deswegen darf ''sum'' per ''return'' zurückgegeben werden. Alle ''int''-Expressions sind miteinander austauschbar, denn aus allen kann ein ''int'' ausgelesen werden (Wichtig: Es geht ums Lesen. Der Expression ''left + right'' kann nichts zugewiesen werden, ''left'' oder ''right'' schon.). Wir sorgen mit ''sum = left + right;'' dafür, dass in der Variablen ''sum'' der Wert gespeichert wird, den die Expression ''left + right'' erzeugt. Und wir speichern den Wert, damit wir den gleichen Wert später wieder auslesen können - hier wird der Wert bei ''return sum'' wieder ausgelesen. Die ''return''-Anweisung benötigt einen Integer, und die Expression ''left + right'' liefert ja einen Integer. Wir können uns das Zwischenspeichern in einer extra dafür angelegten Variable also sparen: int add( int left, int right ) { return left + right; } Das macht den Quelltext kürzer und einfacher. Die Variable ''sum'' habe ich lediglich angelegt, um die Gleichartigkeit von lokalen Variablen und Parametern aufzuzeigen. Alle Parameter werden kopiert, das bedeutet, dass beim Aufruf auch die Zahlen kopiert werden: int add( int left, int right ) { return left + right; } int main( void ) { int links = 1, rechts = 2; int sum = add( links, rechts ); // <--- Aufruf printf( "Die Summe ist %d\n", sum ); return 0; } Wie im obigen Beispiel ersichtlich, erfolgt der Aufruf einer Funktion durch die Verwendung des Namens, gefolgt von einem Klammernpaar und einem Semikolon. In den Klammern stehen entsprechend der Signatur erforderliche Parameter. Liefert die Funktion zusätzlich noch einen Wert zurück, muss dieser wie im Beispiel direkt einer Variable zugewiesen werden, ansonsten geht er verloren.\\ Denken wir wieder in Werten und Expressions: Beim Aufruf der Funktion ''add'' werden zwei Expressions vom Typ ''int'' angegeben. Diese beiden Expressions werden ausgewertet. ''links'' hat den Wert 1, ''rechts'' hat den Wert 2. Diese Werte werden jetzt in den Arbeitsbereich der Funktion kopiert. Diese Form des Aufrufs nennt man **[[glossary:callbyvalue|Call by Value]]**, was soviel heißt wie "Aufruf mit Werten". Man kann das so verstehen, dass vor dem Aufruf von ''add'' der Speicher für die Funktion bereitgestellt wird und dort, wo später die lokale Variable ''left'' liegt, wird der Wert der Expression (''links'') hineinkopiert und dort, wo später die lokale Variable ''right'' liegen wird, wird der Wert der Expression ''rechts'' hinkopiert. Ich habe die Variablen hier in der Hauptfunktion ''main'' extra deutsch hingeschrieben, damit man sieht, dass die Namen der Variablen unterschiedlich sein dürfen, also die Namen der Variablen beim Aufruf überhaupt nichts mit dem Namen der Parametervariablen zu tun haben. Hier werden die Werte kopiert und existieren damit zweimal. Überschreibt man in der Funktion ''add'' nun den Wert für ''left'' dann wirkt sich das nicht auf den Wert von ''links'' aus, die in Speicherbereich von der Funktion ''main'' als lokale Variable definiert ist. Das lässt sich leicht vor Augen führen, wenn wir andere Expressions beim Aufruf auswerten: int sum = add( 1, 2 ); // <--- Aufruf ''1'' ist ebenso eine Expression mit dem Datentyp ''int'' und dem Wert 1. Die Expression wird ausgewertet und der Wert 1 wird nun wieder in den Speicherbereich für ''add'' an die Stelle geschrieben, wo die Funktion später mit der Variablen ''left'' zugreift. Würde die Funktion ''add'' nun ''left'' überschreiben, wird nur die Kopie des Wertes überschrieben. Die ''1'', die beim Aufruf angegeben wurde, kann man natürlich nicht überschreiben, denn ''1'' ist eine konstante Zahl. Konstanten kann man - wie der Name schon sagt - nicht verändern. ===== Abbrechen einer Funktion ===== Mit der ''return''-Anweisung kann man eine Funktion sofort verlassen. Sie ist damit in gewisser Weise verwandt mit der ''break''-Anweisung für Schleifen. Wir haben im Kapitel über Wächter gesprochen. Nehmen wir an, dass wir nun eine Funktion schreiben wollen, die 1 bei einer positiven Zahl, -1 bei einer negativen Zahl und 0 bei 0 zurückgeben soll. int sign( int value ) { int result; if( value ) { if( value > 0 ) result = 1; else result = -1; } else result = 0; return result; } Wenn ''value'' wahr ist (also nicht 0 ist), dann wird geprüft, ob ''value'' größer oder kleiner 0 ist. Sonst ist das Ergebnis Null. Wir wollen die Funktion nun aber anders neu schreiben, wobei wir die Variable ''result'' aber einsparen wollen und die Funktion verlassen wollen, sobald wir das Ergebnis kennen. Hierfür installieren wir Wächter. Mit return brechen wir die Funktion sofort ab und geben das Ergebnis zurück. Jeder nachfolgende Code der Funktion wird ignoriert. int sign( int value ) { if( !value ) return 0; if( value > 0 ) return 1; return -1; } Wir sehen, dass die Funktion nun keine temporäre Variable mehr benötigt und sogar etwas kürzer ist. Am Schluss wird nicht mehr gefragt, ob ''value'' kleiner 0 ist, denn eine andere Möglichkeit bleibt schließlich nicht mehr über, wenn die beiden Wächter ''value'' schon die Fälle //''value'' gleich 0// und //''value'' größer 0// abgefangen haben. ===== Prozeduren ===== Eine Funktion hat in der Mathematik die Aufgabe aus diversen Eingabevariablen einen Funktionswert zu bestimmen. Nun kann man in C aber auch Funktionen schreiben, die nichts zurückgeben. Will man die Tatsache betonen, dass es keine Funktionsrückgabe gibt, spricht man gelegentlich von "void-Funktionen" oder aus dem Pascal-Sprachgebrauch von "Prozeduren". Eine Prozedur handelt halt einige Anweisungen entsprechend der Übergabeparameter ab und fertig. Die Unterscheidung wurde in Pascal mit "procedure" und "function" vollzogen, in C wird der Unterschied nicht so offensichtlich festgehalten: man gibt als Rückgabetyp einfach ''void'' an. void SayHello( void ) { printf( "Hello\n" ); } Wie man sieht, fehlt hier auch die ''return''-Anweisung am Ende. Da auch nichts zurückgegeben werden muss, kann man return auch keinen Wert übergeben und so endet die Prozedur, sobald die schließende geschweifte Klammer erreicht wird. Möchte man eine Funktion dennoch vorzeitig verlassen, zum Beispiel weil man nicht mehr als fünfmal hintereinander "Hallo" sagen möchte, so kann man ''return'' auch ohne Parameter aufrufen: void SayHello( int howOften ) { int i; for( i=0; i Dieser Code zeigt nur, dass man ''return'' jederzeit verwenden kann. An dieser Stelle wäre es schöner vor der Schleife die Variable ''howOften'' einmalig zu überprüfen und falls sie größer als 5 ist, sie auf 5 zu korrigieren. Damit kann man sich die Abfrage innerhalb der Schleife wieder sparen. Probiert das doch mal als kleine Übung :-) ===== Rekursion ===== Ich werde Dir nun eine Funktion zeigen, die uns im Verlauf des Tutorials noch häufiger begegnen wird. Sie berechnet ein Element der Fibonacci-Folge. Folgendes Problem: Ein Element der Fibonacci-Folge ist definiert über die natürlichen Zahlen und entspricht der Summe der beiden vorangegangenen Elemente. Eine Besonderheit gilt für die beiden ersten Elemente, da sie natürlich nicht über zwei Vorgänger verfügen. Hier gilt, dass das 0. Element 0 ist und das 1. Element den Wert 1 besitzt. fib(0) = 0 fib(1) = 1 fib(n) = fib( n-1 ) + fib( n-2 ) Also gilt für die Fibonacci-Folge: 0, 1, 1, 2, 3, 5, 8... usw. Die Reihe sieht zunächst ziemlich langweilig aus, aber Fibonacci wird uns noch auf viele Abenteuer der Programmierung begleiten. Ihre Formulierung ist jedoch etwas besonderes, denn der n.-Wert hängt vom n-1. und dem n-2. Wert ab. Eine solche Funktion wird **rekursiv** genannt. Schauen wir uns das Hauptproblem an: int fib( int n ) { return fib( n-1 ) + fib( n-2 ); } Das kann man so nicht stehen lassen, denn hier würde die Funktion sich bis in alle Ewigkeit selbst aufrufen. Da jeder Funktionsaufruf ein wenig Speicher kostet, wird das Programm irgendwann wegen Speichermangel abstürzen. Wir müssen also beschreiben, wann die Rekursion enden soll. Diese Bedingung nennt man **Rekursionsanker** und wir werden diesen Anker hier als Wächter implementieren. Denn wir haben ja noch unsere beiden Sonderfälle bei den Indizes 0 und 1. Hierfür positionieren wir einen passenden Wächter: int fib( int n ) { if( n <= 1 ) return n; return fib( n-1 ) + fib( n-2 ); } Wird die Funktion ''fib'' mit den Werten 0 oder 1 für n gerufen, so wird 0 bzw. 1 zurückgegeben. Das passt also. Schauen wir uns den Aufruf für 2 an, ruft sie sich selbst für die Werte 1 und 0 und addiert die Rückgaben: für fib(2) erhalten wir also 1. Das ganze als vollständiges Programm: #include int fib( int n ) { if( n <= 1 ) return n; return fib( n-1 ) + fib( n-2 ); } int main( void ) { int index = 0; while( index <= 10 ) { printf( "fib( %d ) => %d\n", index, fib( index )); index = index + 1; } return 0; } Wir bekommen folgende Ausgabe: fib( 0 ) => 0 fib( 1 ) => 1 fib( 2 ) => 1 fib( 3 ) => 2 fib( 4 ) => 3 fib( 5 ) => 5 fib( 6 ) => 8 fib( 7 ) => 13 fib( 8 ) => 21 fib( 9 ) => 34 fib( 10 ) => 55 Und das entspricht ja - wie gewünscht - genau der [[http://de.wikipedia.org/wiki/Fibonacci-Folge|Fibonacci-Folge]]. So gemächlich die Fibunacci-Folge erstmal aussieht, sie steigt sehr schnell an. Und diese Form der Implementierung sorgt dafür, dass damit ein moderner Rechner relativ schnell an seine Grenzen stößt. ===== Übergabekosten ===== Es ist nicht "umsonst", Argumente an eine Funktion zu übergeben. Keine Angst, es kostet natürlich kein Geld. Nun kommt die Frage auf: Was kostet es denn dann? Beim Aufruf unserer Funktion ''add'', werden die Werte der beiden Variablen ''left'' und ''right'' an die Funktion übergeben. Dabei werden die Werte aus den Speicherstellen, die durch die Variablen definiert sind in neue, extra für die Funktion ''add'' bereitgestellte Speicherbereiche **kopiert**. Diese Übergabe der Funktionsargumente nennt man wie bereits erwähnt [[glossary:callbyvalue|Call by Value]]. Das ist an sich nichts Schlimmes. Es werden dann eben (je nach Computer) 4 oder 8 Byte mehr Speicher benötigt. Bei der Verwendung von [[c:tutorial:arrays|Arrays]] mit vielen Elementen, großen [[c:tutorial:struct|Strukturen]] oder tiefen Rekursionen, kann der Speicherverbrauch aber deutlich ansteigen. Wie man dieses Problem vermeidet, werden wir im Kapitel zu [[c:tutorial:pointer|Zeiger]] lernen. ====== Ziel dieser Lektion ====== Du solltest nun in der Lage sein, die Signatur einer Funktion, die Deklaration und die Definition einer Funktion zu unterscheiden. Du hast gelernt, dass C Parameter beim Funktionsaufruf kopiert (Fachbegriff Call-by-Value). Die Bedeutung von Expressions und Werten sollte Dir weiterhin bewusst sein. Wir werden in den kommenden Kapiteln noch eine Vielzahl von Funktionen schreiben, so dass Dir der Aufbau von Funktionen mit anderen Parametern und Rückgabeparametern sicherlich bald in Fleisch und Blut übergeht, doch Du solltest den grundsätzlichen Aufbau einer Funktion verstanden haben und wissen, dass Funktionen andere Funktionen rufen können (main() ruft fib()) und sich selbst rufen können (fib() ruft fib()). Wenn Funktionen sich selbst rufen, nennt man dies einen rekursiven Aufruf. Rekursive Funktionen brauchen einen Rekursionsanker, also eine Bedingung, die dafür sorgt, dass sich die Funktion irgendwann aufhört, sich selbst zu rufen. In der [[c:tutorial:arrays|nächsten Lektion]] werden wir uns mit Arrays beschäftigen.