ThInf-HOME

  1. Einführung

  2. Funktionen mathematisch

  3. Funktionen in C

    1. Wie man sie definiert

    2. Bsp-1: Zahlkonvertierer

    3. Definieren, Prototypen, Aufrufen

    4. Management von Funktionen

  4. Zeiger/ Pointer in C

  5. Zeiger als Argument von Funktionen

  6. Übungsaufgaben


I-PROGRAMMIEREN1 WS 0203 - Vorlesung mit Übung
VL4: Funktionen und Zeiger


AUTHOR: Gerd Döben-Henisch
DATE OF FIRST GENERATION: Oct-21, 2002
DATE OF LAST CHANGE: Oct-21, 2002
EMAIL: Gerd Döben-Henisch



1. Einführung

Ausgehend vom mathematischen Begriff der Funktion wird gezeigt, dass C-Programme aus Funktionen bestehen. Es wird erläutert, wie man eine Funktion definiert, wie man sie aufruft und wie man Funktionsprototypen verwendet. Fermer wird ansatzweise gezeigt, wie man im Falle von mehreren Funktionen diese auf mehrere Dateien verteilt. Es wird dann das Konzept des Zeigers (engl. 'Pointers') eingeführt und speziell erklärt, wie man Zeiger auch als Argument von Funktionen verwenden kann. Zeiger als Argumente von Funktionen machen im Prinzip sogenannte globale Variablen überflüssig.



START

2. Funktionen mathematisch

Mengentheoretisch ist eine Funktion F eine Relation F C A x B mit der zusätzlichen Eigenschaft der Rechtseindeutigkeit, d.h. für jedes Element a in A gilt, dass es genau ein b in B gibt, das dem Element a zugeordnet ist. F(a) ist von daher eindeutig bestimmt als F(a) = b oder auch b = F(a); 'b' ist dann der Wert der Funktion F für das Argument a. Man kann also F auffassen als eine Menge geordneter Paare F = {(a,b), ...}. Die Gesamtheit dieser Paare definiert eine Zuordnungs- bzw. Abbildungsvorschrift. Die Mengen A und B können dabei intern von höherer Komplexität sein, z.B. F C An x Bm. Dann würde ein Element p aus F die Form haben p = (<a1, .., an >,<b1, .., bm > ). Meistens schreibt man eine Funktion hin in der Form:

F: An ---> Bm

und man benutzt eine Funktion in der Form

F(<a1, .., an >) = <b1, .., bm > mit <a1, .., an > in An und <b1, .., bm > in Bm

Definiert man eine neue Funktion, z.B.

f(x,y) = |x-y|

dann führt man im Falle einer Definition durch Gleichheit einen neuen Funktionsnamen 'f' ein (eine Funktionskonstante), mit den beiden Argumenten 'x,y' (das Definiendum), und schreibt rechts vom Gleichheitszeichen einen Ausdruck hin, der als schon definiert gilt (das Definiens), z.B. den absoluten Wert einer Subtraktion von x-y, wenn x,y ganze Zahlen sind. Wichtig ist hier, dass der potentielle Anwender der Funktionsdefinition weiss, wie der definierende Ausdruck '|x-y|' als 'Regel' bzw. als konkrete 'Zuordnungsvorschrift' zu interpretieren ist. Nur dann, wenn der Anwender weiss, wie er vorzugehen hat, um für beliebig vorgegebene x und y zu 'berechnen', wie der endgültige Funktionswert f(x,y) aussieht, nur dann macht diese Schreibweise Sinn.


START

3. Funktionen in C

In der Sprache C kann man sowohl vorgegebene Funktionen benutzen als auch selbst Funktionen neu vereinbaren.



3.i Wie man sie definiert

Eine Funktion, die wir schon gleich zu Beginn kennengelernt haben, war die Funktion 'main':

int main(void){ Anweisung1 ... Anweisungn }  

oder auch in der mathematischen Schreibweise:

main: void ---> int
 

d.h. die Funktion main ist eine Abbildung der leeren Menge auf die Menge der ganzen Zahlen 'int'.

'main' ist der Funktionsname, das 'int' links vor main ist die Angabe des Typs, den die Rückgabewerte der Funktion main haben, das 'void' gab an, dass die Funktion main keine Argumente hat, und die Anweisungen zwischen den geschweiften Klammern legten fest, was die Funktion main tut.

int main(int argc, char *argv[]){ Anweisung1 ... Anweisungn }  

main: int x char* ---> int
 

d.h. die Funktion main ist eine Abbildung vom produkt aus der Menge der ganzen Zahlen 'int' und der Menge der Zeiger vom Typ char 'char*' auf die Menge der ganzen Zahlen 'int'.

In dieser Variante der Funktion kann man der Funktion main zwei Argumente übergeben, wobei festgelegt ist, welchen Typ diese Argumente haben müssen.

Ganz allgemein hat eine Funktionsdefinition in C das folgende Format:

FTYPE f_name(PTYPE p_1; ...) {CMD_1; ...; CMD_n; }

f_name ist der Name der neuen Funktion

FTYPE gibt den Typ des Wertes (=Rückgabewert) an, den die Funktion f_name berechnet.

p_i sind die Namen der Argumente (Parameter), die der Funktion zur Berechnung übergeben werden.

P_TYPE ist derTyp eines Argumentes (Parameters).

CMD_i ist eine schon bekannter mathematischer Ausdruck (Expression), der für die Berechnungen mittels der Funktion f_name benutzt wird. Man beachte: Anweisung := Ausdruck;, d.h. eine Anweisung ist ein Ausdruck gefolgt von einem Semikolon.

Eine neue Definition zu vereinbaren bzw. zu definieren, ist also einfach: man entscheidet sich für einen Namen, der noch nicht vorkommt, legt fest, welche Argumente einer Funktion zur Bearbeitung übergeben werden sollen, und schreibt dann eine Folge von Anweisungen, die nur solche Funktionen benutzen, die zum Zeitpunkt der Definition der neuen Funktion schon beannt sind. Statt von einem 'Funktionsnamen' spricht man in der SW-Literatur oft auch von einem 'Operator'. Z.B. ist '+' das Zeichen für den arithmetischen Operator, der 'addiert', '==' das zeichen für den Operator, mit dem man zwei ausdrücke vergleichen kann, 'if' das zeichen für den Operator, mit dem man eine Unterscheidung abfragen kann, usw.


FUNKTIONSDEFINITION

FUNKTIONEN



START

3.ii Bsp-1: Zahlkonvertierer

Wir betrachten jetzt nochmals das Beispiel der Zahlenkonvertierung aus der vorhergehenden Vorlesung und schreiben jetzt eine kleine Funktion mit Namen 'conv10()', die Zahlen mit der Basis 10 in Zahlen mit der Basis '2', '8' oder '16' konvertiert.

Mathematisch würde die Funktion wie folgt typisiert werden:

 conv10: int x int ---> void
 

Die Funktion 'conv10' bekommt zwei Argumente vom Typ 'int' und erzeugt keinen erkennbaren Wert, da diese Funktion als C-Funktion das Ergebnis auf den Bildschirm druckt.

Die Funktion conv10() berücksichtigt nur die drei Zielbasen '2', '8' und '16', alle anderen werden ignoriert. Für die Konvertierung in das octale oder hexadezimale Zahlensystem wird zurückgegriffen auf die Eigenschaften der C-Funktion 'printf', die es nämlich erlaubt, den Wert einer int-Zahl entweder oktal auszugeben (Konversion '%o') oder hexadezimal (Konversion '%x'). Nur im Fall einer Konversion zur Basis '2' wird eine eigene Konvertierung vorgenommen.

 
/************************************
 *
 * conv10()
 *
 * author: gdh
 * first: oct-21, 02
 * last:
 * idea: convert a number 'zahl' with base=10 into a number 'ziel'
 *       with base = 'base'. For the cases base= '8' and 'x' we use
 *       the built-functionality of the C-function 'printf'
 *
 ************************************************************/

void  conv10(int zahl, int basis){

  int i, x,  zaehler;
int ziel[65];        /* Storage of the number-letters of the new number */

 switch(basis){

 case '2':{

   x = zahl;
     zaehler = 0;
    while (x>0)  {
       ziel[zaehler] = x % 2;
       x /= 2;
       ++zaehler;
       }/* End of while(x>0) */

    printf("\nDEC %d = DUAL ",zahl);

for (i=zaehler-1 ; i>=0 ; i--)   /* print the new number from 'left to right' */
      { printf("%d", ziel[i]);}
    break;}

 case '8':{ printf("DEC %d = OCTAL%o \n",zahl, zahl); break;}

 case 'x':{ printf("DEC %d = HEXA %x \n",zahl, zahl); break;}

 default: {printf("FEHLER; keine Konversion verfuegbar!!!\n"); break;}

 }/* End of switch(basis) */

}

Wenn man auf diese Weise eine neue Funktion conv10() definiert hat, stellt sich die Frage, wie man sie überhaupt benutzen kann.


START

3.iii Definieren, Prototypen, Aufrufen

Man kann theoretisch beliebig viele Funktionen neu definieren. Diese Funktionen werden aber erst wirksam, wenn man sie auch im Rahmen einer anderen schon vereinbarten Funktion (mindestens im Rahmen von main()), aufruft. Einer Aufruf hat immer die Form:

Funktions-Name(Arg_1, ..., Arg_n);

Das Argument, das man beim Aufruf hinschreibt ist dann entweder eine Konstante oder eine Variable von dem Typ, der an dieser Stelle im Rahmen der Definition der Funktion festgelegt wurde. Es kann auch ein komplexer Ausdruck sein, sofern dieser einen konkreten Wert erzeugt, der mit dem geforderten Typ des Argumentes übereinstimmt. Folgendes wären mögliche Aufrufe der Funktion conv10():

 conv10(23,2);
 conv10(25+7,8);
 conv10(25+8-2, 4*2);
 conv10(zahl, basis);

Damit solch ein Funktions-Aufruf in einer anderen Funktion wirksam wird, muss man sicherstellen, dass diese andere aufrufende Funktion (z.B. main()) von der neuen Funktion überhaupt Kenntnis besitzt und weiss, wo die Definition dieser Funktion zu finden ist.

Der einfachste Fall ist der, dass man in der aufrufenden Funktion schreibt:

 extern void conv10(int, int);

Damit wüsste die aufrufende Funktion, dass die Funktion 'conv10' eine Funktion ist, die ausserhalb der aufrufenden Funktion definiert ist und welchen Typ die Argumente und der Funktionswert haben. Einen Ausdruck der Form 'void conv10(int, int);' nennt man einen Funktions-Prototypen.

Damit sind alle Zutaten aufgeführt, die man benötigt, um eine vereinbarte Funktion auch in einer anderen Funktion zu nutzen. Hier ein einfaches Beispiel. In der Datei 'conv10.c' gibt es die Funktion main(), innerhalb deren es sowohl einen Verweis auf die externe Funktion conv10() gibt wie auch einen Aufruf der Funktion conv10().

  /*****************************
 *
 * conv10.c
 *
 * author: gdh
 * first: oct-21, 02
 * last:
 * idea: demonstrate the use of a new function 'conv10()' in one file
 * compilation: gcc -o conv10 conv10.c
 * usage: conv10
 *
 *********************************************/

#include <stdio.h>

int main(void){

  extern void conv10(int, int);  /* Prototype of new function */

  int  basis, zahl;    /* 'zahl' soll zur Basis 'basis' konvertiert werden */
  basis = 0;

  while(basis != 48){   /* Eingabeschleife */

  printf("\n\nKONVERSION EINER DEZIMALZAHL NACH \n");
printf("-> DUAL = 2 \n");
printf("-> OCTAL = 8 \n");
printf("-> HEXADEZIMAL = x \n");
printf("-> ENDE = 0 \n");
 printf("----------------------\n");

 basis = getchar();
 getchar();               /* Herausfiltern von LF = \n */

 printf("Ihre Wahl = %d = %c \n", basis, basis);

 if(basis == 48){ break;}   /* Ende */
 if(basis != '2' && basis != '8' && basis != 'x' ){printf("FEHLER; keine Konversion verfuegbar!!!\n"); break;}

 printf("\nBITTE DEC-ZAHL FUER KONVERSION: ");
 scanf("%d", &zahl);
 getchar();                /* Herausfiltern von LF = \n */

 printf("\nIhre Zahl = %d  \n", zahl);

 conv10(zahl, basis);      /* Aufruf der Funktion conv10() */


  }/* End of while basis == 0 */

 return(0);
}



START

3.iv Management von Funktionen

Im vorausgehenden Beispiel wurde angenommen, dass die Funktionsdefinition sich in der gleichen Datei befindet wie die aufrufende Datei, in diesem Fall main(). Bei grösseren Softwareprojekten ist dies aber unrealistisch. In diesen Fällen organisiert man die Funktionen in einer Weise, die im folgenden Bild skizziert ist.


files

Organisation der Dateien (erste Fassung)


Man verlagert die Definition der neuen Funktionen in eigene Dateien (im Bild quellen_1.c, ..., quellen_k.c). Die Prototypdefinitionen fasst man in einer eigenen Headerdatei (im Bild proto.h) zusammen. Unter Umständen können dies auch mehrere als Dateien sein. In der Hauptdatei mit main(), in der man dann die Funktionen benutzt und aufruft, hat man dann mindestens ein spezielles include #include "proto.h", mit dem man dann die Headerdatei mit den Prototypen einlädt.

Es sei angemerkt, dass auch solch eine Multi-Datei-Organisation noch sehr vereinfachend ist; später mehr dazu.


START

4. Zeiger/ Pointer in C



Pointer sind eines der mächtigsten Werkzeuge von C, um den Inhalt von Variablen und Speicherinhalten manipulieren zu können. Hier sei nur kurz die Grundidee von Pointern vorgestellt; eine ausführlichere Behandlung erfolgt in einer späteren Sitzung.

Wenn in ISO-C eine Variable deklariert wird, z.B. int a;, dann wird einem Variablennamen 'a' intern ein Speicherbereich zugewiessen, der gross genug ist, um Werte des jeweiligen Typs (bei int a vom Typ int) in diesem reservierten Speicherbereich ablegen zu können. Mit einer expliziten Wertzuweisung, wie z.B. a=3;, kann man dann unter dem Namen a den Wert 3 in den Speicherbereich schreiben. Speicherbereiche sind in elementare Speicherzellen aufgeteilt und jede Speicherzelle hat eine eindeutige Adresse. Um zu wissen, wie die Adresse einer Variablen genau lautet, kann man dies mittels des Adressoperators & errechnen lassen, z.B. x =&a;. Um der Variablen x als Wert eine Adresse zuweisen zu können, muss x ein Pointer vom geeigneten Typ sein, also int *x; Hat man der Pointervariablen x mittels des Adressoperators '&' die Adresse der Variablen a übergeben, dann kann man mit dem Inhaltsoperator '*' nun den Inhalt von a über den Umweg von x manipulieren, z.B. durch *x=*x+5;. Nach dieser Operation hat a dann den Wert 3+5=8!



POINTER I

Zeiger/ Pointer


START

5. Zeiger als Argument von Funktionen

Wenn man eine Funktion f(x) definiert, dann kann man mittels x einen Wert an die Funktion f übergeben. In ISO-C gibt es hierfür zwei unterschiedliche Strategien: call-by-value und call-by-address (oder call-by-reference).

call-by-value

Angenommen im aufrufenden Programm (= haupt.c) sei einer Variablen a der Wert 3 zugewiessen worden: int a=3;. Nehmen wir ferner folgende Defintion von f an: int f(int x){...}. Wird jetzt die Funktion f aufgerufen mit f(a);, dann wird der Funktion f eine Kopie des Wertes von a (also '3') übergeben. Was auch immer die Funktion f nun mit diesem Wert tut, der ursprüngliche Wert von a bleibt unverändert.

Möchte man, dass der ursprüngliche Wert von a durch die Funktion f verändert wird, dann muss man direkt die Adresse von a übergeben:



call-by-address (call-by-reference)

Zu diesem Zweck muss man den Übergabeparameter von f als einen Pointer deklarieren: int f(int *x){...}. Bei der Übergabe des Wertes an die Funktion f übergibt man dann die Adresse von a: f(&a);. Damit ist dann die Funktion f in der Lage, den Inhalt der Adresse von a zu manipulieren, in dem sie mit dem Inhaltsoperator * direkt auf den Inhalt der Adresse zugreift, z.B. durch *a=*a+5;


FUNKTION-CALL-BY

CALL BY VALUE/ CALL BY ADDRESS


START

6. Übungsaufgaben

  1. Bilden sie ein Team von 3 Mitgliedern

  2. Erstellung Sie gemeinsam einen Texte mit Namen und Matr.Nr. der AutorenInnen. Abgabe einer Kopie des Textes an den Dozenten vor Beginn der Vorlesung am Di (1 Woche nach Aufgabenstellung). Falls die Aufgabe ausführbaren Programmcode enthält wäre die zusätzliche Übergabe des Quelltextes auf Diskette oder per email wünschensert, aber nicht verbindlich. Im Falle von Programmcode muss im Programmtext auch nochmals Name und Matr.Nr. erscheinen.

  3. Präsentation der Lösung als Team vor der gesamten Gruppe. Präsentationszeit (abhängig von der Gesamtzahl der Teams) 3-5 Min. Mögliche Punkte: 1-2 im Normalfall. Bei besonders guten Leistungen bis zu 3 Punkten.

  4. Versuchen Sie in dem Text Antworten auf folgende Aufgaben zu formulieren:

  5. Schreiben sie ein kleines Programm, das folgende Anforderungen erfüllt:


    1. Es gibt eine Datenstruktur mit Namen 'Stack' (oder auch 'Stapel', 'Keller', 'LIFO := Last in First Out'), die folgende Funktionsaufrufe erlaubt:

      • 'push(...)' := Lege ein Element von einem beliebigen Typ auf die oberste Position des Stacks;

      • 'pop(...)' := Hole das oberste Element des Stacks und gib es aus;

      • 'view(...)' := Zeige den aktuellen Inhalt des Stacks auf dem Bildschirm;

    2. Schreiben Sie ein Rahmenprogramm, innerhalb dessen der Stack mit den genannten Funktionen benutzt wird.


START