CodeGym /Kurse /C++ SELF /Debugging-Strategie

Debugging-Strategie

C++ SELF
Level 34, Lektion 4
Verfügbar

1. Debugging-Strategie ist wichtiger als Debugger-Magie

Wenn ein Programm sich falsch verhält, schaltet das Gehirn leicht in den Modus „Panik und Schamanismus“: ein paar Zeilen ändern, neu starten, vielleicht hat es geholfen. Manchmal hilft es. Manchmal macht man es versehentlich schlimmer, aber der Bug „versteckt sich“ und taucht später wieder auf — im Prüfungsfall oder in Production (also genau dann, wenn es psychologisch besonders kritisch ist).

Eine Debugging-Strategie ist nicht nötig, weil „das so die Profis machen“, sondern weil sie Zeit spart. Der Debugger ist Lupe und Taschenlampe, aber wenn Sie nicht wissen, wonach Sie suchen sollen, leuchten Sie mit der Lampe gegen die Decke und wundern sich, warum der Fehler nicht gefunden wird. Wir debuggen wie Ingenieure: Problem festhalten, Suchbereich einschränken, konkrete Hypothese prüfen.

Unsere Arbeitsformel heute:

flowchart TD
    A[Reproduziere den Bug] --> B[Ort/Bedingungen isolieren]
    B --> C[Hypothese formulieren]
    C --> D[Hypothese im Debugger prüfen]
    D --> E{Bestätigt?}
    E -- ja --> F[Mit minimaler Änderung beheben]
    E -- nein --> C

2. Schritt 1: den Bug reproduzieren

Sehr unangenehme Bugs sind jene, die „manchmal auftreten“. Sie wirken mystisch: heute Absturz, morgen nicht, übermorgen wieder Absturz — aber nur, wenn eine Kaffeetasse danebensteht. In Wirklichkeit gibt es fast immer Bedingungen, Sie haben sie nur noch nicht getroffen.

Reproduzieren heißt, so vorgehen, dass Sie das falsche Verhalten immer wieder nach einem klaren Szenario erzeugen können: gleiche Eingabedaten → gleiches falsches Ergebnis. Das ist wichtig, denn sonst wissen Sie nicht, ob Ihre Änderung geholfen hat oder Sie einfach Glück hatten, dass der Bug sich nicht gezeigt hat.

Stellen wir uns vor, wir entwickeln eine Übungskonsole-Applikation ExpenseTracker (Ausgaben-Tracker). Sie speichert Ausgaben in std::vector, kann Ausgaben hinzufügen und den Durchschnitt berechnen.

Minimalversion des „Kerns“ (noch ohne UI):

#include <iostream>
#include <vector>

int main() {
    std::vector<int> expenses{100, 50, 150};

    int sum = 0;
    for (int x : expenses) sum += x;

    std::cout << "Sum=" << sum << '\n'; // Sum=300
}

Nun stellen wir uns vor, ein Student hat „Average“ hinzugefügt und bekommt eine Merkwürdigkeit: Average=100, obwohl 100.0 erwartet wird — und es scheint zu stimmen, bis ein anderer Datensatz weh tut (z. B. 100 und 101 ergeben 100 statt 100.5). Hier ist wichtig: Sie müssen einen kleinen Szenario haben, das den Fehler zuverlässig zeigt.

Gutes reproduzierendes Beispiel für Ganzzahldivision:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> expenses{100, 101};

    int sum = 0;
    for (int x : expenses) sum += x;

    double avg = sum / expenses.size();
    std::cout << "Average=" << avg << '\n'; // Average=100 (aber erwartet 100.5)
}

Beachten Sie: wir haben nicht „irgendwie gespürt“, dass etwas nicht stimmt. Wir haben Eingaben geschaffen, bei denen der Fehler offensichtlich ist. Das ist die halbe Miete.

Beim Debuggen hängt Reproduzieren oft an Disziplin: Sie speichern die genaue Eingabe, die genauen Schritte, notieren „erwartet/erhalten“. Wenn das Programm interaktiv ist, ist es hilfreich, die Eingabe temporär im Code fest zu hinterlegen, damit man nicht zehnmal hintereinander dasselbe per Hand eingeben muss.

3. Schritt 2: die Ursache isolieren

Nachdem der Bug stabil reproduziert ist, ist die nächste Falle, das ganze Projekt zu debuggen. Das ist wie nach einem verlorenen Schlüssel suchen: man kann die ganze Wohnung umwälzen, oder man erinnert sich an die letzten fünf Minuten und guckt zuerst in die Jackentasche.

Isolieren heißt, den Suchbereich zu verkleinern: herausfinden, in welchem Teil des Programms der Bug entsteht. Häufig tritt das Symptom an einer Stelle auf (z. B. falsche Ausgabe), die Ursache liegt woanders (z. B. falsche Berechnung oder fehlerhafte Eingabe).

Es gibt zwei sehr praktische Wege zur Isolation.

Erster Weg — das große Programm in ein kleines verwandeln: temporär nur den Teil lassen, der zur Reproduktion nötig ist. Wenn der Bug „Average=100 statt 100.5“ ist, brauchen wir nicht CLI, Menüs, Command-Handling oder hübsche Tabellen. Wir brauchen die Durchschnittsberechnung.

Extrahieren wir die Berechnung in eine Funktion:

#include <vector>

double average_expense(const std::vector<int>& v) {
    int sum = 0;
    for (int x : v) sum += x;
    return sum / v.size(); // verdächtige Stelle
}

Und in main lassen wir nur den minimalen Aufruf:

#include <iostream>
#include <vector>

double average_expense(const std::vector<int>& v);

int main() {
    std::vector<int> expenses{100, 101};
    std::cout << average_expense(expenses) << '\n'; // 100 (erwartet 100.5)
}

Jetzt haben Sie eine kleine „Labor“-Szene für den Debugger: kleine Eingabe, kurzer Code, wenige Breakpoints, Sie ertrinken nicht in Details.

Zweiter Weg — Isolation über den Ausführungsweg: Breakpoint an der Stelle des Auftretens setzen, den call stack ansehen, „nach oben“ gehen und herausfinden, wer die schlechten Daten übergeben hat. Das ist besonders nützlich, wenn ein Fehler in einer kleinen Funktion sichtbar wird, die an sich korrekt ist, aber falsche Argumente bekommen hat.

4. Schritt 3: die Hypothese prüfen

Jetzt kommt der reifste Teil des Prozesses: die Hypothese. Eine Debugging-Hypothese ist eine Aussage, die sich durch Beobachtung prüfen lässt.

Eine schlechte Hypothese klingt so: „der Debugger ist dumm“, „vector ist kaputt“, „C++ ist komisch“. Eine gute Hypothese klingt so: „wir führen Ganzzahldivision aus, weil sum und v.size() ganzzahlige Typen sind“.

Damit eine Hypothese nützlich ist, muss sie zwei Fragen beantworten: „was genau ist falsch?“ und „wo werde ich das sehen?“.

Kleine Tabelle, die hilft, Symptome schnell in prüfbare Hypothesen zu verwandeln:

Symptom Häufige Hypothese Wie im Debugger prüfen
Falscher Durchschnitt/Prozente Ganzzahldivision Watch: sum, v.size(), Ausdruck sum / v.size()
Elemente fehlen/nicht alle verarbeitet Fehler in Schleifenbegrenzung (< vs <=) Watch: Index und size(), Breakpoint innerhalb der Schleife
Gelegentlicher Absturz Out-of-Bounds bei vector Breakpoint beim Zugriff, watch i und size()
Branch if „funktioniert nicht“ Bedingung ist anders als gedacht Watch des Bedingungsausdrucks, step durch die Zweige
„Hängt“ Wartet auf Eingabe Aktuelle Zeile ansehen: oft ist das std::cin >> ...

Wichtig: Wir versuchen nicht, „die Korrektur zu erraten“. Wir versuchen, die Ursache zu bestätigen. Die Korrektur ist der nächste Schritt und wird meist offensichtlich, sobald die Ursache bestätigt ist.

5. Mini-Case: Durchschnitt in ExpenseTracker

Angenommen, in unserer App gibt es den Befehl avg, der den Durchschnitt ausgibt. Wir haben Code geschrieben und merkwürdige Ergebnisse erhalten.

So könnte unsere vereinfachte Berechnung aussehen:

#include <vector>

double average_expense(const std::vector<int>& v) {
    int sum = 0;
    for (int x : v) sum += x;
    return sum / v.size(); // hier entsteht der Bug
}

Reproduktion

Wir haben bereits die minimale Eingabe gefunden: {100, 101}. Sie sollte 100.5 ergeben, gibt aber 100. Das ist eine stabile Szene.

#include <iostream>
#include <vector>

double average_expense(const std::vector<int>& v);

int main() {
    std::vector<int> expenses{100, 101};
    std::cout << average_expense(expenses) << '\n'; // 100
}

Isolation

Die Isolation ist bereits gemacht: wir haben eine Funktion und einen Aufruf. Keine Menüs, kein Parsing, kein hübsches Formatting.

Hypothese

„Hypothese: sum / v.size() wird als Ganzzahldivision ausgeführt, weil beide Operanden ganzzahlig sind und der Bruchteil bereits vor der Zuweisung an double wegfällt“.

Hypothesenprüfung im Debugger

Was wir tun (in Handlungen, nicht in Tastenkombinationen):

Sie setzen einen Breakpoint auf die Zeile return sum / v.size()();, starten im Debugger, erreichen diese Zeile und schauen sich die Werte an.

In watches/locals interessieren Sie drei Dinge: sum, v.size() und das Ergebnis des Ausdrucks sum / v.size() „so wie es ist“.

Beim Stop sehen Sie ungefähr Folgendes:

  • sum == 201
  • v.size() == 2
  • sum / v.size() == 100 (und nicht 100.5)

Das ist der Beweis für die Hypothese: die Division ist bereits ganzzahlig.

Minimale Korrektur

Wir schreiben die Funktion nicht komplett um „aus Sicherheitsgründen“. Wir machen einen minimalen Fix, der genau die bestätigte Ursache beseitigt: wir sorgen dafür, dass die Division im Gleitkommaformat stattfindet.

#include <vector>

double average_expense(const std::vector<int>& v) {
    int sum = 0;
    for (int x : v) sum += x;

    return static_cast<double>(sum) / v.size(); // jetzt double-Division
}

Und der Testlauf:

#include <iostream>
#include <vector>

double average_expense(const std::vector<int>& v);

int main() {
    std::vector<int> expenses{100, 101};
    std::cout << average_expense(expenses) << '\n'; // 100.5
}

Beachten Sie den schönen Moment: wir haben nichts erraten. Wir haben Fakten gesehen, die Hypothese bestätigt, genau die Ursache behoben und das Szenario erneut reproduziert, um sicherzugehen, dass das Problem weg ist.

6. Randfälle: leere Liste und Verkleinern der Eingabe

Manchmal liegt der Fehler nicht in der Mathematik, sondern in Randfällen. Zum Beispiel wurde avg aufgerufen, obwohl noch keine Ausgaben vorhanden sind. Das Programm stürzt ab. Klassiker: Sie haben nicht daran gedacht, der User hat nicht daran gedacht, aber die Realität hat daran gedacht.

Typischer „naiver“ Code:

#include <vector>

double average_expense(const std::vector<int>& v) {
    int sum = 0;
    for (int x : v) sum += x;
    return static_cast<double>(sum) / v.size(); // wenn size()==0 → Division durch Null
}

Reproduktion

Wir brauchen eine Null-Eingabe. Direkt in main:

#include <iostream>
#include <vector>

double average_expense(const std::vector<int>& v);

int main() {
    std::vector<int> expenses;
    std::cout << average_expense(expenses) << '\n'; // Absturz/unerwartete Werte
}

Isolation

Sie ist schon da: wieder eine einzelne Funktion.

Hypothese

„Hypothese: v.size() ist gleich 0, und wir teilen durch Null“.

Prüfung

Breakpoint auf der Divisionszeile, watch v.size(). Beim Stop sehen Sie 0. Hypothese bestätigt.

Korrektur

Die Lösung an diesem Punkt des Kurses ist meist einfach: den leeren Fall explizit behandeln (in einem echten Projekt lässt sich diskutieren, was besser zurückzugeben ist, aber jetzt geht es uns um Vorhersagbarkeit).

#include <vector>

double average_expense(const std::vector<int>& v) {
    if (v.empty()) return 0.0;

    int sum = 0;
    for (int x : v) sum += x;
    return static_cast<double>(sum) / v.size();
}

Und wieder reproduzieren wir das Szenario: leere Liste → jetzt wird 0.0 ausgegeben (oder einfach 0, wenn die Ausgabe ohne Nachkommastellen formatiert wird).

Isolation durch Verkleinern der Eingabe

Oft kann man nicht einfach die Hälfte des Programms rausschneiden: z. B. tritt ein Bug nur bei einer bestimmten Befehlssequenz oder bei bestimmten Daten auf. Dann geschieht Isolation nicht so sehr durch „Code entfernen“, sondern durch Verkleinern der Eingabe.

Praktische Logik: wenn ein Bug bei 10.000 Elementen auftritt, versuchen Sie, die minimale Größe zu finden, bei der er noch auftritt. Das ist wie die Lautstärke runterdrehen: Sie reduzieren das Rauschen, bis das Problem noch hörbar ist.

Angenommen, Sie haben eine Suche nach „größter Ausgabe“, die manchmal falsche Antworten gibt. Code:

#include <vector>

int max_expense(const std::vector<int>& v) {
    int m = v[0];
    for (std::size_t i = 1; i < v.size(); ++i) {
        if (v[i] > m) m = v[i];
    }
    return m;
}

Wenn Ihnen gesagt wurde „manchmal falsch“, machen Sie zuerst die Reproduktion: finden Sie ein Set von Daten, bei denen es falsch ist. Dann isolieren Sie: reduzieren Sie das Set, bis die Falschheit bleibt. Häufig stellt sich heraus, dass der Fehler bei einem speziellen Randfall auftritt: z. B. bei negativen Werten, bei mehrfachen gleichen Maxima oder bei leerem vector. Dann wird die Hypothese konkret: „wir greifen auf v[0] zu, obwohl v leer ist“.

Debuggen im Debugger kann hier fast formell sein: Breakpoint auf int m = v[0];, watch v.size() — und Sie verstehen sofort, warum es gekracht hat.

7. Typische Fehler

Fehler Nr. 1: versuchen, einen Bug zu beheben, den man nicht reproduzieren kann.
Wenn Sie das falsche Verhalten nicht stabil erzeugen können, wird jede „Fix“-Versuch zur Kaffeesatzleserei. Manchmal scheint es, als hätten Sie gefixt, aber in Wirklichkeit hat sich der Bug einfach nicht gezeigt. Richtig ist: zuerst ein minimales Szenario erfinden, das das Programm jedes Mal zum Absturz bringt.

Fehler Nr. 2: „isolieren“ durch Umschreiben der Hälfte des Projekts.
Wenn das Problem unklar ist, neigt man zu radikalen Änderungen: „wir schreiben die Schleife um“ oder „nehmen eine andere Datenstruktur“. Das zerstört die Spuren der Ursache und erzeugt neue Bugs. Isolation bedeutet Reduktion: der Eingabe, des Ausführungswegs, der Anzahl beteiligter Funktionen.

Fehler Nr. 3: Hypothese im Stil „alles ist kaputt“.
Sätze wie „vector verhält sich komisch“ oder „die Bedingung funktioniert nicht“ sind nicht prüfbar. Eine gute Hypothese muss beobachtbar sein: „der Index wurde gleich size()“, „bei sum / n sind beide Operanden ganzzahlig“, „diesen if-Branch betreten wir nie“. Dann können Sie Breakpoints setzen und genau das anschauen, was nötig ist.

Fehler Nr. 4: die Hypothese an der falschen Stelle prüfen (falscher Frame / falscher Haltepunkt).
Wenn Sie in einer kleinen Funktion stoppen, die Ursache aber darin liegt, dass ihr falsche Argumente übergeben wurden, müssen Sie nicht nur lokale Variablen ansehen. Sie müssen im Call Stack nach oben gehen, den Frame wechseln und sehen, wo der Wert falsch wurde. Ansonsten ist es leicht, eine funktionierende Funktion fälschlich zu beschuldigen.

Fehler Nr. 5: v[i] im Watch ohne Grenzkontrolle ansehen.
Watches sind mächtig, aber sie retten Sie nicht vor Logikfehlern. Wenn i bereits außerhalb der Grenzen ist, kann der Versuch, v[i] anzusehen, Müll liefern oder seltsame Nebeneffekte des Beobachtens verursachen. Gute Praxis: zuerst i und v.size() beobachten, und erst wenn sicher ist, dass i < v.size(), das Element anschauen.

1
Aufgabe
C++ SELF, Level 34, Lektion 4
Gesperrt
Zwei Divisionen
Zwei Divisionen
1
Aufgabe
C++ SELF, Level 34, Lektion 4
Gesperrt
Teilbarkeitsbericht
Teilbarkeitsbericht
1
Aufgabe
C++ SELF, Level 34, Lektion 4
Gesperrt
Durchschnitt unter der Lupe
Durchschnitt unter der Lupe
1
Aufgabe
C++ SELF, Level 34, Lektion 4
Gesperrt
Spuren des Maximums
Spuren des Maximums
1
Umfrage/Quiz
Debugger, Level 34, Lektion 4
Nicht verfügbar
Debugger
Debugger
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION