Programmieren Teil 5 – Kontrollstrukturen

Die ersten Artikel dieser Reihe haben sich sich mit den allgemeinen Konzepten von Funktionen und Variablen beschäftigt. Wir haben gesehen, wie man neue Funktionen definiert, Variablen deklariert, ihnen Werte zuweist, sie an Funktionen übergibt und ihren Wert als Ergebnis einer Funktion zurückgibt. Heute wollen wir uns mit einem weiteren wichtigen Konzept beschäftigen: den Kontrollstrukturen.

Funktionsaufrufe sind eine Form, den Kontrollfluss im Programm, den Programmablauf, zu beeinflussen; hierbei wird von einer Stelle innerhalb einer Funktion zu einer anderen Funktion gesprungen, diese ausgeführt, und dann zum ursprünglichen Ort der Ausführung zurückgesprungen und dort fortgefahren. Funktionsaufrufe sind in ihrer ursprünglichen Form streng linear und eindeutig: beim Funktionsaufruf ist bekannt, an welche Stelle im Programm gesprungen werden und welcher Code dort ausgeführt werden soll (das gilt nicht für die objektorientierte Programmierung – aber dazu in einem späteren Artikel mehr).

Für komplexere Programme werden aber auch andere Formen der Beeinflussung des Kontrollflusses benötigt; hierfür werden die sogenannten Kontrollstrukturen verwendet. Besonders häufig benötigte Kontrollstrukturen sind Verzweigungen und Wiederholungen. Bei einer Verzweigung wird abhängig von einer Bedingung eine von 2 Anweisungen oder Anweisungsfolgen ausgeführt, wohingegen bei einer Wiederholung eine Anweisungsfolge so lange wiederholt wird, bis eine bestimmte Bedingung nicht mehr gilt. Aber im Detail.

Verzweigungen

Eine Verzweigung teilt den Programmfluss in 2 mögliche Pfade auf. Je nach dem, ob eine angegebene Bedingung erfüllt ist, wird entweder eine oder der andere Anweisungspfad ausgeführt. In C++ lassen sich Verzweigungen in der folgenden Art (an einem Beispiel) definieren:

int abs( int n ) { if ( n > 0 ) return n; else return -n; }

Gut, was sehen wir hier? Als erstes einmal die Definition einer Funktion abs, welche eine ganze Zahl als Parameter verlangt und eine ebensolche als Rückgabewert zurückliefert. Die Verzweigung innerhalb der Funktion besagt nun folgendes: Wenn der Parameter n größer als 0 ist, dann soll er direkt zurückgegeben werden; ansonsten (sprich, wenn er kleiner oder gleich 0 ist) soll er negiert zurückgegeben werden – die klassische Definition des Absolutwertes. Anders gesagt: ist die Bedingung der Verzweigung erfüllt, wird die Anweisung hinter dem if ausgeführt, ansonsten die hinter dem else (englisch für “sonst”).

In den beiden Zweigen können natürlich nicht nur einzelne Anweisungen, sondern ganze Anweisungsblöcke stehen, die, ebenso wie bei einer Funktion, in geschweifte Klammern eingefasst sind. In diesen Anweisungsblöcken können auch neue Variablen definiert werden, die dann nur innerhalb dieses Blocks gültig sind; der folgende Code würde zu einem Fehler führen, da die Variable m hinter der Anweisung nicht bekannt ist:

int abs( int n ) { if ( n > 0 ) { int m; m = n; } else { int m; m = -n; } return m; }

Gültig wäre dagegen der folgende Code (an welchem man auch gleich sieht, dass man die geschweiften Klammern bei Anweisungsfolgen mit nur einer Anweisung auch weglassen kann; das ganze auch gemischt innerhalb einer Verzweigung):

int abs( int n ) { int m; if ( n > 0 ) { m = n; } else m = -n; return m; }

Die Angabe des else-Zweiges ist übrigens nicht obligatorisch; wird er weggelassen, wird einfach kein Code in der Verzweigung ausgeführt, wenn die Bedingung nicht zutrifft (man spricht dann lediglich von einer bedingten Anweisung), etwa wie in dem folgenden Beispiel; das ist wieder die Funktion zur Berechnung des Absolutwertes, nur etwas anders formuliert:

int abs( int n ) { if ( n < 0 ) n = -n; return n; }

Lässt sich eine Bedingung nicht in lediglich 2 Zweige aufbrechen, so können auch mehrere Verzweigungen hintereinander geschaltet werden, etwa so (zur Definition einer mathematischen Funktion; deren Nutzen sei einmal egal):

int f( int m, int n ) { if ( n < 0 ) return -n; else if ( n > 0 ) return n; else return m; }

Wiederholungen

Die Kontrollstruktur der Wiederholung wird verwendet, wenn eine Anweisung oder Anweisungsfolge mehrere Male ausgeführt werden soll. Es lassen sich verschiedenste Formen der Wiederholung unterscheiden, wobei die while-Schleife die einfachste Form ist; in ihr wird eine Anweisung einfach so lange wiederholt, bis eine bestimmte Bedingung nicht mehr gilt. Das sieht in C++ dann zum Beispiel so aus:

int sum( int n ) { int s = 0; while ( n > 0 ) { s = s + n; n = n - 1; } return s; }

Die Funktion sum macht hierbei nichts anderes als die Summe der Zahlen von 1 bis n (oder, um den Programmablauf besser sprachlich abzubilden: von n bis 1) zu berechnen. Die Schleife wird dabei so oft durchlaufen, wie die Variable n einen Wert größer als 0 besitzt; in jedem Durchlauf wird das aktuelle n auf den Wert von s addiert und n um eins verringert. Denkbar einfach also.

Eine zweite, häufig anzutreffende Wiederholung in C++-Code ist die Zählschleife oder for-Schleife. Diese erlaubt (unter anderem), eine Schleife eine festgelegte Anzahl von Malen zu durchlaufen. Zu diesem Zweck wird eine Laufvariable definiert und für diese der Startwert, der Endwert (in Form einer Bedingung) und die Schrittweite (in Form eines Ausdruckes) angegeben. Die Summen-Funktion lässt sich damit auch folgendermaßen schreiben:

int sum( int n ) { int s = 0; for ( int i = 1; i <= n; i = i + 1 ) s = s + i; return s; }

In Worten besagt die Schleife folgendes: deklariere eine Variable i (die übrigens nur innerhalb der Schleife gültig ist) und weise ihr den Wert 1 zu; solange i kleiner oder gleich n ist (<= ist der C++-Operator für ≤), führe die Anweisungen in der Schleife aus und erhöhe am Ende jedes Durchlaufes i um 1.

Neben der while- und for-Schleife gibt es noch die do-while-Schleife, die ähnlich wie die while-Schleife funktioniert, die Bedingung aber am Ende eines Schleifendurchlaufes und nicht am Anfang prüft; einziger Unterschied zur while-Schleife ist damit, dass die Anweisungen in der Schleife wenigstens ein mal auf jeden Fall ausgeführt werden - unabhängig von der Bedingung. Geschrieben wird sie so:

int sum( int n ) { int s = 0; do { s = s + n; n = n - 1; } while ( n > 0 ); return s; }

Beim Verwenden von Schleifen ist auf eine Tücke zu achten: wird nicht sichergestellt, dass die Bedingung einer Schleife irgendwann nicht mehr gilt, kann man in eine sogenannte unendliche Schleife geraten, also eine Schleife, die niemals endet und immer wieder durchlaufen wird - was dazu führt, dass ein Programm nicht mehr reagiert und/oder abstürzt.

Zwei Konzepte sind bei der Verwendung von Schleifen noch interessant: das vorzeitige Abbrechen einer Schleife und das vorzeitige Abbrechen des aktuellen Schleifendurchlaufes.

Der vorzeitige Abbruch der gesamten Schleife erfolgt mit Hilfe des Schlüsselwortes break; wird es im Schleifenkörper angetroffen, so wird die Schleife sofort abgebrochen. Hier ein kleines Beispiel einer Summen-Schleife, die abbricht, sobald die berechnete Summe einen Wert größer als 1000 erreicht:

int sum1000( int n ) { int s = 0; while ( n > 0 ) { s = s + n; n = n - 1; if ( s > 1000 ) break; } return s; }

Mit Hilfe des Schlüsselwortes continue kann dagegen lediglich der aktuelle Schleifendurchlauf abgebrochen werden; wird es angetroffen, beginnt die Schleife sofort mit dem nächsten Durchlauf. Das folgende Beispiel zeigt eine Schleife, welche lediglich gerade Zahlen aufsummiert (der %-Operator ist der Restwert-Operator, der den Rest einer ganzzahligen Division zurückgibt):

int sum1000( int n ) { int s = 0; while ( n > 0 ) { if ( n % 2 != 0 ) { n = n - 1; continue; } s = s + n; n = n - 1; if ( s > 1000 ) break; } return s; }

So viel erst einmal zum Thema Verzweigungen und Schleifen. Der nächste Artikel wird wieder etwas technischer, da wir uns da etwas mit den Geheimnissen des Stacks beschäftigen werden.

Kommentare

  1. #1 Dr. Webbaer
    April 23, 2013

    Wobei “WHILE”, “DO” & “FOR” nur “IF” ist, der Bedinungsblock oder die bedingte Abfrage den Programmfluss steuert.

    Faszinierend, oder?

    MFG
    Dr. W

  2. #2 Sabine
    Berlin
    April 23, 2013

    Hallo,
    an der Stelle ein Kompliment und vielen Dank an Marcus Frenkel. Die Artikel haben mir tatsächlich DEN Schubs gegeben, um mit dem Programmieren-lernen anzufangen. Allerdings bin ich (faule Socke) von C++ weg und habe mich Python zugewandt. Deshalb verabschiede ich mich als Kommentierende und lese nur mehr still weiter mit – immerhin verstehe ich noch, worum es geht, auch wenn die Umsetzung nun konkret anders ist. Also nochmal: Danke!!

  3. #3 Stefan W.
    http://demystifikation.wordpress.com
    April 24, 2013

    Für Fortgeschrittene die Frage, wann der erste Code ein falsches Ergebnis zurückliefert.

    Außerdem gesucht: Einfache Beispiele, die einen sinnvollen Gebrauch von break und continue zeigen, der nicht eleganter anders gelöst werden kann.

    Für Überflieger: Wie bringt man Markus vom unseeligen “sogenannt” weg? Könnte eine Kontrollstruktur helfen – ein Verließ? ;) Vielleicht mit dem YAGNI des pragmatischen Programmierers?

    Außerdem schreit dieses Zitat nach dem DRY-Principle:

    dass die Anweisungen in der Schleife wenigstens ein mal auf jeden Fall ausgeführt werden – unabhängig von der Bedingung.

    Es wird 3x das gleiche gesagt.

    Mir fehlt etwas das switch/case-Statement, oder wurde das abgeschafft?
    Außerdem ist die for-Schleife als Zählschleife profanisiert worden. Tatsächlich besteht sie – neben dem womöglich auszuführenden body – aus 3 Elementen,

    for (init; cond; incr) {
    body;
    }

    dem initial ausgeführten statement, welches leer sein kann und welches aus mehreren, durch Komma getrennten Statements bestehen kann – Du korrigierst mich, Marcus? – zweitens, einer Bedingung, die leer sein kann, und die eine Kombination mehrerer Bedingungen sein kann. Es kann ein Zufallsgenerator aufgerufen werden, der recht willkürlich die Schleife terminiert. Und dann der incr.-Teil, der jedesmal zum Schleifenende aufgerufen wird.

    for (a=4, b=5; 3*a<b+100; a+=2, ++b) {
    body;
    }

    wäre etwa eine ebenso gültige for-Schleife wie
    for (;true;) {
    // eine Endlosschleife
    body;
    }

    Man kann eine for-Schleife wie oben umschreiben in

    {
    init;
    while (cond) {
    body;
    incr;
    }
    }

    wobei die äußeren, geschweiften Klammern nötig sind, weil im init-Teil Variablen deklariert werden können, die außerhalb der for-Schleife nicht sichtbar sind.

    Durch diesen Aufbau ist die for-Schleife zwar als Zählschleife geeignet, aber andere Einsatzgebiete sind ebenso legitim

    for (fileopen; not end of file; read char) {
    do something with the character
    }

  4. #4 michael
    April 24, 2013

    @StefanW
    Es kommt nur zwei mal das Wort ‘sogenannt’ vor. Wo ist das Problem ?

  5. #5 MartinB
    April 24, 2013

    Ich finde ja die abs-Variante mit dem n=-n ziemlich unschön – du verlässt dich darauf, dass ein call-by-value verwendet wird, aber zum Lesen ist das in meinen Augen problematisch, weil ich beim Lesen des Programms selbst zum Funktionsaufruf zurückgucken muss.

  6. #6 Marcus Frenkel
    April 24, 2013

    @StefanW

    Mir fehlt etwas das switch/case-Statement, oder wurde das abgeschafft?

    Alles zu seiner Zeit.

    Außerdem ist die for-Schleife als Zählschleife profanisiert worden.

    Um mich mal selber zu zitieren:

    Diese erlaubt (unter anderem), eine Schleife eine festgelegte Anzahl von Malen zu durchlaufen.

    Das hier ist ein Einführungsartikel. Weiterführende Konzepte kommen noch.

    @MartinB
    call-by-value oder call-by-reference sind in C++ zum Glück nicht implizit in den Parametern versteckt, sondern müssen immer explizit hingeschrieben werden; damit ist ein call-by-value recht leicht zu erkennen. Bei so einer kurzen Funktion ist das auch kein Problem; bei längeren Funktionen aber, da stimme ich gern zu, sollte man nicht unbedingt auf den Parametern direkt operieren.

  7. #7 CM
    April 24, 2013

    Total esoterisch, aber im neuen Standard ist:

    include

    float n = 0;
    n =-n;

    Dann ergäbe “std::copysign(1, n)” -1. Also sollte die Abfrage eher
    if ( n >= 0 )
    lauten.

    Aber wie gesagt, dass ist total esoterisch (weil nahezu ohne Relevanz) und den “>=”-Operator kennen wir auch noch nicht.

    Möglicherweise ist es das, was StefanW meinte?

    (Wie gibt man in Kommentaren Codeblocks am besten an? Der HTML-Code der Seite ist mir zu umständlich, um das für ‘nen schnelles Kommentar zu verwenden.)

  8. #8 Marcus Frenkel
    April 24, 2013

    @CM

    Beim include fehlt wohl etwas…die Zeichen müssen auch da immer mit dem entsprechenden HTML-Code angegeben werden. Das ist manchmal etwas frustrierend, dass hier keine BB-Codes verwendet werden, aber leider wohl nicht zu ändern.

    Aber ja: die Unterscheidung, ob man eine 0 negiert oder nicht, kann schon bei Gleitkommazahlen von Interesse sein, übrigens nicht erst seit dem neuen Standard. Die Bitmuster von +0 und -0 können sich unterscheiden, je nach dem, wie der benutzte C++-Compiler Gleitkommazahlen implementiert.

    Für Codeblöcke gibt es nicht wirklich gute Tags, fürchte ich. Man könnte es mit
    <pre><code> Code </code></pre>
    probieren, falls der Editor nicht die Leerzeichen entfernt…

    Experiment:

    Dies
      ist
        ein
          Test
    
  9. #9 Marcus Frenkel
    April 24, 2013

    Ja,
    <pre><code> Code </code></pre>
    geht einigermaßen.

  10. #10 CM
    April 24, 2013

    ups, Danke! Da sollte
    #include
    stehen.
    Und
    copysign()
    sowie
    signbit()
    gibt es doch erst mit C++11? Aber da war ich in der Tat viel zu undeutlich.

  11. #11 Marcus Frenkel
    April 24, 2013

    Fehlt bei include immer noch etwas? ;)
    Und ja, die genannten sind glaube ich neu, die Unterscheidung zwischen +0 und -0 ist aber schon uralt.

  12. #12 CM
    April 24, 2013

    math.h! Natürlich, ich will “math.h”! ;-)
    Eckige Klammern funktionieren trotz des Code-Tags nicht. Das könnte noch lustig werden …

  13. #13 DeLuRo (Der Lustige Robot)
    April 24, 2013

    @alle:
    Schreibt für … folgendes (ohne Leerzeichen, Semikoln beachten):
      spitze Klammer Auf, kleiner-als:     &­lt;
      spitze Klammer ZU, größer-als:     &­gt;

    Leerzeichen links lassen sich auch durch &­nbsp; darstellen.

  14. #14 Marcus Frenkel
    April 24, 2013

    @CM
    Auch die müssen per HTML-Code dargestellt werden.

    #include <math.h>

    Dann geht es. ;)

  15. #15 CM
    April 24, 2013

    Entschuldigt – eigentlich “kann” ich ganz gut HTML. Bin aber zu faul / zu verwöhnt, um das stets per Hand zu tippen – nur für ein Kommentar. Und bei etwas komplexeren Code würde es auch ziemlich fehleranfällig. Asche auf mein Haupt!
    (Vielleicht sollte ich ein SB-Blog-Skript zur Umwandlung schreiben? ;-) )

  16. #16 Stefan W.
    http://demystifikation.wordpress.com/2013/04/09/balkentrager/
    April 30, 2013

    Was ergibt dieser Code mit obiger abs-Funktion?

    cout << abs (-2147483648) << endl;

  17. #17 m
    Mai 3, 2013

    @CM: http://tohtml.com

    @Stefan W: das hängt davon ab wie int gespeichert wird :-)
    - 16 bis 31 bit 1-er-, 2-er-Komplement oder Vorzeichen-Betrag, oder 32 bit 1-er-Komplement oder Vorzeichen-Betrag: -2147483648 ist long int oder long long int, das Ergebnis der Konvertierung zu int ist dann implementation defined.
    - bei 32 bit 2-er-Komplement: bei C wären wir über die trap representations tief im Sumpf der implementation definedness; für C++ habe ich es nicht herausfinden können :-(.
    - mehr als 32 bit: 2147483648

  18. #18 michael
    Mai 3, 2013

    @m

    Ist in der ersten Antwort hier beschrieben.

  19. #19 m
    Mai 3, 2013

    Danke, michael.
    Und der Vollständigkeit halber: abs(INT_MIN) wäre über Kapitel 5, Absatz 4 oder 5 (je nach Ausgabe) undefined

    Sollte mich einer meiner Freunde fragen, weshalb ich das letzte mal einen Sprachstandard gelesen habe: ich würde vermutlich lügen :-)

  20. #20 Dieter
    Mai 23, 2013

    Zuerst mal danke für die C++ Serie. Finde ich sehr gut gemacht. Zwei Fragen zu den beiden letzten Beispielen dieser Folge:

    Woher kommt die Variable sum?

    Werden im letzten Beispiel nicht die ungeraden statt der geraden Zahlen summiert?

  21. #21 Marcus Frenkel
    Mai 23, 2013

    @Dieter

    Woher kommt die Variable sum?

    Magie. ;)
    Nein, Spaß, Fehler meinerseits. Sollte natürlich s heißen, ich habe es korrigiert.

    Werden im letzten Beispiel nicht die ungeraden statt der geraden Zahlen summiert?

    Auch hier mein Fehler; es sollte n % 2 != 0 heißen. Ist korrigiert.

    Danke für die Hinweise.

  22. #22 yohak
    Juli 21, 2013

    Mir scheint im letzten Code-Beispiel wird eine Endlosschleife
    produziert. Die Anweisung “n=n-1″ befindet sich ja hinter dem
    “continue”-Befehl, d.h. wenn n ungerade ist, und “continue”
    aufgerufen wird, kommt es gar nicht mehr zur Ausführung
    von “n=n-1″ sodass die Zählvariable n bis in alle Ewigkeit
    bei demselben ungeraden Wert bleibt und man in einer Endlosschleife landet. Oder übersehe ich da was?

  23. #23 Marcus Frenkel
    Juli 21, 2013

    @yohak
    Natürlich, danke. Ist korrigiert.