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

ANSI C - Ein Programmierkurs - Teil VII

Kontrollstrukturen

Bis jetzt ist es nur möglich, Anweisungen der Reihe nach abzuarbeiten. Für etwas komplexere Programme ist es aber erforderlich, den Ablauf des Programms noch weiter zu kontrollieren. Dazu gibt es Möglichkeiten, Anweisungen in Abhängigkeit von Bedingungen auszuführen und auch Anweisungen kontrolliert zu wiederholen.

Spielen Sie ruhig etwas mit den folgenden Beispielen herum. Setzen Sie sie in main ein, deklarieren benutzte Variable, falls die Deklarationen noch nicht vorhanden sind und setzen Sie sie auf sinnvolle Werte. Das sind zum einen die Werte, die abgefragt werden, das sind aber auch andere Werte für den Fehlerfall. Erst durch solche Übungen kommt die nötige Anschauung.

if

Die if-Anweisung dient dazu, in Abhängigkeit von einer Bedingung Anweisungen auszuführen. Zusätzlich können mit einem else-Befehl Anweisungen für den Nicht-if-Fall ausgeführt werden. Die Bedingung wird in Klammern hinter dem if aufgeführt und ist ein Ausdruck, der 0 oder ungleich 0 ist. Ist der Ausdruck ungleich 0, wird die dahinter stehende Anweisung bzw. der dahinter stehende Block ausgeführt. Wir erinnern uns, in C gibt es keinen logischen Datentyp.

Und damit das etwas anschaulicher wird, hier einige Beispiele:

...
if (i!=0) printf("i ist nicht 0");

if (i)
   printf("i ist nicht 0");

if (i==0)
   printf("i ist 0");

if (!i)
   printf("i ist 0");

if (i==0)
   printf("i ist 0");
else
   printf("i ist nicht 0");

if (i!=0)
{
   printf("i ist nicht 0");
   i=0;
}
 
if (i=3)
   printf("i ist nicht 0");

Was machen jetzt die einzelnen Beispiele?

Das erste Beispiel prüft, ob i ungleich 0 ist und gibt dann einen Text aus.

Das zweite Beispiel macht genau das Gleiche wie das erste. Allerdings wird hier ausgenutzt, dass ein Wert ungleich 0 schon einem true entspricht. Wenn die Variable aber nicht schon für logische Werte benutzt wird, sollte der Vergleich wie im ersten Beispiel geschrieben werden. Dies erleichtert es, das Programm zu verstehen. Die Anweisung wurde in die nächste Zeile geschrieben und eingerückt. Auch wenn häufig in anderen Programmen die Anweisung in die gleiche Zeile geschrieben wird, wie im Beispiel 1, sollte dem Einrücken der Vorzug gegeben werden. Damit ist die Anweisung leichter erkennbar. Dies gilt erst recht, wenn es sich um eine leere Anweisung mit lediglich einem Semikolon handelt.

Das dritte Beispiel prüft auf Gleichheit mit 0.

Und das vierte Beispiel zeigt eine kürzere Form. Ein Wert von 0 entspricht einem false und wird negiert zu einem true.

Das fünfte Beispiel zeigt die Anwendung von else. Wenn i den Wert 0 hat, die Bedingung also zutrifft, wird die erste Ausgabe gemacht, im anderen Fall die zweite Ausgabe nach dem Schlüsselwort else. Ist im else-Fall wieder eine if-Abfrage nötig, werden üblicherweise die Blöcke nicht weiter eingerückt, sondern das if direkt hinter das else geschrieben. Dies erhöht die Übersichtlichkeit solcher Abfragen beträchtlich.

...
/* if mit Einrückung */
if (i==3)
   printf("i ist 3");
else
{
   if (i==4)
      printf("i ist 4");
   else
      if (i==5)
         printf("i ist 5");
}
 
/* Die übersichtliche Einrückung */
if (i==3)
   printf("i ist 3");
else if (i==4)
   printf("i ist 4");
else if (i==5)
   printf("i ist 5");

Das sechste Beispiel hat keine Anweisung für den if-Fall, sondern einen Block.

Das siebte Beispiel enthält eine Zuweisung als Ausdruck. Das Ergebnis der Zuweisung ist auch das Ergebnis des Ausdrucks und kann deshalb auch als Bedingung benutzt werden. Solche Abkürzungen sollten aber vermieden werden, da die Zuweisung so etwas versteckt und damit das Programm schwerer zu verstehen ist. Deshalb geben viele Compiler an dieser Stelle eine Warnung aus. Achtung! Wird versehentlich bei einem Vergleich auf Gleichheit ein Gleichzeichen vergessen, kann der Ausdruck stattdessen eine Zuweisung ergeben und damit einen schwer zu findenden Fehler.

Da das else bei geschachtelten if-Anweisungen zu dem letzten if gehört, muss hier unter Umständen mit geschweiften Klammern gearbeitet werden! Auch dazu ein Beispiel, hier soll das else zu dem ersten if gehören:

...
/* Falsch */
if (i==3)
   if (j==5)
      x=3;
else
   x=1;

/* Das entspricht */
if (i==3)
{
   if (j==5)
      x=3;
   else
      x= 1;
}

/* Gewollt */
if (i==3)
{
   if (j==5)
      x=3;
}
else
   x=1;

Die if-Anweisung bauen wir auch sofort in unser Cookie-Programm ein, denn wir müssen ja prüfen, ob die Systemvariable überhaupt einen Zeiger auf den Cookie-Jar enthält.

#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;

int main(void)
{  CookieEntry **CookieJarPtr, *CookieJar;
   long OldStack;

   OldStack=Super(0L);
   CookieJarPtr = (CookieEntry**)0x5a0L;
   CookieJar=*CookieJarPtr;
   Super((void *)OldStack);
   if (CookieJar == (CookieEntry *)0)
      printf("Dieses System hat keinen Cookie Jar\n");
   else
   {
      /* Hier können wir jetzt die Cookies ausgeben */
   }
   return 0;
}

switch

Soll eine Variable oder das Ergebnis eines Ausdrucks mit Konstanten verglichen werden, kann hierzu die switch-Anweisung benutzt werden. Nach dem Schlüsselwort switch folgt in Klammern der Ausdruck, darunter innerhalb geschweifter Klammern die Konstanten. Jeder Konstante geht ein case voraus und nach der Konstanten folgt ein Doppelpunkt und die Anweisungen für diese Konstante. Das sieht dann so aus:

...
switch (i)
{
   case 3:
      printf("i ist 3");
      break;
   case 4:
      printf("i ist 4");
   case 5:
      printf("i ist 5");
      break;
   default:
      printf("nix passt");
}

Sobald das erste case mit einer passenden Konstanten gefunden wird, werden die dahinter stehenden Anweisungen ausgeführt. Ein weiteres case führt nicht zu einem Abbruch der switch-Anweisung. Dies muss explizit mit einem break gemacht werden. Da es in der Regel nicht erwünscht ist, wenn die Anweisungen des darunter liegenden case ausgeführt werden, sollte es kommentiert werden, wenn es erwünscht ist.

...
switch (i)
{
   case 3:
      printf("i ist 3");
      break;
   case 4:
      printf("i ist 4");
      /* fall through */
   case 5:
      printf("i ist 5");
      break;
   default:
      printf("nix passt");
}

Es kann zusätzlich noch mittels default ein Fall angegeben werden, der immer dann ausgeführt wird, wenn sämtliche andere Konstanten nicht zutreffen.

while

Die while-Schleife erlaubt es, eine Bedingung zu prüfen und Anweisungen so oft zu wiederholen, wie die Bedingung einen Wert ungleich 0 ergibt. Diese Bedingung wird hinter dem Schlüsselwort while in Klammern angegeben. Da die Bedingung vor dem Schleifendurchlauf überprüft wird, kann es auch vorkommen, dass die Schleife überhaupt nicht durchlaufen wird.

Bei den Anweisungen kann es sich um eine Einzelne Anweisung oder einen Block (dann in geschweiften Klammern) handeln.

int i;
 
i = 0;
while (i++ < 5)
{
   printf("i = %d\n",i);
}
i = 0;
while (++i < 5)
{
   printf("i = %d\n",i);
}

Dieses Beispiel zeigt auch die Auswirkung für eine vorangestellten und einen nachgestellten Inkrementoperator.

Mit dieser Schleife können wir jetzt den Cookie-Jar ausgeben, nachdem wir den Zeiger auf den Cookie-Jar ermittelt und bereits geprüft haben, ob ein Cookie-Jar existiert.

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

typedefstruct cookie_entry {
   union{
      unsignedlong name_long;
      char name_array[4];
   } name;
   unsignedlong value;
} CookieEntry;

int main(void)
{  CookieEntry **CookieJarPtr, *CookieJar;
   long OldStack;

   OldStack=Super(0L);
   CookieJarPtr = (CookieEntry**)0x5a0L;
   CookieJar=*CookieJarPtr;
   Super((void *)OldStack);
   if (CookieJar == (CookieEntry *)0)
      printf("Dieses System hat keinen Cookie Jar\n");
   else
   {
      while (CookieJar->name.name_long != 0)
      {
         printf("Name des Cookies: %d\n", CookieJar->name.name_long);
         printf("Wert des Cookies: %d\n", CookieJar->value);
         CookieJar++;
      }
   }
   return 0;
}

Achtung! Damit die Schleife terminiert, muss mindestens eine Anweisung vorhanden sein, die einen Einfluss auf die Bedingung in dem Schleifenkopf hat, also z.B. eine Variable, die abgefragt wird, auf einen anderen Wert setzt.

do-while

Die do-while Schleife prüft nach dem Schleifendurchlauf eine Bedingung. Deshalb wird diese Schleife immer mindestens einmal durchlaufen. Diese Schleife besteht aus dem Schlüsselwort do, einer Anweisung oder einem Block von Anweisungen in geschweiften Klammern, dem Schlüsselwort while und in Klammern eine Bedingung. Die Schleife wird so lange durchlaufen, wie die Bedingung einen Wert ungleich 0 ergibt.

...
int i, f[10];

i = 0;
do {
   f[i] = i;
   i++;
} while (i < 10);

Damit entspricht die do-while Schleife der repeat-until-Schleife in Pascal und Modula. Aber Achtung: hier liegt eine mögliche Fehlerquelle für Pascal- und Modula-Programmierer. Während bei der repeat-until-Schleife die Bedingung für den Abbruch der Schleife angegeben wird, wird bei der do-while-Schleife die Bedingung für den Schleifendurchlauf angegeben!

for

Die for-Schleife erlaubt es, Anweisungen mehrfach zu wiederholen und dazu in dem Kontrollblock der Schleife eine Initialisierung, eine Bedingung für den Schleifendurchlauf und eine Aktion für jeden Schleifendurchlauf anzugeben. Nach dem Schlüsselwort for folgt in Klammern, durch Semikolon getrennt, zunächst eine Anweisung, die vor dem ersten Schleifendurchlauf ausgeführt wird. Anschließend folgt ein Ausdruck, der vor jedem Schleifendurchlauf geprüft wird und einen Wert ungleich 0 ergeben muss, damit die Schleife ausgeführt wird. Zum Schluss folgt wieder eine Anweisung, die nach jedem Durchlauf der Schleife ausgeführt wird. Nach diesem Schleifenkopf folgt eine Anweisung oder ein Block in geschweiften Klammern als Schleifenrumpf. Es ist auch möglich, Teile wie z.B. die Initialisierung wegzulassen, also eine leere Anweisung zu schreiben. Dies macht für die Abbruchbedingung allerdings wenig Sinn.

Ein Beispiel sind Schleifen, bei denen die Anzahl der Durchläufe bekannt ist. Das folgende Beispiel setzt sämtliche Elemente eines Feldes auf den Index.

...
int i,f[10];

 for (i=0;i<10;i++)
{
   f[i] = i;
}

Der Schleifenzähler i wird vor dem ersten Durchlauf mit 0 initialisiert und nach jedem Schleifendurchlauf inkrementiert. Die Schleife wird so lange ausgeführt, wie i kleiner 10 ist. Sie können sich das Verhalten durch zusätzliche Ausgaben verdeutlichen.

Um auch mehr als einen Ausdruck zu verwenden, kann der Komma-Operator eingesetzt werden:

...
int i, j, f[10], g[10];

for (i=0, j=9; i<10; i++, j--)
{
   f[i] = g[j];
}

Die for-Schleife ist nicht nur auf Schleifen mit einem Schleifenzähler anwendbar. Das folgende Beispiel initialisiert einen Pointer und prüft den Wert des Pointers:

...
char *p, *Text="Hallo";

for (p=Text; *p != '\0'; p++)
   printf("%c\n",*p);

Auch wenn es möglich ist, sehr komplexe Ausdrücke in den Schleifenkopf zu stecken, sollte man dennoch nur die Anweisungen dort hinschreiben, die für den Abbruch der Schleife relevant sind. In obigem Beispiel sind dies also das Initialisieren und Inkrementieren des Pointers. Dadurch bleiben die Programme übersichtlich.

continue

Die continue-Anweisung dient dazu, in Schleifen den aktuellen Durchlauf abzubrechen. Es wird sofort die Bedingung überprüft und gegebenfalls mit dem nächsten Schleifendurchlauf begonnen. Bei for-Schleifen wird vorher die Anweisung für jeden Durchlauf ausgeführt. Und das sieht dann wie folgt aus:

...
int i, a[10];

for (i=0; i<10; i++)
{
   if (a[i] < 0)
      continue;
   a[i] = a[i] * 10;
}

In diesem Beispiel wird mit dem nächsten Schleifendurchlauf fortgefahren, wenn das Feldelement negativ ist. Wer das Beispiel testen möchte, sollte das Feld zuerst mit sinnvollen Werten vorbesetzen.

Da durch die continue Anweisung etwas verschleiert wird, dass die darunter stehenden Anweisungen nicht in jedem Fall ausgeführt werden, sollte nach Möglichkeit darauf verzichtet werden. Das obige Beispiel lässt sich auch umformulieren.

...
int i, a[10];

for (i=0; i<10; i++)
{
   if (a[i] >= 0)
      a[i] = a[i] * 10;
}

Wenn dadurch die folgenden Teile zu weit eingerückt werden, so ist zu überlegen, ob nicht sinnvollerweise Teile besser in separaten Funktionen aufgehoben sind.

break

Die break-Anweisung dient dazu, Schleifen und die switch-Anweisung zu verlassen und mit der ersten Anweisung hinter der Schleife bzw. switch fortzufahren.

...
char *p, *Text="Hallo";

for (p=Text; ; p++)
{
   if (*p == '\0')
      break;
   printf("%c\n",*p);
}

Da mit der break-Anweisung ein Nebenausgang aus einer Schleife geschaffen wird, ist damit der Programmablauf schwerer zu erkennen. Es sollte deshalb nach Möglichkeit in Schleifen auf ein break verzichtet und die Schleife über eine geeignete Bedingung im Schleifenkopf verlassen werden.

goto

Auch C kennt ein goto, mit dem zu einer beliebigen Marke innerhalb der gleichen Funktion gesprungen werden kann. Dazu wird nach dem Schlüsselwort goto der Name der Marke angegeben. Die Marke wird durch den Namen, gefolgt von einem Doppelpunkt, gesetzt und kann vor jeder Beliebigen Anweisung stehen. Eine mögliche sinnvolle Anwendung könnte das Verlassen von verschachtelten Schleifen für eine Fehlerbehandlung sein, da ein break nur die aktuelle Schleife (und nicht mehrere, ineinander geschachtelte Schleifen auf einmal) verlassen kann. Das unvollständige Beispiel unten zeigt die Anwendung.

...
for ( ... )
{
   for ( ... )
   {
      if ( Fehler_passiert )
         goto error;
   }
}
error:
   Fehlerbehandlung;

Allerdings muss jetzt die Fehlerbehandlung für den normalen Programmlauf umgangen werden. Wie man sieht, wird es schwieriger, den Programmlauf bei Anwendung von goto zu verfolgen. Es ist auch immer möglich, ein Programm ohne goto zu formulieren. Deshalb sollte ein goto vermieden werden. In meinen über 10 Jahren Erfahrung als Softwareentwickler habe ich niemals ein goto benutzen müssen.

Funktionen

Eine Funktion dient dazu, eine Menge von Anweisungen zusammenzufassen und unter einem Namen anzusprechen. Ihr können Werte mitgegeben werden, um die Aufgabe für verschiedene Ausgangssituationen durchzuführen. Diese Werte nennt man Parameter. Die Funktion kann ein Ergebnis zurückliefern, das in Ausdrücken verwendet werden kann.

Eine Funktion besteht aus dem Funktionskopf und dem Rumpf in geschweiften Klammern, der Deklarationen und Anweisungen enthalten kann.

Der Funktionskopf wiederum besteht aus dem Datentyp des Wertes, den die Funktion zurückliefert. Daran schließt sich der Funktionsname an. Anschließend folgen in geschweiften Klammern die Parameter. Die Parameter sind eine Liste von Datentypen und Namen des Parameters, durch Komma getrennt.

Eine Funktion kann keine zusammengesetzten Datentypen wie Strukturen oder Felder zurückliefern.

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

int main(void)
{  int x;

   x = Addiere(3,4);
   return 0;
}

Diese Funktion liefert einen int zurück, heißt Addiere und hat zwei Parameter x und y vom Typ int.

Beendet wird die Funktion entweder bei Erreichen der letzten geschweiften Klammer oder mit dem Operator return. Soll die Funktion einen Wert zurück geben, wird dieser Wert als Parameter des Operators return angegeben. Die Funktion Addiere gibt den Wert x+y zurück, also die Summe. Liefert die Funktion keinen Wert zurück, ist also typlos, kann das Schlüsselwort void verwendet werden. Ein Beispiel findet sich in dem Kapitel über offene Felder. Genauso kann bei einer Funktion ohne Parameter in die Klammern void geschrieben werden.

Aufgerufen wird die Funktion durch ihren Namen gefolgt von Klammern. Wenn die Funktion Parameter erwartet, werden diese in den Klammern durch Komma getrennt angegeben. Der Returnwert einer Funktion muss nicht in einem Ausdruck verwendet werden, sie kann auch dann, wenn sie nicht vom Typ void ist, ignoriert werden. Eine Funktion mit Returnwerten kann wie eine Prozedur in Pascal verwendet werden. Dies ist allerdings in der Regel nicht sinnvoll. Denn entweder ist der Returnwert das Ergebnis der Berechnung der Funktion und wird deshalb benötigt - oder es handelt sich um einen Fehlerwert, der deshalb auch abgefragt werden sollte.

Eine Funktion innerhalb einer Funktion (wie z.B. in Pascal) zu schreiben, ist nicht möglich.

Um ein Programm leichter verständlich zu machen, sollten alle Variablen, die von außerhalb der Funktion benutzt werden sollen, als Parameter übergeben werden. Ein Zugriff auf globale Variablen versteckt, welche Funktionen welche Variablen verändern. Ausnahmen sind dann sinnvoll, wenn kleine Funktionen sehr häufig aufgerufen werden und der Aufruf auf Geschwindigkeit optimiert werden muss.

main

Die Funktion mit dem Namen main muss in einem Programm genau einmal vorhanden sein. Sie ist die erste Funktion, die durch den Programmstart aufgerufen wird. Daher ist auch vorgegeben, welche Parameter und welcher Returnwert möglich sind.

Der Returnwert ist immer int. Diesen Wert kann das Programm, das unser Programm gestartet hat, abfragen. Dies wird z.B. in Batchprogrammen oder Shellscripts ausgenutzt. Normalerweise steht eine Returnwert von 0 für eine fehlerfreie Ausführung.

Parameter können entweder nicht akzeptiert werden (wie es z.B. bei Programmen üblich ist, die eine grafische Benutzeroberfläche haben) oder main bekommt die Parameter übergeben, die ein Aufrufer in die Kommandozeile geschrieben hat. In diesem Fall ist der erste Parameter vom Typ int und gibt an, wieviele Parameter dem Programm übergeben wurden. Der erste Parameter für das Programm enthält üblicherweise den Programmnamen, also das erste Wort der eingegebenen Kommandozeile. Es ist auf dem Atari vom Startupcode abhängig, ob der Programmname korrekt übergeben wird. Der zweite Parameter von main ist ein Feld von Zeigern auf die einzelnen Parameter des Programms.

#include <stdio.h>
 
int main(int argc, char *argv[])
{  int i;

   printf("%d Parameter bekommen\n",argc);
   for (i=0;i<argc;i++)
      printf("Parameter %d = %s\n",i,argv[i]);
   return 0;
}

Dieses Programm sollte von einem Kommandointerpreter gestartet werden, um unterschiedliche Parameter zu übergeben.

Prototypen

Für den Geltungsbereich gilt, wie bei Deklarationen, dass eine Funktion nur unterhalb ihrer Definition bekannt ist. Eine Funktion, die von main aufgerufen wird, muss also über main stehen. Wenn dies nicht erwünscht ist, um z.B. die Funktion main in einem Programm schneller zu finden, kann ein Prototyp angegeben und die Funktion an einer weiter unten liegenden Stelle definiert werden. Ein Prototyp ist der Funktionskopf mit einem Semikolon anstelle des Funktionsrumpfes.

int Addiere(int x, int y);

int main(void)
{  int x;

   x = Addiere(3,4);
   return 0;
}

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

In dem Prototyp kann darauf verzichtet werden, die Namen der Parameter anzugeben. Der Compiler prüft lediglich die Typen der Parameter anhand des Prototypen. Allerdings fördert die Angabe der Namen das Verständnis des Programms. Es ist nicht mehr erforderlich, in der Funktion selbst nachzuschauen.

int Addiere(int, int);

int main(void)
{  int x;

   x = Addiere(3,4);
   return 0;
}

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

Offene Felder

Wenn Felder als Parameter übergeben werden, ist es nicht nötig, die Größe des Feldes explizit anzugeben. Da der Name eines Feldes ohne eckige Klammern der Adresse des ersten Elementes entspricht, können auch Felder übergeben werden, wo ein Pointer erwartet wird und umgekehrt.

void Upcase(char Zeile[])
{  int i;

   for (i=0; Zeile[i] != '\0'; i++)
      if (Zeile[i]>='a' && Zeile[i]<='z')
         Zeile[i] = Zeile[i] - 'a' + 'A';
}

int main(void)
{  char *Text = "Hallo";

   Upcase(Text);
   return 0;
}

Der nächste Teil beschäftigt sich mit den Möglichkeiten, Parameter zu verändern.

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