CodeGym /Kurse /C++ SELF /Breakpoints — normale und bedingte

Breakpoints — normale und bedingte

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

1. Wie Breakpoints funktionieren

Wenn ein Programm sich falsch verhält, möchte man gern „überall std::cout << „Ich bin hier!” setzen“ — das ist eine natürliche Reaktion von jemandem, der einen Bug zum ersten Mal sieht. Das Problem ist, dass nach 15 Minuten solcher Einfügungen ein „Dschungel aus Prints“ entsteht, dessen Entfernen schmerzhafter ist als ein Zahnarztbesuch. Breakpoint ist ein saubererer Weg: man hält das Programm an einer Stelle an und betrachtet den tatsächlichen Zustand, ohne den Code umzuschreiben oder die Ausgabe zuzumüllen.

Vereinfacht gesagt ist ein Breakpoint wie das Pausieren des Programms in einer konkreten Zeile, um sich in Ruhe umzuschauen: welche Werte haben Variablen, wie sind wir hierhergekommen und warum ist der gesunde Menschenverstand schon längst aus dem Chat verschwunden.

Normaler Breakpoint und der Moment des Auslösens

Wichtig ist ein Detail, wegen dem Anfänger manchmal falsche Schlüsse ziehen: ein Breakpoint löst vor der Ausführung der Zeile aus, auf der er gesetzt ist. Der Debugger sagt also: „Gleich werden wir diese Zeile ausführen. Ich halte an, damit du sehen kannst, was vor der Änderung war“.

Das ist praktisch, weil man den Zustand „vor“ und „nachher“ vergleichen kann. Wenn zum Beispiel eine Zeile einen Zähler erhöht, sieht man beim Breakpoint den alten Wert und nach dem Step den neuen (Steps werden in der nächsten Vorlesung ausführlich erklärt; heute behalten wir die Idee nur im Kopf).

Ein Mini-Beispiel, nur um das Anhalten zu spüren:

#include <iostream>

int main() {
    int x = 10;
    x = x + 5;                 // breakpoint hier: x noch 10
    std::cout << x << '\n';    // Ausgabe: 15
}

Wenn man den Breakpoint auf die Zeile x = x + 5; setzt, ist bei Stoppen x noch gleich 10. Nach Ausführung der Zeile wird es 15.

Wo Breakpoints setzen: logische Knoten im Code

Es ist verführerisch, Breakpoints „irgendwohin“ zu setzen — und sich dann zu wundern, warum man 200 mal anhält und trotzdem nichts verstanden hat. Gute Gewohnheit: Breakpoints dort setzen, wo Entscheidungen getroffen werden oder Zustand geändert wird.

Das sind üblicherweise Stellen, an denen der Code „abbiegt“ (Bedingungen), „sich wiederholt“ (Schleifen) oder „ein Ergebnis festschreibt“ (Rückgaben aus Funktionen). Man sollte das nicht als Dogma betrachten — eher wie Verkehrsschilder: man setzt sie, wo wirklich eine Gabelung oder eine gefährliche Kurve ist.

Nachfolgend eine kleine Spickzettel-Tabelle. Man muss sie nicht auswendig lernen, aber es ist nützlich, kurz davor einen Blick darauf zu werfen.

Stelle im Code Warum praktisch Typische Frage
Eingang in die Funktion Argumente sind sofort sichtbar „Habe ich wirklich das bekommen, was ich erwartet habe?“
Vor if Bedingung und Werte sind sichtbar „Warum sind wir in diesem Branch gelandet?“
Innerhalb der Schleife bei Zustandsänderung Man kann den Moment des Fehlers erwischen „In welcher Iteration ist alles seltsam geworden?“
Vor return Ergebnis überprüfen „Warum wird genau das zurückgegeben?“

Breakpoints verwalten: setzen, deaktivieren, löschen

In jeder IDE/Debugger (egal ob CLion, Visual Studio, VS Code, Qt Creator — das Prinzip ist dasselbe) hat ein Breakpoint mindestens drei „Lebenszustände“.

Zuerst setzt man einen Breakpoint — meist per Klick links von der Zeile (im Gutter, neben der Zeilennummer). Dann kann man ihn löschen, wenn er nicht mehr gebraucht wird. Und es gibt eine dritte, von Anfängern stark unterschätzte Option: man kann einen Breakpoint vorübergehend deaktivieren.

Deaktivieren bedeutet, dass der Punkt in der Liste bleibt, das Programm dort aber nicht anhält. Das ist nützlich, wenn man schnell „den Lärm dämpfen“ möchte, aber sich später nicht fragen will: „Wo hatte ich diese wichtige, fast richtige Marke gesetzt?“

Ein hilfreiches mentales Modell: Löschen ist wie „die Markierung auf der Karte löschen“, Deaktivieren ist wie „das Fensterlädchen zuziehen“.

Ein weiterer praktischer Punkt: wenn ein Breakpoint nicht ausgelöst wurde, ist das nicht unbedingt ein Debugger-Fehler. Meist ist die Ursache einfacher und ärgerlicher: die Ausführung ist einfach nicht bis zu dieser Zeile gekommen. Zum Beispiel wurde ein früheres return ausgeführt, die if-Bedingung hat den Weg verhindert oder man debuggt nicht die richtige Konfiguration/den richtigen Start.

Bedingte Breakpoints

Ein bedingter Breakpoint ist ein normaler Breakpoint mit Filter: „halte nur an, wenn die Bedingung wahr ist“. Das ist die Rettung bei Schleifen, großen Datenmengen und Situationen, in denen sich ein Fehler erst bei Iteration 15342 zeigt, und man nicht 15341 mal auf Continue klicken möchte.

Aber es gibt eine wichtige Disziplin: die Bedingung muss einfach und sicher sein. Ideale Prüfungen sind Indexvergleiche, Überprüfungen auf einen konkreten Wert, Prüfungen auf nullptr (wenn man zu Zeigern kommt), Grenzüberprüfungen. Eine schlechte Idee ist es, eine Bedingung zu schreiben, die den Programmzustand verändert. Debugging sollte nicht zu einer „Quest“ werden: rate, was der Debugger kaputt gemacht hat.

Kurzübersicht zur Orientierung:

Breakpoin ttyp Wann verwenden Beispielbedingung
Normal Wir wollen immer an dieser Stelle anhalten —
Bedingt Wir wollen nur in seltenen Momenten anhalten
i == 100, sum < 0, v[i] == target

3. Praxis: Bugs in PocketBudget aufspüren

Damit wir nicht einen „abstrakten Pferd-im-Vakuum“-Fall debuggen, machen wir weiter im Lernstil: Wir haben eine kleine Konsolenanwendung PocketBudget, die mit einer Liste von Ausgaben/Einnahmen arbeitet (einfache int-Werte in std::vector<int>). Keine Magie: Zahlen, Schleifen, Funktionen — alles bereits bekannt.

Szenario: Summe wird falsch berechnet

Angenommen, wir haben eine Summe-Funktion geschrieben, aber irgendwo einen Fehler gemacht. Hier eine minimalistische Version:

#include <cstddef>
#include <vector>

int total_sum(const std::vector<int>& a) {
    int s = 0;
    for (std::size_t i = 1; i < a.size(); ++i) { // BUG: starten bei 1
        s += a[i];
    }
    return s;
}

Sie ist beinahe korrekt… bis auf die Tatsache, dass sie mit i = 1 beginnt, also das Element an Index 0 überspringt. Das ist ein typischer „hab ich einfach nicht bemerkt“-Bug.

Nun das main, das diese Funktion verwendet:

#include <iostream>
#include <vector>

int total_sum(const std::vector<int>& a);

int main() {
    std::vector<int> ops{10, -3, -4};
    std::cout << total_sum(ops) << '\n'; // erwartet 3, bekommt aber -7
}

Wie helfen hier Breakpoints?

Man setzt einen normalen Breakpoint auf die Zeile s += a[i];. Startet das Debugging. Wenn es stoppt — schaut man: welche Werte haben i, s, a.size(), a[i]. Schon beim ersten Halt sieht man, dass i == 1, also das erste Element a[0] gar nicht berücksichtigt wird.

Wenn man nicht „oft anhalten“ möchte (weil der Vektor groß ist), setzt man einen bedingten Breakpoint auf die gleiche Zeile mit der Bedingung i == 1. Dann gibt es genau einen Halt: in der ersten Iteration. So prüft man schnell die Hypothese: „wir starten wirklich an der falschen Stelle“.

Szenario: eine seltene Iteration in einer großen Schleife erwischen

Manchmal ist ein Bug nicht offensichtlich: er tritt nur bei großen Datenmengen oder nur in einer bestimmten Iteration auf. Zur Demonstration nehmen wir ein synthetisches Beispiel:

#include <iostream>

int main() {
    long long s = 0;
    for (int i = 0; i < 1'000'000; ++i) {
        s += i; // bedingter breakpoint: i == 999'999
    }
    std::cout << s << '\n';
}

Wenn man einen normalen Breakpoint auf s += i; setzt, würde man eine Million Mal anhalten (und anfangen, Computer zu hassen). Ein bedingter Breakpoint i == 999'999 führt zu genau einem Halt — in der letzten Iteration — wo man den Zustand „direkt vor Beendigung“ der Schleife prüfen kann.

Szenario: Suche nach einem Element und zwei Breakpoints für zwei Ergebnisse

Jetzt ein Beispiel, das näher an realen Aufgaben ist: wir suchen die erste Operation, die das Limit übersteigt (z.B. „finde die erste Ausgabe größer als 100“).

#include <cstddef>
#include <vector>

int first_over_limit(const std::vector<int>& ops, int limit) {
    for (std::size_t i = 0; i < ops.size(); ++i) {
        if (ops[i] > limit) return static_cast<int>(i); // breakpoint A
    }
    return -1; // breakpoint B
}

Und das main:

#include <iostream>
#include <vector>

int first_over_limit(const std::vector<int>& ops, int limit);

int main() {
    std::vector<int> ops{10, 50, 120, 30};
    std::cout << first_over_limit(ops, 100) << '\n'; // 2
}

Hier ist es praktisch, zwei Breakpoints zu setzen: einen am „erfolgreichen return“, einen am return -1;. Dann beantwortet man bei jedem Lauf sofort die Frage: „Haben wir ein Element gefunden oder nicht?“.

Wenn gefunden — sieht man i, ops[i], limit. Wenn nicht gefunden — lief die Schleife bis zum Ende, und man muss herausfinden, warum die Bedingung nie wahr wurde (vielleicht ist das Limit falsch, vielleicht sind die Daten anders, vielleicht wurde Vergleich vertauscht).

Sehr praktische Technik: Breakpoint vor if

Manchmal ist man überzeugt, dass die Bedingung auslösen sollte, tut sie aber nicht. Klassiker. Dann ist ein Breakpoint direkt vor dem if der beste Freund:

#include <iostream>

int main() {
    int balance = -5;
    if (balance > 0) {                 // breakpoint: ist balance wirklich > 0?
        std::cout << "OK\n";
    } else {
        std::cout << "Not OK\n";        // Not OK
    }
}

Beim Breakpoint schaut man: welchen Wert hat balance? Vielleicht dachte man, er sei positiv, dabei ist er schon lange negativ — und das Problem liegt gar nicht im if, sondern bei dem, der den Wert zuvor zugewiesen hat. Der Breakpoint hilft, den Moment zu erwischen, in dem das „Weltbild“ nicht mehr zur Realität passt.

5. Typische Fehler beim Arbeiten mit Breakpoints

Fehler Nr. 1: Breakpoint auf eine Zeile setzen, die tatsächlich nicht ausgeführt wird.
Anfänger setzen manchmal einen Haltepunkt auf eine geschweifte Klammer, eine leere Zeile, einen Kommentar oder eine Variablendeklaration, die der Optimierer/Compiler so „zusammengefaltet“ hat, dass dort keine tatsächliche ausführbare Aktion ist. Dann wirkt der Breakpoint „kaputt“. Lösung: setze den Breakpoint auf eine Zeile mit sicherer Aktion — Zuweisung, Funktionsaufruf, return, Rumpf des if oder eine echte Zeile innerhalb der Schleife.

Fehler Nr. 2: erwarten, dass der Breakpoint bereits den geänderten Wert zeigt.
Da der Breakpoint vor der Zeile auslöst, sieht man oft den „alten“ Variablenwert. Das ist kein Bug — das ist normale Logik. Wenn man den „nachher“-Wert sehen will, setzt man den Breakpoint auf die nächste Zeile oder führt einen Step aus (Steps sind Thema der nächsten Vorlesung).

Fehler Nr. 3: Breakpoint zu früh setzen und in Stopps zu ertrinken.
Wenn man einen Breakpoint am Anfang einer Schleife mit 100000 Iterationen setzt, macht man sich faktisch ein Training im Drücken der Taste Continue. Viel effizienter ist es, den Breakpoint näher an die Stelle zu setzen, wo das Symptom auftritt, oder ihn bedingt nach Index/Wert zu machen.

Fehler Nr. 4: in der Breakpoint-Bedingung etwas „Cleveres“ schreiben, das gefährlich ist.
Die Versuchung ist groß: „Lass mich in der Bedingung eine Funktion aufrufen, die alles prüft“. Aber die Bedingung sollte maximal sicher sein und das Programm nicht verändern. Andernfalls debuggt man nicht das Programm, sondern die Folgen der eigenen Bedingung. Bleibe bei Prüfungen wie i == ..., x < 0, s.size() == ..., v[i] == target (aber nur, wenn man sicher ist, dass i im Bereich liegt).

Fehler Nr. 5: anhalten und sofort eine Schlussfolgerung ziehen, ohne den Kontext zu prüfen.
Ein Breakpoint erklärt den Fehler nicht von selbst. Er hält lediglich einen Moment fest. Wichtig ist, beim Anhalten die richtige Frage zu stellen: „Welche Bedingungen sollten in diesem Moment gelten?“ und diese zu prüfen. Wenn man nur variables starr anschaut ohne Hypothese, kann man lange Zahlen betrachten und ein Gefühl philosophischer Leere entwickeln.

1
Aufgabe
C++ SELF, Level 34, Lektion 1
Gesperrt
Zwei Haltepunkte
Zwei Haltepunkte
1
Aufgabe
C++ SELF, Level 34, Lektion 1
Gesperrt
Seltene Iteration
Seltene Iteration
1
Aufgabe
C++ SELF, Level 34, Lektion 1
Gesperrt
Fehlendes Element
Fehlendes Element
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION