Zum Inhalt springen
Topos Uranos

Zusammenfassung

Die Lektion Schleifen hat gezeigt, wie man eine Wiederholung schreibt; diese zeigt, wie man sie gut schreibt und weiß, warum sie richtig ist. Sie beginnt mit den Mustern, aus denen die meisten Schleifen gebaut sind (Zähler, Akkumulator, Maximum und Minimum, Wächter (Sentinel) und Flag), jedes bestimmt durch das, was seine Variable nach jedem Durchlauf bedeutet. Diese Beobachtung führt zum zentralen Begriff: der Invariante, einer Aussage über die Variablen, die vor dem ersten Durchlauf und nach jedem weiteren gilt und die zusammen mit der Abbruchbedingung das Ergebnis impliziert. Sie ist eine Induktion über die Anzahl der Durchläufe, und mit ihr entwirft man eine Schleife rückwärts: zuerst das, was am Ende gelten muss, dann die Invariante, zuletzt den Rumpf. Die Terminierung wird mit der Variante begründet, die schon in der Lektion Vom Problem zum Algorithmus vorgestellt wurde, und die Lektion zeigt drei Schleifen, die nicht terminieren: eine mit einer Bedingung über Gleitkommawerte, eine mit einem vorzeichenlosen Zähler und eine, deren vorzeichenbehafteter Zähler überläuft, was undefiniertes Verhalten ist. Sie schließt mit dem ersten Kontakt mit dem Debugger von Visual Studio Code, der das Programm an einer Zeile anhält und seine Variablen Durchlauf für Durchlauf zeigt.

Lernziele

  1. In einer Schleife die Muster Zähler, Akkumulator, Maximum und Wächter erkennen und sie verwenden, um neue Schleifen zu entwerfen.
  2. Die Invariante einer einfachen Schleife formulieren und nachweisen, dass sie in einem beliebigen Durchlauf erhalten bleibt.
  3. Die Terminierung einer Schleife mit einer ganzzahligen Größe begründen, die abnimmt und beschränkt ist.
  4. Die Ausführung einer Schleife mit dem Debugger von Visual Studio Code verfolgen, mit einem Haltepunkt und der Variablenansicht.

Muster

Was eine Variable bedeutet

Die meisten Schleifen werden nicht erfunden: Sie werden aus wenigen Bausteinen zusammengesetzt, die in allen Programmen wiederkehren. Jeder Baustein ist eine Variable mit einer Initialisierung vor der Schleife, einer Aktualisierung in ihr und vor allem einer Bedeutung, die nach jedem Durchlauf gilt. Die Bausteine an ihrer Bedeutung und nicht an ihrer Form zu erkennen, ist das, was erlaubt, sie fehlerfrei zu kombinieren.

  • Ein Zähler gibt an, wie viele Daten verarbeitet worden sind (oder wie viele eine Bedingung erfüllen). Er wird mit 0 initialisiert, weil vor dem ersten Durchlauf noch keines verarbeitet worden ist, und mit ++count aktualisiert, nur wenn es etwas zu zählen gibt.
  • Ein Akkumulator enthält die Verknüpfung der verarbeiteten Daten: ihre Summe, ihr Produkt. Er wird mit dem neutralen Element der Operation initialisiert (0 für die Summe, 1 für das Produkt), weil die Summe von keinem Datum 0 ist und das Produkt von keinem Datum 1.
  • Ein Maximum enthält das größte der verarbeiteten Daten, ein Minimum das kleinste. Das Maximum von keinem Datum existiert nicht: Deshalb wird es mit dem ersten Datum initialisiert und nicht mit einer zufällig gewählten Zahl.
  • Ein Wächter ist ein Wert der Eingabe, der kein Datum ist, sondern das Signal, dass die Daten zu Ende sind. Das Muster verlangt, einmal vor der Schleife und noch einmal am Ende des Rumpfes zu lesen, wie es die Lektion Schleifen beim Summieren bis zur Null getan hat, sodass die Bedingung jeden Wert prüft, bevor er verarbeitet wird, und der Wächter nie als Datum gezählt wird.
  • Ein Flag ist ein bool, das angibt, ob unter den verarbeiteten Daten irgendwann etwas eingetreten ist. Es wird mit false initialisiert und wird true, sobald es zum ersten Mal eintritt; kein späterer Durchlauf setzt es auf false zurück.

Ein Beispiel mit allen Bausteinen

Ein Regenprotokoll verzeichnet die Millimeter Niederschlag jedes Tages. Das Programm liest zuerst einen Schwellenwert und danach die Messwerte mehrerer Tage, abgeschlossen durch eine negative Zahl, die kein möglicher Messwert ist und als Wächter dient; es gibt aus, wie viele Messwerte es gibt, ihren Mittelwert, das Maximum, an wie vielen Tagen der Schwellenwert überschritten wurde und ob es einen Tag ohne Regen gab.

C++-Programm
#include <iostream>#include <print>​int main(){    double threshold{};    std::cin >> threshold;    int count{0};          // Zähler: verarbeitete Messwerte    int aboveCount{0};     // Zähler: Messwerte über dem Schwellenwert    double total{0.0};     // Akkumulator: Summe der Messwerte    double largest{0.0};   // Maximum: gilt nur, wenn count > 0    bool dryDay{false};    // Flag: es gab einen Tag ohne Regen    double value{};    std::cin >> value;    while (std::cin && value >= 0.0) {        if (count == 0 || value > largest) {            largest = value;        }        ++count;        total += value;        if (value > threshold) {            ++aboveCount;        }        if (value == 0.0) {            dryDay = true;        }        std::cin >> value;    }    if (count == 0) {        std::println("Keine Messwerte.");    } else {        std::println("Messwerte: {}; Mittelwert: {:.2f} mm", count, total / count);        std::println("Maximum: {:.1f} mm; Tage über {:.1f} mm: {}", largest, threshold, aboveCount);        std::println("Tage ohne Regen: {}", dryDay ? "ja" : "nein");    }}
Programmeingabe
104.5 0 12.5 21.5 7.5 -1
Ausgabe
Messwerte: 5; Mittelwert: 9.20 mmMaximum: 21.5 mm; Tage über 10.0 mm: 2Tage ohne Regen: ja

Vier Entscheidungen verdienen einen Kommentar. Die Bedingung count == 0 || value > largest initialisiert das Maximum mit dem ersten Datum ohne gesondertes Lesen: Im ersten Durchlauf ist der Zähler 0, und die Zuweisung findet immer statt; in den folgenden nur, wenn das Datum das Maximum übertrifft. Der Vergleich value == 0.0 ist exakt, weil der Wert gelesen und nicht berechnet wurde: Die Ziffer 0 wird in die exakte Null umgewandelt, und die Warnung der Lektion Gleitkommazahlen vor == bezieht sich auf Ergebnisse von Rechnungen. Die Schleifenbedingung enthält std::cin, sodass auch eine Eingabe, die ohne Wächter endet, die Schleife beendet. Und der Mittelwert wird nur berechnet, wenn es Messwerte gibt: Mit count gleich 0 wäre total / count gleich 0/00/0, was im Gleitkommaformat kein Fehler ist, sondern der Wert „keine Zahl“, den das Programm ohne Hinweis ausgeben würde.

BeispielDie Variablen Durchlauf für Durchlauf

Beweis

  1. Vor der Schleife: Der Schwellenwert ist 10, die Zähler und der Akkumulator sind 0, das Flag ist falsch, und der erste Messwert, 4.5, ist schon gelesen. Das Maximum ist 0, aber dieser Wert bedeutet nichts, solange count gleich 0 ist.

  2. Erster Durchlauf: count ist 0, also wird 4.5 zum Maximum; der Zähler steigt auf 1 und die Summe auf 4.5. Gelesen wird 0.

  3. Zweiter Durchlauf: 0 übertrifft das Maximum nicht, setzt aber das Flag. Gelesen wird 12.5.

  4. Dritter Durchlauf: 12.5 ist das neue Maximum und überschreitet den Schwellenwert: aboveCount steigt auf 1. Gelesen wird 21.5.

  5. Vierter Durchlauf: 21.5 ist das neue Maximum und überschreitet ebenfalls den Schwellenwert. Gelesen wird 7.5.

  6. Fünfter Durchlauf: 7.5 ändert weder das Maximum noch aboveCount; die Summe erreicht 46. Gelesen wird −1-1.

  7. Die Bedingung prüft −1-1, und das ist negativ: Die Schleife endet, ohne es zu verarbeiten. Der Mittelwert ist 46/5=9.246 / 5 = 9.2.

Man beachte, dass in jeder Zeile der Abbildung jede Variable ihre Bedeutung in Bezug auf die bis dahin verarbeiteten Messwerte erfüllt: total ist ihre Summe, largest ihr Maximum, dryDay gibt an, ob unter ihnen eine Null war. Diese Eigenschaft, die nach jedem Durchlauf gilt, hat einen Namen.