Grundlagen der Programmiersprache C für Studierende der Naturwissenschaften Teil 5: Funktionen, Gültigkeitsbereiche und Rekursion Martin Nolte Abteilung für Angewandte Mathematik Universität Freiburg i. Br. Vorlesung vom 23. Mai 2016
Motivation: Binomialkoeffizienten Gegeben seien Zahlen n, k N mit k n. Der Binomialkoeffizient n über k ( n n! = k) k! (n k)! entspricht der Anzahl der k-elementigen Teilmengen einer Menge mit n Elementen. Um dies direkt zu berechnen, muss die Fakultät 3 mal ausgewertet werden: int fac_n = 1; for(int i = 2; i <= n; i = i+1) fac_n = fac_n * i; Allerdings wollen wir diese Schleife nicht 3 mal schreiben müssen.
Motivation: Binomialkoeffizienten Stattdessen schreiben wir eine Funktion für die Fakultät: int fac ( int n ) int fac_n = 1; for(int i = 2; i <= n; i = i+1) fac_n = fac_n * i; return fac_n; Unser Binomialkoeffizient könnte dann wie folgt aussehen: int nk = fac( n ) / (fac( k ) * fac( n - k ));
Inhalt Funktionen Die Funktion main Gültigkeitsbereiche Rekursive Funktionen Beispiel: Ein kleiner Taschenrechner
Funktionen Funktionen berechnen aus ihren Argumenten ein Ergebnis, den Rückgabewert. Funktionen können mehrfach und überall im Programm aufgerufen. Die Typen von Rückgabewert und Argumenten bilden den Prototyp der Funktion. Syntax: type 0 identifier 0 ( type 1 identifier 1,..., type n identifier n ) type 0 Typ der Rückgabewertes identifier 0 Name der Funktion type i Typ des i-ten Arguments identifier i Name es i-ten Arguments Die Argumentenliste kann auch leer sein.
Definition von Funktionen Eine Funktion wird definiert durch ihren Prototypen gefolgt von einem Anweisungsblock. Syntax: type 0 identifier 0 ( type 1 identifier 1,..., type n identifier n ) statement... Beispiel: int fac ( int n ) int fac_n = 1; for( int i = 2; i <= n; i = i + 1 ) fac_n = fac_n * i; return fac_n;
Die return-anweisung Syntax: return expression ; expression Ausdruck für den Rückgabewert Semantik: Verlasse die Funktion und gebe den Wert des Ausdrucks zurück. Der Typ des Ausdrucks muss mit dem Typ des Rückgabewertes der Funktion übereinstimmen. Ist Typ des Rückgabewertes void, muss der Ausdruck leer sein. Hinweis: Im Fall der Funktion main wird das Programm beendet.
Funktionsaufrufe Funktionsaufrufe sind elementare Ausdrücke. Syntax: identifier ( expression 1,..., expression n ) identifier Name der Funktion expression i Ausdruck für das i-te Funktionsargument Semantik: Rufe die Funktion mit den angegebenen Werten auf. Typ: Rückgabetyp der Funktion identifier. Wert: Rückgabewert der Funktion identifier. Die Typen der Ausdrücke müssen zu den Typen der Argumente im Funktionsprototypen passen. Achtung: Die Auswertungsreihenfolge der Argumente ist implementierungsabhängig.
Deklaration von Funktionen Funktionen müssen bekannt sein, bevor sie benutzt werden. Beispiel: 1 #include <stdio.h> 2 3 int print main () 4 5 printf( "The factorial of %d is %d", n, fac( n ) ); 6 7 8 int fac ( int n ) 9 10 int fac_n = 1; 11 for( int i = 2; i <= n; i = i + 1 ) 12 fac_n = fac_n * i; 13 printf( "The factorial of %d is %d", n, fac_n ); 14 return fac_n; 15 Falsch: Zum Zeitpunkt des Aufrufes ist der Prototyp der Funktion fac unbekannt.
Deklaration von Funktionen Wir können dem Compiler eine Funktion bekannt geben, indem wir sie deklarieren. Dazu schreiben wir den Prototypen der Funktion gefolgt von einem Semikolon. Syntax: type 0 identifier 0 ( type 1 identifier 1,..., type n identifier n ) ; Beispiel: int fac ( int n );
Implizite Deklaration von Funktionen Historisch mussten Funktionen nicht vor Benutzung deklariert werden: Sie wurden dann bei der ersten Verwendung implizit deklariert. Die Typen der Argumente kann der Compiler aus dem Aufruf ermitteln. Der Rückgabetyp wurde auf int festgelegt. Sie können zu zu schwer auffindbaren Fehlern führen. Seit C99 sind implizite Funktionsdeklarationen verboten. Warnung: Viele moderne Compiler (z.b. gcc Version 5) akzeptieren sie trotzdem. Sie geben lediglich eine Warnung aus.
Seiteneffekte Funktionen können Seiteneffekte haben. Beispiel: int fac ( int n ) int fac_n = 1; for( int i = 2; i <= n; i = i + 1 ) fac_n = fac_n * i; printf( "%d! = %d.\n", n, fac_n ); return fac_n; Hier wird bei jeder Berechnung das Ergebnis zusätzlich ausgegeben. Eine Funktion hat einen Seiteneffekt (engl. side effect), wenn Sie den Zustand des Programms verändert. Eine Funktion ohne Seiteneffekte heißt pur (engl. pure). In C gibt es keine Möglichkeit, Funktionen als pur zu kennzeichnen.
Der leere Datentyp void Manche Funktionen, etwa printf haben kein Ergebnis, sondern nur Seiteneffekte. Für diesen Fall gibt es den Datentyp void. Eine void-funktion muss nicht mit einer return-anweisung verlassen werden. Eine return-anweisung in einer void-funktion darf keinen Wert enthalten; sie lautet: return ; Beispiel: void print_fac ( int n ) printf( "%d! = %d.\n", n, fac( n ) );
Inhalt Funktionen Die Funktion main Gültigkeitsbereiche Rekursive Funktionen Beispiel: Ein kleiner Taschenrechner
Die Funktion main Beim Programmstart wird automatisch die Funktion main aufgerufen. Der Prototyp von main kann folgende Formen haben: int main () int main ( int argc, char *argv[] ) eine implementierungsabhängige Form Die zweite Form bekommt zusätzlich die Arguments, die auf der Kommandozeile angegeben werden. Ansonsten ist die Funktion main nicht besonders ausgezeichnet.
Programmargumente In der Form int main ( int argc, char *argv[] ) bekommt main die Arguments des Programms übergeben. argc Anzahl der Argumente (nichtnegativ) argv Feld der Länge argc von nullterminierten Strings Das erste Argument ist der Programmname (falls argc > 0). Er ist Implementierungsabhängig und kann leer sein (""). Das Programm darf die Stings in argv verändern.
Programmargumente Beispiel: #include <stdio.h> int main ( int argc, char *argv[] ) for( int i = 0; i < argc; i = i+1 ) printf( "argv[ %d ] = \"%s\"\n", i, argv[ i ] ); return 0; liefert folgende Ausgabe: $./args arg1 "argument 2" arg3 argv[ 0 ] = "./args" argv[ 1 ] = "arg1" argv[ 2 ] = "argument 2" argv[ 3 ] = "arg3"
Der Rückgabewert von main Der Rückgabewert von main gibt üblicherweise Auskunft über den Zustand des Programms bei Programmende. Die Datei stdlib.h definiert hierfür zwei Konstanten: EXIT_SUCCESS Das Programm wurde normal (erfolgreich) beendet. Unter Unix ist dieser Wert 0. EXIT_FAILURE Das Programm wurde aufgrund eines Fehlers beendet. Unter Unix kann dies eine beliebige, nichtverschindende Zahl sein. Die genaue Interpretation des Rückgabewertes ist nicht vorgeschrieben.
Inhalt Funktionen Die Funktion main Gültigkeitsbereiche Rekursive Funktionen Beispiel: Ein kleiner Taschenrechner
Gültigkeitsbereiche Variablen sind nicht global gültig, sondern nur in ihrem Gültigkeitsbereich (engl. scope). Der Gültigkeitsbereich einer (lokalen) Variablen ist der Block, in dem sie deklariert wurde. Außerhalb ihres Gültigkeitsbereiches ist die Variable nicht sichtbar und kann auch nicht verwendet werden. Beispiel: int s = 1; for( int i = 1; i <= 5; i = i + 1 ) int k = i*i; s = s + k; printf( "%d %d\n", s, k ); Fehler: k ist außerhalb des Schleifenrumpfes nicht mehr gültig.
Gültigkeitsbereiche Variablennamen können in jedem Block neu verwendet werden. In diesem Fall überdeckt die lokale Variable eine in einem umgebenden Block Gültigkeitsbereich (engl. scope). Beispiel: int s = 0; for( int i = 1; i <= 3; i = i + 1 ) int s = i*i; printf( "inner: %d\n", s ); printf( "outer: %d\n", s ); Ausgabe: inner: 1 inner: 4 inner: 9 outer: 0
Gültigkeitsbereich von Schleifenvariablen Beispiel: for( int i = 1; i <= 3; i = i + 1 ) printf( "inner: %d\n", s ); printf( "outer: %d\n", i ); Auch das ist falsch, denn die for-schleife ist äquivalent zu int i = 1; while( i <= 3 ) printf( "inner: %d\n", s ); i = i + 1; Also: Schleifenvariablen sind nur innerhalb der Schleife gültig.
Call-by-Value In C werden Argumente als Werte an Funktionen übergeben (call-by-value). Die Funktion kann sie nicht ändern. Beispiel: void initialize ( int a ) a = 0; int main () int a = 1; initialize( a ); printf( "a = %d\n", a ); /* a = 1 */ return 0; Die Funktion initialize besitzt eine Variable a, die mit dem Wert des Ausdrucks a initialisiert wird: 1. Offensichtlicher wird dies, wenn man beachtet, dass folgende Aufruf gültig ist: initialize( 1 ); Es gibt keinen Zusammenhang zwischen der Variablen a in initialize und der Variablen a in main.
Übergabe von Referenzen Manchmal möchte man aber Variablen beim Aufrufer verändern. In diesem Fall möchte man Referenzen übergeben (call-by-reference). In C muss man dies simulieren, indem man Zeiger auf die Variablen übergibt. Beispiel: void swap ( int *a, int *b ) int c = *a; *a = *b; *b = c; int main () int a = 1, b = 2; swap( &a, &b ); printf( "a = %d, b = %d\n", a, b ); /* a = 2, b = 1 */ return 0;
Übergabe von Feldern Eine Besonderheit stellen Felder als Argumente dar. Erinnerung: Ist a ein Feld vom Typ T, so ist der Ausdruck a vom Typ T. Beispiel: void f1 ( int x[] ); void f2 ( int x[ 5 ] ); void f3 ( int *x ); Alle drei Funktionsprototypen sind quivalent. Konsequenzen: Felder werden immer als Referenz übergeben. Die Anzahl der Feldelemente hat keinen Einfluss auf den Typ. Sie dient lediglich der Dokumentation.
Rückgabe von Feldern Funktionen können keine Felder zurückgeben. Lösungsmöglichkeiten: Das Feld für den Rückgabewert wird als Argument erwartet: void squares( int n, int s[] ) for( int i = 0; i < n; i = i + 1 ) s[ i ] = i*i; Man gibt einen Zeiger auf das Feld zurück (dynamische Speicherverwaltung). Man gibt einen benutzerdefinierten Datentyp zurück. Dynamische Speicherverwaltung und benutzerdefinierte Datentypen behandeln wir später.
Inhalt Funktionen Die Funktion main Gültigkeitsbereiche Rekursive Funktionen Beispiel: Ein kleiner Taschenrechner
Rekursive Funktionen Funktionen dürfen sich auch selber aufrufen. Damit lassen sich rekursive Algorithmen einfach implementieren. Beispiel: 1 double power ( double x, int n ) 2 3 if( n == 0 ) 4 return 1.0; 5 else 6 return x * power( x, n - 1 ); 7 Eine Rekursion braucht eine Abbruchbedingung (Zeile 2 3), sonst terminiert die Funktion nicht (vgl. Endlosschleife).
Rekursive Berechnung von Binomialkoeffizienten Die Formel ( ) n n! = k k! (n k)! ist für die Implementierung ungeeignet. Die Zahl n! wird schnell sehr groß: Bereits 13! = 6227020800 benötigt mehr als 32 Bit, um dargestellt zu werden. Hingegen ist ( ) 13 = 1716 relativ klein. 6 Die Binomialkoeffizienten lassen sich aber auch rekursiv berechnen. Für 0 < k < n gilt: ( ) ( ( ) n + 1 n n = +. k + 1 k) k + 1
Rekursive Berechnung von Binomialkoeffizienten Für 0 < k < n gilt: ( n = k) ( ) n 1 + k 1 ( ) n 1. k Beispiel: int binomial ( int n, int k ) if( (k == 0) (k == n) ) return 1; else return binomial( n-1, k-1 ) + binomial( n-1, k );
Divide and Conquer Klassisches Anwendungsfeld für Rekursion ist das Zerlegen großer Probleme in kleinere ( Teile und Herrsche ). Beispiel: /* let x be a sorted array */ /* find the position of i in the interval [begin,end) */ int binary_search ( int x[], int begin, int end, int i ) if( begin >= end ) /* empty interval */ return -1; const int mid = (end - begin) / 2; if( i == x[ mid ] ) return mid; if( i < x[ mid ] ) return binary_search( x, begin, mid ); else return binary_search( x, mid+1, end );
Endrekursion Ist der rekursive Aufruf die letzte (elementare) Instruktion, so sprechen wir von einer Endrekursion (engl. tail-recursion). Beispiel: void power_impl ( double x, int n, double *result ) if( n == 0 ) return; *result = *result * x; power_impl( x, n - 1, result ); double power ( double x, int n ) double result = 1.0; power_impl( x, n, &result ); return result;
Endrekursion Bei einer Endrekursion kann der rekursive Aufruf durch einen Sprung an den Funktionsanfang ersetzt werden. Beispiel: void power_impl ( double x, int n, double *result ) start: if( n == 0 ) goto end *result = *result * x; n = n - 1; goto start end: Dies ist äquivalent zu void power_impl ( double x, int n, double *result ) for( ; n!= 0; n = n - 1 ) *result = *result * x;
Endrekursion Eine Endrekursion ist damit äquivalent zu einer Schleife. Die Behauptung, Rekursion sei weniger effizient als eine Schleife ist daher i.a. falsch. Das Umwandeln einer Endrekursion in eine Schleife ist ein Beispiel für eine Compiler-Optimierung. Der übersetzte Code wird dadurch effizienter. Achtung: Unser ursprüngliches Beispiel ist nicht endrekursiv. double power ( double x, int n ) if( n == 0 ) return 1.0; else return x * power( x, n - 1 ); Hier muss nach dem Aufruf von power noch das Ergebnis mit x multipliziert werden.
Inhalt Funktionen Die Funktion main Gültigkeitsbereiche Rekursive Funktionen Beispiel: Ein kleiner Taschenrechner
Beispiel: Ein kleiner Taschenrechner Wir können jetzt bereits sehr mächtige Programme schreiben. Im folgenden soll ein kleiner Taschenrechner vorgestellt werden. Dieser liest einen Ausdrücke vom Benutzer, wertet diese aus und gibt das Ergebnis aus. Eigenschaften: Er beherrscht die Grundrechenarten. Er beachtet Punktrechnung vor Strichrechnung. Er bisitzt 26 Variablen (a z), die jederzeit zugewiesen werden können.
Grammatik Grammatik: expression assignexpr assignexpr, expression assignexpr letter = assignexpr addexpr addexpr mulexpr + addexpr mulexpr - addexpr mulexpr preexpr * mulexpr preexpr / mulexpr preexpr - preexpr + preexpr powexpr powexpr powexpr ^ terminal terminal terminal ( expression ) letter float mulexpr preexpr White Space soll ignoriert werden.
Eingabebeispiel $./calc 1+2*2^2 9.000000 a=4,b=3,a*b 12.000000 -(b-a)*(a+b) 7.000000 2^0.5 1.414214 i=1,j=1 1.000000 k=i,i=j,j=j+k 2.000000 k=i,i=j,j=j+k 3.000000 k=i,i=j,j=j+k 5.000000 k=i,i=j,j=j+k 8.000000 k=i,i=j,j=j+k 13.000000 $
Die Funktion main int main () double vars[ 26 ]; for( int i = 0; i < 26; i = i + 1 ) vars[ i ] = 0.0; while( true ) char line[ 256 ]; getline( line, sizeof( line ) ); if( line[ 0 ] == \0 ) return 0; double result; const char *end = evalexpr( line, vars, &result ); if( end[ 0 ]!= \0 ) printf( "Error: Trailing characters \"%s\".\n", end ); abort(); else printf( "%lf\n", result );
Die Funktion evalexpr Die Funktion evalexpr wertet einen Ausdruck aus. const char *evalexpr ( const char *s, double *vars, double *result ) s = evalassign( s, vars, result ); while( s[ 0 ] ==, ) s = evalassign( s+1, vars, result ); return s; Argumente: s Zeiger auf Anfang des auszuwertenden Ausdrucks vars Feld für die 26 Variablen result Zeiger auf Variable für das Ergebnis Rückgabewert: Zeiger auf erstes nicht ausgewertetes Zeichen Für jede Regel in der Grammatik gibt es eine solche Funktion.
Die Funktion evalterminal const char *evalterminal ( const char *s, double *vars, double *result ) if( s[ 0 ] == ( ) s = evalexpr( s+1, vars, result ); if( s[ 0 ]!= ) ) printf( "Error: \ )\ expected.\n" ); abort(); else return s+1; else if( (s[ 0 ] >= a ) && (s[ 0 ] <= z ) ) *result = vars[ s[ 0 ] - a ]; return s+1; else char *end; *result = strtod( s, &end ); if( end == s ) printf( "Error: Terminal expression expected.\n" ); abort(); return end;
Zwei weitere Funktionen als Beispiel const char *evalpowexpr ( const char *s, double *vars, double *result ) s = evalterminal( s, vars, result ); while( s[ 0 ] == ^ ) const double base = *result; s = evalterminal( s+1, vars, result ); *result = pow( base, *result ); return s; const char *evalmulexpr ( const char *s, double *vars, double *result ) s = evalprefixexpr( s, vars, result ); while( (s[ 0 ] == * ) (s[ 0 ] == / ) ) const double v = *result; const char op = s[ 0 ]; s = evalprefixexpr( s+1, vars, result ); if( op == * ) *result = v * (*result); else *result = v / (*result); return s;
Autoren Autoren die an diesem Skript mitgewirkt haben: 2011 2014 : Christoph Gersbacher 2014 2015 : Patrick Schön 2016 : Martin Nolte This work is licensed under a Creative Commons Attribution- ShareAlike 4.0 International (CC BY-SA 4.0) License. http://creativecommons.org/licenses/by-sa/4.0/legalcode