Zum Inhalt springen

C-Programmierung: Kontrollstrukturen

Aus Wikibooks

Bisher haben unsere Programme einen streng linearen Ablauf gehabt. In diesem Kapitel werden Sie lernen, wie Sie den Programmfluss steuern können.

Bedingungen

[Bearbeiten]

Um auf Ereignisse zu reagieren, die erst bei der Programmausführung bekannt sind, werden Bedingungsanweisungen eingesetzt. Eine Bedingungsanweisung wird beispielsweise verwendet, um auf Eingaben des Benutzers reagieren zu können. Je nachdem, was der Benutzer eingibt, ändert sich der Programmablauf.

Beginnen wir mit der if -Anweisung. Sie hat die folgende Syntax:

 if(expression) statement;

Optional kann eine alternative Anweisung angegeben werden, wenn die Bedingung expression nicht erfüllt wird:

 if(expression)
   statement;
 else
   statement;

Mehrere Fälle müssen verschachtelt abgefragt werden;

 if(expression1)
   statement;
 else
   if(expression2)
     statement;
   else
     statement;

Hinweis: else if - und else -Anweisungen sind optional.

Wenn der Ausdruck (engl. expression) nach seiner Auswertung wahr ist, d.h. von Null(0) verschieden, so wird die folgende Anweisung bzw. der folgende Anweisungsblock ausgeführt (statement). Ist der Ausdruck gleich Null und somit die Bedingungen nicht erfüllt, wird der else -Zweig ausgeführt, sofern vorhanden.

Klingt kompliziert, deshalb werden wir uns dies nochmals an zwei Beispielen ansehen:

#include <stdio.h>

int main(void)
{
  int zahl;
  printf("Bitte eine Zahl >5 eingeben: ");
  scanf("%i", &zahl);

  if(zahl > 5)
    printf("Die Zahl ist größer als 5\n");

  printf("Tschüß! Bis zum nächsten Mal\n");

  return 0;
}

Wir nehmen zunächst einmal an, dass der Benutzer die Zahl 7 eingibt. In diesem Fall ist der Ausdruck zahl > 5 true (wahr) und liefert eine 1 zurück. Da dies ein Wert ungleich 0 ist, wird die auf if folgende Zeile ausgeführt und "Die Zahl ist größer als 5" ausgegeben. Anschließend wird die Bearbeitung mit der Anweisung printf("Tschüß! Bis zum nächsten Mal\n") fortgesetzt .

Wenn wir annehmen, dass der Benutzer eine 3 eingegeben hat, so ist der Ausdruck zahl > 5 false (falsch) und liefert eine 0 zurück. Deshalb wird printf("Die Zahl ist größer als 5") nicht ausgeführt und nur "Tschüß! Bis zum nächsten mal" ausgegeben.

Wir können die if -Anweisung auch einfach lesen als: "Wenn zahl größer als 5 ist, dann gib "Die Zahl ist größer als 5" aus". In der Praxis wird man sich keine Gedanken machen, welches Resultat der Ausdruck zahl > 5 hat.

Das zweite Beispiel, das wir uns ansehen, besitzt neben if auch ein else if und ein else :

#include <stdio.h>

int main(void)
{
  int zahl;
  printf("Bitte geben Sie eine Zahl ein: ");
  scanf("%d", &zahl);

  if(zahl > 0)
    printf("Positive Zahl\n");
  else if(zahl < 0)
    printf("Negative Zahl\n");
  else
    printf("Zahl gleich Null\n");

  return 0;
}

Nehmen wir an, dass der Benutzer die Zahl -5 eingibt. Der Ausdruck zahl > 0 ist in diesem Fall falsch, weshalb der Ausdruck ein false liefert (was einer 0 entspricht). Deshalb wird die darauffolgende Anweisung nicht ausgeführt. Der Ausdruck zahl < 0 ist dagegen erfüllt, was wiederum bedeutet, dass der Ausdruck wahr ist (und damit eine 1 liefert) und so die folgende Anweisung ausgeführt wird.

Nehmen wir nun einmal an, der Benutzer gibt eine 0 ein. Sowohl der Ausdruck zahl > 0 als auch der Ausdruck zahl < 0 sind dann nicht erfüllt. Der if - und der if - else -Block werden deshalb nicht ausgeführt. Der Compiler trifft anschließend allerdings auf die else -Anweisung. Da keine vorherige Bedingung zutraf, wird die anschließende Anweisung ausgeführt.

Wir können die if - else if - else –Anweisung auch lesen als: "Wenn zahl größer ist als 0, gib "Positive Zahl" aus, ist zahl kleiner als 0, gib "Negative Zahl" aus, ansonsten gib "Zahl gleich Null" aus."

Fassen wir also nochmals zusammen: Ist der Ausdruck in der if oder if - else -Anweisung erfüllt (wahr), so wird die nächste Anweisung bzw. der nächste Anweisungsblock ausgeführt. Trifft keiner der Ausdrücke zu, so wird die Anweisung bzw. der Anweisungsblock, die else folgen, ausgeführt.

Es wird im Allgemeinen als ein guter Stil angesehen, jede Verzweigung einzeln zu klammern. So sollte man der Übersichtlichkeit halber das obere Beispiel so schreiben:

#include <stdio.h>

int main(void)
{
  int zahl;
  printf("Bitte geben Sie eine Zahl ein: ");
  scanf("%d", &zahl);

  if(zahl > 0) {
    printf("Positive Zahl\n");
  } else if(zahl < 0) {
    printf("Negative Zahl\n");
  } else {
    printf("Zahl gleich Null\n");
  }

  return 0;
}

Versehentliche Fehler wie

int a;

if(zahl > 0)
  a = berechne_a(); printf("Der Wert von a ist %d\n", a);

was so verstanden werden würde

int a;

if(zahl > 0) {
  a = berechne_a();
}

printf("Der Wert von a ist %d\n", a);

werden so vermieden.

Bedingter Ausdruck

[Bearbeiten]

Mit dem bedingten Ausdruck kann man eine if - else -Anweisung wesentlich kürzer formulieren. Sie hat die Syntax

exp1 ? exp2 : exp3

Zunächst wird das Ergebnis von exp1 ermittelt. Liefert dies einen Wert ungleich 0 und ist somit true, dann ist der Ausdruck exp2 das Resultat der bedingten Anweisung, andernfalls ist exp3 das Resultat.

Beispiel:

 int x = 20;
 x = (x >= 10) ? 100 : 200;

Der Ausdruck x >= 10 ist wahr und liefert deshalb eine 1. Da dies ein Wert ungleich 0 ist, ist das Resultat des bedingten Ausdrucks 100.

Der obige bedingte Ausdruck entspricht

 if(x >= 10)
   x = 100;
 else
   x = 200;

Die Klammern in unserem Beispiel sind nicht unbedingt notwendig, da Vergleichsoperatoren einen höheren Vorrang haben als der  ?: -Operator. Allerdings werden sie von vielen Programmierern verwendet, da sie die Lesbarkeit verbessern.

Der bedingte Ausdruck wird häufig, aufgrund seines Aufbaus, ternärer bzw. dreiwertiger Operator genannt.

switch

[Bearbeiten]

Eine weitere Auswahlanweisung ist die switch -Anweisung. Sie wird in der Regel verwendet, wenn eine unter vielen Bedingungen ausgewählt werden soll. Sie hat die folgende Syntax:

  switch(expression)
  {
    case const-expr: statements
    case const-expr: statements
    ...
    default: statements
  }

In den runden Klammern der switch-Anweisung steht der Ausdruck, welcher mit den Konstanten (const-expr) verglichen wird, die den case-Anweisungen direkt folgen. War ein Vergleich positiv, wird zur entsprechenden case-Anweisung gesprungen und sämtlicher darauffolgender Code ausgeführt (eventuelle weitere case-Anweisungen darin sind wirkungslos). Eine break-Anweisung beendet die switch-Verzweigung und setzt bei der Anweisung nach der schließenden geschweiften Klammer fort. Optional kann eine default-Anweisung angegeben werden, zu der gesprungen wird, falls keiner der Vergleichswerte passt.

Vorsicht: Im Gegensatz zu anderen Programmiersprachen bricht die switch-Anweisung nicht ab, wenn eine case-Bedingung erfüllt ist. Eine break-Anweisung ist zwingend erforderlich, wenn die nachfolgenen case-Blöcke nicht bearbeitet werden sollen.

Sehen wir uns dies an einem textbasierenden Rechner an, bei dem der Benutzer durch die Eingabe eines Zeichens eine der Grundrechenarten auswählen kann:

#include <stdio.h>

int main(void)
{
  double zahl1, zahl2;
  char auswahl;
  printf("\nMini-Taschenrechner\n");
  printf("-----------------\n\n");

  do
  {
     printf("\nBitte geben Sie die erste Zahl ein: ");
     scanf("%lf", &zahl1);
     printf("Bitte geben Sie die zweite Zahl ein: ");
     scanf("%lf", &zahl2);
     printf("\nZahl (a) addieren, (s) subtrahieren, (d) dividieren oder (m) multiplizieren?");
     printf("\nZum Beenden wählen Sie (b) ");
     scanf(" %c",&auswahl);

     switch(auswahl)
     {
       case 'a' :
       case 'A' :
         printf("Ergebnis: %f", zahl1 + zahl2);
         break;
       case 's' :
       case 'S' :
         printf("Ergebnis: %f", zahl1 - zahl2);
         break;
       case 'D' :
       case 'd' :
         if(zahl2 == 0)
           printf("Division durch 0 nicht möglich!");
         else
           printf("Ergebnis: %f", zahl1 / zahl2);
         break;
       case 'M' :
       case 'm' :
         printf("Ergebnis: %f", zahl1 * zahl2);
         break;
       case 'B' :
       case 'b' :
         break;
       default:
         printf("Fehler: Diese Eingabe ist nicht möglich!");
         break;
     }
   }

   while(auswahl != 'B' && auswahl != 'b');

   return 0;
}

Mit der do-while -Schleife wollen wir uns erst später beschäftigen. Nur so viel: Sie dient dazu, dass der in den Blockklammern eingeschlossene Teil nur solange ausgeführt wird, bis der Benutzer b oder B zum Beenden eingegeben hat.

Die Variable auswahl erhält die Entscheidung des Benutzers für eine der vier Grundrechenarten oder den Abbruch des Programms. Gibt der Anwender beispielsweise ein kleines 's' ein, fährt das Programm bei der Anweisung case('s') fort und es werden solange alle folgenden Anweisungen bearbeitet, bis das Programm auf ein break stößt. Wenn keine der case Anweisungen zutrifft, wird die default -Anweisung ausgeführt und eine Fehlermeldung ausgegeben.

Etwas verwirrend mögen die Anweisungen case('B') und case('b') sein, denen unmittelbar break folgt. Sie sind notwendig, damit bei der Eingabe von B oder b nicht die default -Anweisung ausgeführt wird.

Schleifen

[Bearbeiten]

Schleifen werden verwendet, um einen Programmabschnitt mehrmals zu wiederholen. Sie kommen in praktisch jedem größeren Programm vor.

For-Schleife

[Bearbeiten]

Die for-Schleife wird in der Regel dann verwendet, wenn von vornherein bekannt ist, wie oft die Schleife durchlaufen werden soll. Die for-Schleife hat die folgende Syntax:

for (expressionopt; expressionopt; expressionopt)
 statement

In der Regel besitzen for-Schleifen einen Schleifenzähler. Dies ist eine Variable, zu der bei jedem Durchgang ein Wert addiert oder subtrahiert wird (oder die durch andere Rechenoperationen verändert wird). Der Schleifenzähler wird über den ersten Ausdruck initialisiert. Mit dem zweiten Ausdruck wird überprüft, ob die Schleife fortgesetzt oder abgebrochen werden soll. Letzterer Fall tritt ein, wenn dieser den Wert 0 annimmt – also der Ausdruck false (falsch) ist. Der letzte Ausdruck dient schließlich dazu, den Schleifenzähler zu verändern.

Mit einem Beispiel sollte dies verständlicher werden. Das folgende Programm zählt von 1 bis 5:

#include <stdio.h>

int main()
{
   int i;

   for(i = 1; i <= 5; ++i)
     printf("%d  ", i);

   return 0;
}

Die Schleife beginnt mit dem Wert 1 (i = 1) und erhöht den Schleifenzähler i bei jedem Durchgang um 1 (++i). Solange der Wert i kleiner oder gleich 5 ist (i <= 5), wird die Schleife durchlaufen. Ist i gleich 6 und daher die Aussage i <= 5 falsch, wird der Wert 0 zurückgegeben und die Schleife abgebrochen. Insgesamt wird also die Schleife 5mal durchlaufen.

Wenn das Programm kompiliert und ausgeführt wird, erscheint die folgende Ausgabe auf dem Monitor:

1  2  3  4  5 

Anstelle des Präfixoperators hätte man auch den Postfixoperator i++ benutzen und for(i = 1; i <= 5; i++) schreiben können. Diese Variante unterscheidet sich nicht von der oben verwendeten. Eine weitere Möglichkeit wäre, for(i = 1; i <= 5; i = i + 1) oder for(i = 1; i <= 5; i += 1) zu schreiben. Die meisten Programmierer benutzen eine der ersten beiden Varianten, da sie der Meinung sind, dass schneller ersichtlich wird, dass i um eins erhöht wird und dass durch den Inkrementoperator Tipparbeit gespart werden kann.

Damit die for -Schleife noch etwas klarer wird, wollen wir uns noch ein paar Beispiele ansehen:

 for(i = 0;  i < 7; i += 1.5)

Der einzige Unterschied zum letzten Beispiel besteht darin, dass die Schleife nun in 1,5er Schritten durchlaufen wird. Der nachfolgende Befehl oder Anweisungsblock wird insgesamt 5mal durchlaufen. Dabei nimmt der Schleifenzähler i die Werte 0, 1.5, 3, 4.5 und 6 an (Die Variable i muss hier natürlich einen Gleitkommadatentyp haben).

 for(i = 20; i > 5; i -= 5)

Diesmal zählt die Schleife rückwärts. Sie wird dreimal durchlaufen. Der Schleifenzähler nimmt dabei die Werte 20, 15 und 10 an. Und noch ein letztes Beispiel:

 for(i = 1; i < 20; i *= 2)

Prinzipiell lassen sich für die Schleife alle Rechenoperationen benutzen. In diesem Fall wird in der Schleife die Multiplikation benutzt. Sie wird 5mal durchlaufen. Dabei nimmt der Schleifenzähler die Werte 1, 2, 4, 8 und 16 an.

Wie Sie aus der Syntax unschwer erkennen können, sind die Ausdrücke in den runden Klammern optional. So ist beispielsweise

  for(;;)

korrekt. Da nun der zweite Ausdruck immer wahr ist, und damit der Schleifenkopf niemals den Wert 0 annehmen kann, wird die Schleife unendlich oft durchlaufen. Eine solche Schleife wird auch als Endlosschleife bezeichnet, da sie niemals endet (in den meisten Betriebssystemen gibt es eine Möglichkeit das dadurch "stillstehende" Programm mit einer Tastenkombination abzubrechen). Endlosschleifen können beabsichtigt (siehe dazu auch weiter unten die break-Anweisung) oder unbeabsichtigte Programmierfehler sein.

Mehrere Befehle hinter einer for-Anweisung müssen immer in Blockklammern eingeschlossen werden:

 for(i = 1; i < 5; i++)
 {
   printf("\nEine Schleife: ");
   printf("%d ", i);
 }

Schleifen lassen sich auch schachteln, das heißt, innerhalb einer Schleife dürfen sich eine oder mehrere weitere Schleifen befinden. Beispiel:

#include <stdio.h>

int main()
{
   int i, j, Zahl = 1;

   for (i = 1; i <= 11; i++)
   {
      for (j = 1; j <= 10; j++)
      {
         printf ("%4i", Zahl++);
      }
      printf ("\n");
   }

   return 0;
}

Nach der Kompilierung und Übersetzung des Programms erscheint die folgende Ausgabe:

   1   2   3   4   5   6   7   8   9  10
  11  12  13  14  15  16  17  18  19  20
  21  22  23  24  25  26  27  28  29  30
  31  32  33  34  35  36  37  38  39  40
  41  42  43  44  45  46  47  48  49  50
  51  52  53  54  55  56  57  58  59  60
  61  62  63  64  65  66  67  68  69  70
  71  72  73  74  75  76  77  78  79  80
  81  82  83  84  85  86  87  88  89  90
  91  92  93  94  95  96  97  98  99 100
 101 102 103 104 105 106 107 108 109 110

Damit bei der Ausgabe alle 10 Einträge eine neue Zeile beginnt, wird die innere Schleife nach 10 Durchläufen beendet. Anschließend wird ein Zeilenumbruch ausgegeben und die innere Schleife von der äußeren Schleife wiederum insgesamt 11-mal aufgerufen.

While-Schleife

[Bearbeiten]

Häufig kommt es vor, dass eine Schleife, beispielsweise bei einem bestimmten Ereignis, abgebrochen werden soll. Ein solches Ereignis kann z.B. die Eingabe eines bestimmen Wertes sein. Hierfür verwendet man meist die while-Schleife, welche die folgende Syntax hat:

 while (expression)
   statement

Im folgenden Beispiel wird ein Text solange von der Tastatur eingelesen, bis der Benutzer die Eingabe abschließt (In der Microsoft-Welt geschieht dies durch <Strg>-<Z>, in der UNIX-Welt über die Tastenkombination <Strg>-<D>). Als Ergebnis liefert das Programm die Anzahl der Leerzeichen:

#include <stdio.h>

int main()
{
  int c;
  int zaehler = 0;

  printf("Leerzeichenzähler - zum Beenden STRG + D / STRG + Z\n");

  while((c = getchar()) != EOF)
  {
    if(c == ' ')
      zaehler++;
  }

  printf("Anzahl der Leerzeichen: %d\n", zaehler);

  return 0;
}

Die Schleife wird abgebrochen, wenn der Benutzer die Eingabe (mit <Strg>-<Z> oder <Strg>-<D>) abschließt und somit das nächste zu liefernde Zeichen das EOF-Zeichen ist. In diesem Fall ist der Ausdruck ((c = getchar()) != EOF) nicht mehr wahr, liefert 0 zurück, und die Schleife wird beendet.

Bitte beachten Sie, dass die Klammer um c = getchar() nötig ist, da der Ungleichheitsoperator eine höhere Priorität hat als der Zuweisungsoperator = . Neben den Zuweisungsoperatoren besitzen auch die logischen Operatoren Und (&), Oder (|) sowie XOR (^) eine niedrigere Priorität.

Noch eine Anmerkung zu diesem Programm: Wie Sie vielleicht bereits festgestellt haben, wird das Zeichen, das getchar() zurückliefert, in einer Variable des Typs Integer gespeichert. Für die Speicherung eines Zeichenwertes genügt, wie wir bereits gesehen haben, eine Variable vom Typ Character. Der Grund dafür, dass wir dies hier nicht können, liegt im ominösen EOF-Zeichen. Es dient normalerweise dazu, das Ende einer Datei zu markieren - auf Englisch das End of File - oder kurz EOF. Allerdings ist EOF ein negativer Wert vom Typ int , so dass kein "Platz" mehr in einer Variable vom Typ char ist. Viele Implementierungen benutzen -1 um das EOF-Zeichen darzustellen, was der ANSI-C-Standard allerdings nicht vorschreibt (der tatsächliche Wert ist in der Headerdatei <stdio.h> abgelegt).

Ersetzen einer for-Schleife

[Bearbeiten]

Eine for-Schleife kann immer durch eine while-Schleife ersetzt werden. So ist beispielsweise unser for-Schleifenbeispiel aus dem ersten Abschnitt mit der folgenden while-Schleife äquivalent:

#include <stdio.h>

int main()
{
  int x = 1;

  while(x <= 5)
  {
    printf("%d  ", x);
    ++x;
  }

  return 0;
}

Ob man while oder for benutzt, hängt letztlich von der Vorliebe des Programmierers ab. In diesem Fall würde man aber vermutlich eher eine for -Schleife verwenden, da diese Schleife eine Zählervariable enthält, die bei jedem Schleifendurchgang um eins erhöht wird.

Do-While-Schleife

[Bearbeiten]

Im Gegensatz zur while -Schleife findet bei der Do-while-Schleife die Überprüfung der Wiederholungsbedingung am Schleifenende statt. So kann garantiert werden, dass die Schleife mindestens einmal durchlaufen wird. Sie hat die folgende Syntax:

 do
   statement
 while (expression);

Das folgende Programm addiert solange Zahlen auf, bis der Anwender eine 0 eingibt:

#include <stdio.h>

int main(void)
{
 float zahl;
 float ergebnis = 0;

 do
 {
   printf ("Bitte Zahl zum Addieren eingeben (0 zum Beenden):");
   scanf("%f",&zahl);
   ergebnis += zahl;
 }
 while (zahl != 0);

 printf("Das Ergebnis ist %f \n", ergebnis);

 return 0;
}

Die Überprüfung, ob die Schleife fortgesetzt werden soll, findet in Zeile 14 statt. Mit do in Zeile 8 wird die Schleife begonnen, eine Prüfung findet dort nicht statt, weshalb der Block von Zeile 9 bis 13 in jedem Fall mindestens einmal ausgeführt wird.

Wichtig: Beachten Sie, dass das while mit einem Semikolon abgeschlossen werden muss, sonst wird das Programm nicht korrekt ausgeführt!

Schleifen abbrechen

[Bearbeiten]

continue

[Bearbeiten]

Eine continue-Anweisung beendet den aktuellen Schleifendurchlauf und setzt, sofern die Schleifen-Bedingung noch erfüllt ist, beim nächsten Durchlauf fort.

#include <stdio.h>

int main(void)
{
  double i;

  for(i = -10; i <= 10; i++)
  {
    if(i == 0)
     continue;

    printf("%lf \n", 1/i);
  }

  return 0;
}

Das Programm berechnet in ganzzahligen Schritten die Werte für 1/i im Intervall [-10, 10]. Da die Division durch Null nicht erlaubt ist, springen wir mit Hilfe der if-Bedingung wieder zum Schleifenkopf.

break

[Bearbeiten]

Die break-Anweisung beendet eine Schleife und setzt bei der ersten Anweisung nach der Schleife fort. Nur innerhalb einer Wiederholungsanweisung, wie in for-, while-, do-while-Schleifen oder innerhalb einer switch-Anweisung ist eine break-Anweisung funktionsfähig. Sehen wir uns dies an folgendem Beispiel an:

#include <stdio.h>

int eingabe;
int passwort = 2323;

int main(void) {
    while (1) {
        printf("Geben Sie bitte das Zahlen-Passwort ein: ");
        scanf("%d", &eingabe);

        if (passwort == eingabe) {
            printf("Passwort korrekt\n");
            break;
        } else {
            printf("Das Passwort ist nicht korrekt.\n");
        }

        printf("Bitte versuchen Sie es nochmal!\n");
    }

    printf("Programm beendet\n");

    return 0;
}


Wie Sie sehen ist die while-Schleife als Endlosschleife konzipiert. Hat man das richtige Passwort eingegeben, so wird die printf-Anweisung ausgegeben, und anschließend wird diese Endlosschleife durch die break-Anweisung verlassen. Die nächste Anweisung, die dann ausgeführt wird, ist die printf-Anweisung unmittelbar nach der Schleife. Ist das Passwort aber inkorrekt, so wird der else-Block mit den weiteren printf-Anweisungen in der while-Schleife ausgeführt. Anschließend wird die while-Schleife wieder ausgeführt.

Tastaturpuffer leeren

[Bearbeiten]

Es ist wichtig, den Tastaturpuffer zu leeren, damit Tastendrücke nicht eine unbeabsichtigte Aktion auslösen (Es besteht außerdem noch die Gefahr eines Puffer-Überlaufs). In ANSI-C-Compilern bzw. deren Laufzeitbibliothek ist die Vollpufferung die Standardeinstellung; diese ist auch sinnvoller als keine Pufferung, da dadurch weniger Schreib- und Leseoperationen stattfinden. Die Puffergröße ist abhängig vom Compiler. Weiteres zu Pufferung und setbuf()/setvbuf() wird in den weiterführenden Kapiteln behandelt.

Sehen wir uns dies an einem kleinen Spiel an: Der Computer ermittelt eine Zufallszahl zwischen 1 und 100, die der Nutzer dann erraten soll. Dabei gibt es immer einen Hinweis, ob die Zahl kleiner oder größer als die eingegebene Zahl ist.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
  int zufallszahl,  eingabe;
  int durchgaenge;
  char auswahl;
  srand(time(0));

  printf("\nLustiges Zahlenraten");
  printf("\n--------------------");
  printf("\nErraten Sie die Zufallszahl in moeglichst wenigen Schritten!");
  printf("\nDie Zahl kann zwischen 1 und 100 liegen");

  do
  {
    zufallszahl = (rand() % 100 + 1);
    durchgaenge = 1;

    while(1)
    {
      printf("\nBitte geben Sie eine Zahl ein: ");
      scanf("%d", &eingabe);

      if(eingabe > zufallszahl)
      {
        printf("Leider falsch! Die zu erratende Zahl ist kleiner");
        durchgaenge++;
      }
      else if(eingabe < zufallszahl)
      {
        printf("Leider falsch! Die zu erratende Zahl ist größer");
        durchgaenge++;
      }
      else
      {
        printf("Glückwunsch! Sie haben die Zahl in %d", durchgaenge);
        printf(" Schritten erraten.");
        break;
      }
    }

    printf("\nNoch ein Spiel? (J/j für weiteres Spiel)");

    /* Rest vom letzten scanf aus dem Tastaturpuffer löschen */
    while((auswahl = getchar()) != '\n' && auswahl != EOF);

    auswahl = getchar();

  } while(auswahl == 'j' || auswahl == 'J');

  return 0;
}

Wie Sie sehen, ist die innere while -Schleife als Endlosschleife konzipiert. Hat der Spieler die richtige Zahl erraten, so wird der else-Block ausgeführt. In diesem wird die Endlosschleife schließlich mit break abgebrochen. Die nächste Anweisung, die dann ausgeführt wird, ist die printf -Anweisung unmittelbar nach der Schleife.

Die äußere while -Schleife in Zeile 52 wird solange wiederholt, bis der Benutzer nicht mehr mit einem kleinen oder großen j antwortet. Beachten Sie, dass im Gegensatz zu den Operatoren & und | die Operatoren && und || streng von links nach rechts bewertet werden.

In diesem Beispiel hat dies keine Auswirkungen. Allerdings schreibt der Standard für den || -Operator auch vor, dass, wenn der erste Operand des Ausdrucks verschieden von 0 (wahr) ist, der Rest nicht mehr ausgewertet wird. Die Folgen soll dieses Beispiel verdeutlichen:

 int c, a = 5;

 while (a == 5 || (c = getchar()) != EOF)

Da der Ausdruck a == 5 true ist, liefert er also einen von 0 verschiedenen Wert zurück. Der Ausdruck c = getchar() wird deshalb erst gar nicht mehr ausgewertet, da bereits nach der Auswertung des ersten Operanden feststeht, dass die ODER-Verknüpfung den Wahrheitswert true besitzen muss (Wenn Ihnen dies nicht klar geworden ist, sehen Sie sich nochmals die Wahrheitstabelle der ODER-Verknüpfung an). Dies hat zur Folge, dass getchar() nicht mehr ausgeführt und deshalb kein Zeichen eingelesen wird. Wenn wir wollen, dass getchar() aufgerufen wird, so müssen wir die Reihenfolge der Operanden umdrehen.

Dasselbe gilt natürlich auch für den && -Operator, nur dass in diesem Fall der zweite Operand nicht mehr ausgewertet wird, wenn der erste Operand bereits 0 ist.

Beim || und && -Operator handelt es sich um einen Sequenzpunkt: Wie wir gesehen haben, ist dies ein Punkt, bis zu dem alle Nebenwirkungen vom Compiler ausgewertet sein müssen. Auch hierzu ein Beispiel:

  i = 7;

  if(i++ == 5 || (i += 3) == 4)

Zunächst wird der erste Operand ausgewertet (i++ == 5) - es wird i mit dem Wert 5 verglichen und dann um eins erhöht (Post-Inkrement!). Wie wir gerade gesehen haben, wird der zweite Operand ((i += 3) == 4) nur dann ausgewertet, wenn feststeht, dass der erste Operand 0 liefert (bzw. keinen nicht von 0 verschiedenen Wert). Da der erste Operand keine wahre Aussage darstellt (7 wird auf Gleichheit mit 5 überprüft und gibt "falsch" zurück, da 7 nicht gleich 5 ist) wird der zweite ausgewertet. Hierbei wird zunächst 8 um 3 erhöht, das Ergebnis der Zuweisung (11) mit 4 verglichen. Es wird also der gesamte Ausdruck ausgewertet (er ergibt insgesamt übrigens "falsch", da weder der erste noch der zweite Operand "wahr" ergeben; 8 ist ungleich 5, und 11 ist ungleich 4).

Die Auswertung findet auf jeden Fall in dieser Reihenfolge statt, nicht umgekehrt. Es ist also nicht möglich, dass zu i zuerst die 3 addiert wird und so den Wert 10 annimmt, um anschließend um 1 erhöht zu werden. Diese Tatsache ändert in diesem Beispiel nichts an der Falschheit des gesamten Ausdruckes, kann aber zu unbedachten Resultaten führen, wenn im zweiten Operator eine Funktion aufgerufen wird, die Nebenwirkungen hat (beispielsweise das Anlegen einer Datei). Ergibt der erste Operand einen Wert ungleich 0 (also wahr), so wird der zweite (rechts vom ||-Operator) nicht mehr aufgerufen und die Datei nicht mehr angelegt.

Bevor wir uns weiter mit Kontrollstrukturen beschäftigen, lassen Sie uns aber noch einen Blick auf den Zufallsgenerator werfen, da er eine interessante Anwendung für den Modulo–Operator darstellt. Damit der Zufallsgenerator nicht immer die gleichen Zahlen ermittelt, muss zunächst der Zufallsgenerator über srand(time(0)) mit der Systemzeit initialisiert werden (wenn Sie diese Bibliotheksfunktionen in Ihrem Programm benutzen wollen, beachten Sie, dass Sie für die Funktion time(0) die Headerdatei <time.h> und für die Benutzung des Zufallsgenerators die Headerdatei <stdlib.h> einbinden müssen). Aber wozu braucht man nun den Modulo-Operator? Die Funktion rand() liefert einen Wert zwischen 0 und mindestens 32767. Um nun einen Zufallswert zwischen 1 und 100 zu erhalten, führen wir eine Moduloberechnung mit hundert durch und addieren 1. Den Rest, der ja nun zwischen eins und hundert liegen muss, verwenden wir als Zufallszahl.

Bitte beachten Sie, dass rand() in der Regel keine sehr gute Streuung liefert. Für statistische Zwecke sollten Sie deshalb nicht auf die Standardbibliothek zurückgreifen.

Sonstiges

[Bearbeiten]

Mit einer goto-Anweisung setzt man die Ausführung des Programms an einer anderen Stelle des Programms fort. Diese Stelle im Programmcode wird mit einem sogenannten Label definiert:

 LabelName:

Zu einem Label springt man mit

 goto LabelName;

In der Anfangszeit der Programmierung wurde goto anstelle der eben vorgestellten Kontrollstrukturen verwendet. Das Ergebnis war eine sehr unübersichtliche Programmstruktur, die auch häufig als Spaghetticode bezeichnet wurde. Bis auf wenige Ausnahmen ist es möglich, auf die goto-Anweisung zu verzichten (neuere Sprachen wie Java kennen sogar überhaupt kein goto mehr). Einige der wenigen Anwendungsgebiete von goto werden Sie im Kapitel Programmierstil finden, darüber hinaus werden Sie aber keine weiteren Beispiele in diesem Buch finden.