Das ATOS-Magazin
 
  zurück zum News-Archiv
Anfang zurück vorwärts Ende 

ANSI C - Ein Programmierkurs - Teil VIII

Parameter verändern

In C sind sämtliche Parameter Kopien. Eine Funktion kann deshalb nicht einen Parameter so ändern, dass der Aufrufer von dieser Änderung Notiz nimmt.

#include <stdio.h>
 
void Rechne(int a)
{
   a = 10 * a;
}

int main(void)
{  int i = 3;

   printf("i vor dem Aufruf: %d\n",i);
   Rechne(i);
   printf("i nach dem Aufruf: %d\n",i);
   return 0;
}

Sollen die Parameter verändert werden, z.B. weil es Strukturen sind, die als Returnwerte nicht zulässig sind, muss ein Zeiger auf diesen Datentyp übergeben werden:

#include <stdio.h>

void Rechne(int *a)
{
   *a = 10 * (*a);
}

int main(void)
{  int i = 3;

   printf("i vor dem Aufruf: %d\n",i);
   Rechne(&i);
   printf("i nach dem Aufruf: %d\n",i);
   return 0;
}

Man beachte, dass jetzt in dem Aufruf natürlich auch der Adressoperator benutzt wird, um die Adresse der Variablen zu ermitteln. Da der Name eines Feldes der Adresse des ersten Elements entspricht, können Elemente von Feldern auch verändert werden. Ein Beispiel dazu findet sich im Kapitel über offene Felder.

const-Parameter

Um anzuzeigen, dass übergebene Felder oder die Variable, auf die ein Pointer zeigt, nicht verändert werden, kann man das Attribut const benutzen:

int string_laenge(const char p[])
{  int i=0;

   while (p[i] != 0)
      i++;
   return i+1;
}

int main(void)
{  int i;

   i = string_laenge("Hallo");
}

Im obigen Beispiel wird der Funktion eine Zeichenkettenkonstante als Parameter übergeben. Die Übergabe von Konstanten an Funktionen, die den Parameter ändern, liefert unvorhersagbare Ergebnisse. Das Attribut const erlaubt es festzustellen, ob eine Funktion auch mit Konstanten als Parameter keine Probleme bereitet.

Alte Deklarationsformen

Es ist möglich, den Typ der Funktion und die Parameter nicht anzugeben. Der Compiler geht dann davon aus, dass die Funktion einen int zurückliefert. Der Compiler ist aber nicht mehr in der Lage, zu prüfen, ob die Funktion mit den richtigen Parametern aufgerufen wird. Werden in dem Prototypen Parameter und Typ der Funktion weggelassen, wird damit auch das Verständnis des Programms erschwert. Denn nun muss man die Definition der Funktion suchen, um Informationen über Parameter zu bekommen.

a()
{
   return 3;
}

int main(void)
{  int i;

   i = a();
   return 0;
}

Es ist zwar auch möglich, die Parameter in den Klammern nur mit Namen, getrennt durch Kommata, anzugeben und darunter die Parameter nochmals mit ihrem Datentyp aufzuführen. Diese Vorgehensweise ist aber ein Überbleibsel von K&R C und sollte nicht mehr benutzt werden.

int a(x, y)
int x;
int y;
{
   return x+y;
}

Rekursionen

Eine Funktion kann sich selbst wieder aufrufen. Man spricht dann von einer Rekursion. Jede Rekursion benötigt eine Abbruchbedingung, in der sie sich nicht selbst aufruft. Andernfalls erhält man eine Endlosrekursion.

Ein klassisches Beispiel für eine Rekursion ist die Berechnung der Fakultät einer Zahl, da die Definition der Fakultät rekursiv ist. Allerdings lässt sich die Fakultät nichtrekursiv schneller berechnen. Die Fakultät von 1 (geschrieben 1!) ist definiert als 1. Die Fakultät einer beliebigen Zahl n ist diese Zahl multipliziert mit der Fakultät der um eins kleineren Zahl. Oder mathematisch formuliert: n! = n * (n-1)!

int fakultaet(int x)
{
   if (x == 1)
      return 1;
   else
      return x * fakultaet(x - 1);
}

Achtung! Die Fakultät erreicht sehr schnell große Werte, so dass man mit einem 16-Bit-int-Wert schnell an die Grenzen stoßen kann. Außerdem prüft obige Funktion nicht, ob der Parameter auch positiv ist. Machen Sie sich ruhig den Programmlauf klar, indem Sie Ausgaben einfügen und die Funktion in main aufrufen.

Oder versuchen Sie sich an den Türmen von Hanoi. Hierbei haben Sie 3 Stäbe, auf die Sie Scheiben aufschieben können. Jede Scheibe kann nur auf einen leeren Stab oder auf einen Stab mit einer größeren Scheibe geschoben werden. Wie verschiebt man jetzt den Turm von Scheiben auf einen anderen Stab? Auch dieses Problem kann man rekursiv lösen. Man nimmt einfach den Turm aus allen Scheiben bis auf die letzte, schiebt ihn auf den Stab, wo später der Turm nicht hin soll. Dann schiebt man die letzte Scheibe auf den Zielstab und anschließend den geparkten Turm auf diese Scheibe. Der kleinere Turm wird natürlich auch wieder mit dem gleichen Algorithmus verschoben. Erst wenn man einen Turm aus nur einer Scheibe hat, ist die Abbruchbedingung erreicht. Diese Scheibe kann direkt verschoben werden.

Funktionspointer

Auch für Funktionen können Zeiger auf Funktionen definiert werden. Und über diese Zeiger können die Funktionen auch wieder aufgerufen werden. Da hier genau auf die Auswertungsreihenfolge der Operatoren geachtet werden muss, ist den Funktionspointern ein eigenes kleines Kapitel gewidmet. Wir erinnern uns an die Zeiger:

int *a;

a ist ein Zeiger auf ein int.

int * d(void);

Da die Klammern eine höhere Priorität haben, ist dies eine Funktion d mit dem Returnwert int *. Damit wir einen Funktionspointer haben, muss also der * an den Namen der Funktion gebunden werden. Also setzen wir Klammern:

int (*d)(void);

Dies ist jetzt ein Zeiger d vom Typ Funktion ohne Parameter mit einem Returnwert int. Wenn wir den Zeiger verwenden wollen, muss er wieder dereferenziert werden. Da auch hier der * eine geringere Priorität als die Klammern für den Parameter haben, muss die Dereferenzierung geklammert werden.

Die Adresse einer Funktion bekommt man, analog der Adresse von Feldern, einfach durch den Namen ohne Klammern:

#include <stdio.h>

int a(int x,int y)
{
   return x+y;
}

int main(void)
{  int (*p)(int,int);

   p = a;
   printf("Summe = %d\n",(*p)(3,4));
   return 0;
}

Funktionspointer lassen sich immer dann verwenden, wenn ein Algorithmus unabhängig von einem konkreten Datentyp formuliert werden soll. Ein Sortieralgorithmus beispielsweise muss die Daten vergleichen; er benötigt also eine Vergleichsfunktion. Ein Beispiel findet sich in der ANSI Lib. Wer einmal in die Datei stdlib.h nachschaut, findet dort die QuickSort-Funktion (qsort):

void    qsort( void *base, size_t nmemb, size_t size,
          int (*compar)() );

Oder ein Zeichenprogramm verwaltet grafische Objekte und zu jedem Objekt gehört eine passende Zeichenfunktion, die das Objekt malen kann. Wenn als Zeiger auf die eigentlichen Objektdaten ein typloser Zeiger benutzt wird, muss die Verwaltung keine Kenntnisse der Objekte haben. Wir nähern uns damit schon den Möglichkeiten der objektorientierten Programmierung, wenn wir unterschiedliche Daten mit der gleichen Schnittstelle behandeln können.

Cookie-Jar

Und zum Schluss nochmals zu unserem Cookie-Jar. Wir können jetzt einige Teile auch in Funktionen auslagern.

#ifdef __TURBOC__
#include <tos.h>
#else
#ifdef __GNUC__
#include <osbind.h>
#else
#include <tosbind.h>
#endif
#endif
#include <stdio.h>

typedef struct cookie_entry {
   union {
      unsigned long name_long;
      char name_array[4];
   } name;
   unsigned long value;
} CookieEntry;

CookieEntry *GetCookieJar(void)
{  long OldStack;
   CookieEntry *CookieJar;

   OldStack = Super(0L);
   CookieJar = *((CookieEntry**)0x5a0L);
   Super((void *)OldStack);
   return CookieJar;
}

void PrintCookie(CookieEntry *Cookie)
{
   printf("Name des Cookies: %d\n", Cookie->name.name_long);
   printf("Wert des Cookies: %d\n", Cookie->value);
}

int IsNullCookie(CookieEntry *Cookie)
{
   return (Cookie->name.name_long == 0);
}

void TraverseCookieJar(CookieEntry *Cookie)
{
   while (!IsNullCookie(Cookie))
   {
      PrintCookie(Cookie);
      Cookie++;
   }
}

int main(void)
{  CookieEntry *CookieJar;

   CookieJar = GetCookieJar();
   if (CookieJar == (CookieEntry *)0)
      printf("Dieses System hat keinen Cookie Jar\n");
   else
      TraverseCookieJar(CookieJar);
   return 0;
}

Was haben wir jetzt gewonnen, ausser mehr Code? Zum einen können die Funktionen an anderer Stelle wiederverwendet werden - auch in einem anderen Programmen, wie wir im Kapitel über modulares Kompilieren sehen werden. Zum anderen wird das Programm leichter verständlich. Denn jede Teilaufgabe ist in eine Funktion verschoben worden, die ihre Aufgabe auch als Namen trägt. Und diese Vorgehensweise erlaubt es, ein Programm nicht sofort komplett verstehen oder auch programmieren zu müssen. Zuerst nimmt man sich main vor. Wir ermitteln den Cookie-Jar, und wenn wir einen Zeiger darauf bekommen, durchwandern wir ihn. Eigentlich doch ganz einfach. Und dann können wir uns einer der beiden Funktionen zuwenden und sie weiter analysieren. Bei der Entwicklung wird diese Vorgehensweise top down (von oben nach unten) genannt. Wir zerlegen das große Problem in kleinere Teilprobleme, die wir nacheinander lösen. Diese Teilprobleme kann man wieder weiter zerlegen. Dies führt man solange durch, bis sich das Teilproblem leicht lösen lässt.

Der folgende Teil beschäftigt sich mit dem Stack.

Michael Bernstein


Anfang zurück vorwärts Ende  Seitenanfang

Copyright und alle Rechte beim ATOS-Magazin. Nachdruck und Veröffentlichung von Inhalten nur mit schriftlicher Zustimmung der Redaktion.
Impressum - Rückmeldung via Mail oder Formular - Nachricht an webmaster