1. Warum eine Checkliste für die Fehlersuche wichtig ist
Wenn ein Programm abstürzt, fängt das Gehirn sehr schnell an, zwei Spiele zu spielen: „jetzt tippe ich die Ursache“ und „jetzt repariere ich das schnell“. Beide Spiele fühlen sich gut an, weil sie Kontrolle suggerieren, aber in Wirklichkeit führen sie oft dazu, dass man Symptome behandelt und nicht die Ursache. Die Checkliste soll dazu dienen, dass Sie wie ein Ingenieur handeln: reproduzierbar, mit Festhalten der Fakten und mit minimaler Magie.
Die nützlichste Denkänderung bei der Untersuchung ist, aufzuhören, einen Bug als „Ereignis“ zu sehen, und stattdessen ihn als Prozess zu betrachten. Ein Prozess besteht aus kleinen Schritten, und jeder Schritt sollte etwas Prüfbares hinterlassen: Reproduzierbarkeit, ein minimales Szenario, einen konkreten Punkt im Code, einen konkreten Fix und eine kurze Prüfung, dass das Problem nicht zurückkehrt.
Unten folgt die allgemeine Route, die wir durchgehen und am Code »landen« lassen:
flowchart TD
A[Reproduzieren] --> B[Signal festhalten: ASan/assert/crash]
B --> C[Szenario minimieren]
C --> D[Ursache lokalisieren]
D --> E[Fix anwenden]
E --> F[Unter Diagnostik erneut prüfen]
F --> G[Regressionsprüfung: Szenario darf nicht zurückkehren]
Damit das richtig »working« ist, behalten Sie im Kopf eine kleine Spickzettel‑Tabelle:
| Schritt | Was Sie erreichen wollen | Was Sie aufbewahren (Artefakt) | Woran Sie erkennen, dass der Schritt abgeschlossen ist |
|---|---|---|---|
| Reproduzieren | Der Bug tritt gleich auf | Eingabe/Command/Bedingungen | stürzt in 3–5 Läufen hintereinander ab / tritt auf |
| Minimieren | Das kürzeste Szenario, das immer noch bricht | kurze Eingabe/Stück Code | Unnötiges entfernt, der Fehler bleibt |
| Lokalisieren | Die Stelle, an der es „zuerst falsch wurde“ | konkrete Zeile/Funktion/Vertrag | Sie können die Ursache mit Worten formulieren |
| Fix anwenden | Beseitigung der Primärursache | diff / Codeänderung | Diagnostik schweigt, Verhalten korrekt |
| Regressionsprüfung | Der Bug kommt nicht still zurück | assert/Self‑Check | Wenn man den alten Bug wiederherstellt — die Prüfung schlägt fehl |
2. Reproduzieren: aus „manchmal stürzt es ab“ ein „stürzt immer ab“ machen
Das Wort „reproduzieren“ klingt, als würde man Musik auflegen, aber in Wahrheit ist es der am meisten unterschätzte Teil der Untersuchung. Solange Sie keine stabile Reproduktion haben, können Sie Ursache und Zufall nicht unterscheiden. Sie ändern den Code, der Bug „verschwindet“, und das Gehirn schließt freudig „repariert“. Und zwei Tage später kommt der Bug zurück, weil Sie nur aufgehört haben, den richtigen Timing‑ oder Speicherzustand zu treffen.
Festhalten, „wie genau zu starten“
Fangen Sie einfach an: ein Kommando / eine Eingabe / ein Szenario. In Lern‑CLI‑Programmen bedeutet das typischerweise: entweder eine feste Konsoleneingabe, oder Lesen aus einer String, oder ein spezieller Schalter --demo.
Wir setzen unser Lernprogramm fort — den konsolenbasierten Mini‑Task‑Manager TaskBook, in dem Aufgaben in einem std::vector liegen. Angenommen, wir haben einmal eine „praktische Optimierung“ eingeführt: einen Pointer auf die ausgewählte Aufgabe speichern, um schnell ihre Details zu drucken. Und plötzlich bekamen wir einen mysteriösen Absturz beim Hinzufügen neuer Tasks.
Hier ist ein minimales Demo‑Fragment einer schlechten Idee, die wir stabil reproduzieren möchten:
#include <iostream>
#include <string>
#include <vector>
struct Task { int id; std::string title; };
int main() {
std::vector<Task> tasks{{1, "Write report"}};
Task* selected = &tasks[0]; // remembered address of vector element
tasks.push_back({2, "Drink tea"}); // may reallocate and "move"
std::cout << selected->title << '\n';// UB: selected may become dangling
}
Es geht nicht darum, „was UB ist“ (das wurde schon früher besprochen), sondern darum, dass Sie einen kontrollierten Start brauchen, der das Problem reproduzierbar macht.
Deterministisches Reproduzieren erreichen
Manchmal reicht eine einzige Eingabe nicht: der Bug hängt davon ab, wie genau der Speicher umverteilt wurde. Dann ist Ihr Ziel nicht „schön machen“, sondern „vorhersehbar machen“.
Zum Beispiel kann man bewusst Bedingungen schaffen, unter denen ein vector sehr wahrscheinlich umverteilt: die capacity begrenzen.
#include <iostream>
#include <string>
#include <vector>
struct Task { int id; std::string title; };
int main() {
std::vector<Task> tasks;
tasks.reserve(1); // try to make capacity = 1 (or close)
tasks.push_back({1, "A"});
Task* selected = &tasks[0];
tasks.push_back({2, "B"}); // high chance of reallocation
std::cout << selected->title << '\n';// UB (might "luck out", but it's a signal)
}
Ja, das erinnert ein bisschen an das Aufsetzen eines Laborexperiments. Und das ist in Ordnung: während der Untersuchung schreiben Sie keinen perfekten Prod‑Code, Sie setzen das Experiment gegen den Bug.
Was ins „Repro‑Pass“ gehört
Super‑Praxis: halten Sie wörtlich als Kommentar (oder in einer separaten Notiz) drei Dinge fest: welche Eingabe, welches Kommando, welches erwartete Signal (ASan/crash/assert).
Im Code kann das so aussehen:
// Repro: run -> crashes/ASan complains after push_back
// Scenario: store pointer to tasks[0], then push_back one more task, then read old pointer.
Das ist keine Bürokratie. Das ist Versicherung gegen die Situation „gestern ist es abgestürzt, heute nicht, und ich weiß nicht mehr wie“.
3. Minimierung: alles entfernen außer der Ursache
Minimierung heißt, Sie nehmen ein großes, furchteinflößendes Projekt, in dem „irgendwo beim Beenden etwas abstürzt“, und verwandeln es in 15 Zeilen, in denen „es beim zweiten push_back abstürzt“. Dieser Schritt spart Stunden, weil er die Suchfläche und die Zahl möglicher Ursachen reduziert. Und ja: Minimierung ist fast immer wichtiger als der erste Versuch, „sofort zu reparieren“.
Minimierung der Eingabe und des Codes sind unterschiedliche Dinge
Manchmal tritt das Problem bei einer spezifischen Eingabe auf: lange String, große Datei, spezielle Befehlsreihenfolge. Dann minimieren Sie die Eingabe, ohne den Code anzufassen. Manchmal entsteht das Problem aufgrund der Architektur: ein Pointer wird lange gehalten, ein Container verschiebt sich, Speicher wird freigegeben. Dann minimieren Sie den Code, selbst wenn die Eingabe gleich bleibt.
Für TaskBook ist es oft sinnvoller, den Code zu minimieren: wir brauchen nicht das ganze CLI, Menü, Command‑Parser. Wir brauchen nur: vector, den Pointer, push_back, und das Lesen über den alten Pointer.
Die »eins nach dem anderen«‑Technik
Wenn Sie sofort die Hälfte des Programms entfernen, entfernen Sie möglicherweise auch versehentlich den Bug und verstehen später nicht, was wichtig war.
Gute Regel: machen Sie Änderungen so, dass Sie nach jedem Schritt ehrlich sagen können: „der Bug ist weiterhin reproduzierbar“. Das ist wie ein Jenga‑Spiel: ziehen Sie einen Block und prüfen Sie, ob der Turm noch steht.
Zum Beispiel: wenn der Bug ursprünglich in der Funktion zum Drucken der ausgewählten Aufgabe auftrat, können Sie temporär die »schöne Ausgabe« durch eine einzige Zeile ersetzen. Das ist kein »finaler Code«, sondern eine Taschenlampe im Dunkeln:
#include <iostream>
#include <string>
struct Task { int id; std::string title; };
void print_selected(const Task* t) {
std::cout << t->title << '\n'; // if t is dangling — it will crash here / ASan will complain
}
Jetzt bleiben im minimalen Szenario nur die wichtigen Rollen: das Speichern des Pointers und der Moment der Nutzung.
Minimierung, um falsche Ursachen zu entlarven
Die häufigste Falle: Sie sehen einen Absturz „im Destruktor eines Strings“ oder „innerhalb von vector“ und denken, der String sei kaputt. Minimierung zeigt oft, dass der String nichts damit zu tun hat, sondern dass Sie auf Speicher nach einem Move/Free zugreifen.
Psychologisch ist das nützlich: bei kleinem Code hört das Gehirn auf zu fantasieren und sieht kausale Zusammenhänge.
4. Dokumentation: die Untersuchung reproduzierbar machen
Dokumentation bedeutet, zu sagen: „dieses konkrete kleine Szenario ist unser Referenz‑Bug“. Das ist nicht nur nützlich, um «jetzt» zu reparieren, sondern damit man sich nicht in einer Woche selbst widerspricht: „was war eigentlich kaputt?“
Wichtig ist hier die Unterscheidung zweier Dokumentationen: das Festhalten des Szenarios und das Festhalten des erwarteten Verhaltens.
Szenario im Code festhalten: demo_*
In Lernprojekten ist es praktisch, solche Dinge als separate Funktionen zu halten, die man an‑/abschalten kann. Zum Beispiel:
#include <iostream>
#include <string>
#include <vector>
struct Task { int id; std::string title; };
void demo_dangling_pointer() {
std::vector<Task> tasks{{1, "A"}};
Task* selected = &tasks[0];
tasks.push_back({2, "B"});
std::cout << selected->title << '\n'; // UB (demo‑bug)
}
int main() {
demo_dangling_pointer();
}
Das ist aktuell ein »Demo‑Bug«. Nach der Korrektur kann dieselbe Funktion zur »Demo‑Prüfung« werden: sie darf nicht mehr abstürzen und muss korrekt arbeiten (oder bei Verletzung des Vertrags durch Ihr assert bewusst abklingen).
Das erwartete Verhalten festlegen
Hier beginnt die erwachsene Ingenieursarbeit: Sie müssen festlegen, was die Anwendung in dieser Situation tun soll.
Optionen (auf Prinzip‑Ebene, ohne großes Design‑Dokument):
- Wenn Ihr Vertrag sagt „wir speichern keinen rohen Pointer auf ein vector‑Element“, dann ist das korrekte Verhalten — diese Möglichkeit im Code nicht zuzulassen. Die Dokumentation wäre dann: „nach Refactoring verschwindet der Pointer, die ausgewählte Aufgabe wird anders gehalten“.
- Wenn Ihr Vertrag sagt „wir speichern selectedIndex und holen das Element jedes Mal neu“, dann ist korrektes Verhalten: die Aufgabe über den Index drucken und vor dem Zugriff Grenzen prüfen.
- Wenn Ihr Vertrag sagt „die ausgewählte Aufgabe soll unabhängig vom Container leben“, dann ist korrektes Verhalten: eine Kopie (oder Ownership) speichern, nicht die interne Adresse.
Wir gehen heute nicht in die vollständige Ownership‑Architektur (das sind eigene Kurstage), aber die Idee, das Verhalten zu spezifizieren, ist wichtig: ohne Spezifikation wird ein »Fix« schnell zu einer zufälligen Sammlung von Änderungen.
5. Regressionsdenken: verhindern, dass der Bug zurückkommt
Regressionsdenken ist die Gewohnheit, nicht zu fragen „wie repariere ich jetzt“, sondern „wie sorge ich dafür, dass das nicht wieder kaputt gemacht wird“. Das ist keine Paranoia, sondern Zeitersparnis. Wenn ein Bug einmal gefunden wurde, hat er bewiesen, dass er realistisch ist. Die Wahrscheinlichkeit, dass er zurückkehrt, ist nicht null — besonders, wenn das Team wächst oder Sie die Details in einem Monat vergessen.
Kleine Regressionsprüfung ohne Testframeworks: self_check() auf assert
In diesem Kurs binden wir keine Testframeworks ein, deshalb benutzen wir den einfachsten „Nagel an die Wand“ — eine kleine Self‑Check mit assert.
Wenn Sie sich entscheiden, die ausgewählte Aufgabe als Index zu speichern, könnte die Regressionsprüfung so aussehen:
#include <cassert>
#include <string>
#include <vector>
struct Task { int id; std::string title; };
void self_check_selected_index() {
std::vector<Task> tasks{{1, "A"}};
std::size_t selected = 0;
tasks.push_back({2, "B"}); // reallocation is not scary anymore
assert(tasks.at(selected).title == "A"); // check expected behavior
}
Achten Sie auf ein Detail: at() ist hier nützlich als »boundary shot«. Wenn der Index falsch wird, bekommen Sie ein klares Signal (Exception oder Diagnostics, je nach Umgebung), statt stiller UB.
Regressionsdenken als Designbarriere
Manchmal ist die beste Regression nicht ein assert, sondern ein Verbot eines falschen Zustands.
Wenn es in Ihrem Projekt zum Beispiel die Regel gibt „speichere keine Task* auf Elemente von vector<Task>“, dann können Sie das durch API‑Stil unterstützen: Funktionen geben TaskId oder Index zurück, aber niemals die Adresse eines Elements. Ja, das klingt wie Architektur, aber selbst in einem kleinen Projekt kann man den Schritt machen: nicht mehr die Adressen der Elemente nach außen geben.
Im Lern‑TaskBook sieht ein einfacher Schritt so aus: statt find_task(...), das einen Pointer zurückgibt, macht man find_task_index(...) und arbeitet weiter über den Container.
Merkzettel für eine Bildschirmseite
Wenn Sie mitten in einer Untersuchung stecken, ist der Kopf oft voll mit Emotionen: „warum ist es wieder abgestürzt“, „ich habe doch nichts geändert“, „der Computer hasst mich“. Dann ist ein kurzer Merkzettel nützlich, der Sie zurück auf die Schiene bringt.
Unten ist derselbe Prozess, aber operativer, mit Formulierungen im Stil „mach → erhalte → prüfe“.
flowchart TD
A[1 Reproduzieren] --> A1[Notiere Eingabe/Kommando/Bedingungen]
A1 --> A2[Stabilität erreichen: 3–5 mal hintereinander]
A2 --> B[2 Minimieren]
B --> B1[Eingabe kürzen / unnötigen Code entfernen]
B1 --> B2["Minimalstes Szenario (MRE) belassen"]
B2 --> C[3 Lokalisieren]
C --> C1[Erste Stelle im eigenen Code finden]
C1 --> C2[Primärursache mit Worten formulieren]
C2 --> D[4 Fix anwenden]
D --> D1[Änderung, die die Ursache und nicht das Symptom entfernt]
D1 --> E[5 Erneut prüfen]
E --> E1[Unter Diagnostik / mit assert laufen lassen]
E1 --> F[6 Regressionsprüfung]
F --> F1[Kurz‑self_check für das Szenario]
Das Wichtigste: wenn Sie sich denken „ich weiß doch sowieso, was nicht stimmt“, versuchen Sie trotzdem, die Checkliste durchzugehen. Sie fängt erstaunlich oft Lücken: zum Beispiel stellt sich heraus, dass die Reproduktion instabil ist und Sie einen halben Tag an einem nicht existenten Bug gearbeitet haben.
6. Praktisches Beispiel: vom Bug zur Regressionsprüfung
Wir fassen die Geschichte in einer kompakten Sequenz zusammen: es war UB durch dangling pointer; wir entschieden uns für die Lösung „Index speichern“; wir fügten eine Self‑Check hinzu.
Schlechte Version
#include <iostream>
#include <string>
#include <vector>
struct Task { int id; std::string title; };
int main() {
std::vector<Task> tasks{{1, "A"}};
Task* selected = &tasks[0];
tasks.push_back({2, "B"});
std::cout << selected->title << '\n'; // UB
}
Gefixte Version und Regressionsprüfung
#include <cassert>
#include <string>
#include <vector>
struct Task { int id; std::string title; };
void self_check() {
std::vector<Task> tasks{{1, "A"}};
std::size_t selected = 0;
tasks.push_back({2, "B"});
assert(tasks.at(selected).title == "A");
}
int main() {
self_check();
}
Das ist nicht „die ganze Architektur der Welt“. Das ist ein minimales Beispiel für den Regressionsansatz: Sie reparieren nicht nur den Code, Sie lassen ein kleines Leuchtfeuer zurück, das sagt „wieder kaputt gemacht“.
7. Typische Fehler bei der Untersuchung
Fehler Nr. 1: mit dem »Reparieren« anfangen, bevor eine stabile Reproduktion erreicht ist.
Wenn ein Bug „mal ja, mal nein“ auftritt, möchte man gerne sofort Änderungen vornehmen — das scheint das Tempo zu erhöhen. In der Praxis bekommen Sie ein Roulette: Sie ändern Code, der Bug tritt nicht mehr auf, Sie denken, Sie haben es erledigt, und am nächsten Tag ist er wieder da. Stabile Reproduktion ist Ihr Null‑Kilometer; ohne sie laufen Sie einfach ohne Karte durch den Wald.
Fehler Nr. 2: zu aggressiv minimieren und den Bug verlieren.
Manche entfernen die Hälfte des Programms in einem Rutsch, der Bug verschwindet, und Panik entsteht: „also war es das doch nicht“. Meistens haben Sie nur versehentlich ein wichtiges Element des Szenarios entfernt. Minimieren Sie schrittweise und prüfen Sie nach jeder Änderung, ob der Fehler noch da ist — sonst verlieren Sie die kausale Verbindung.
Fehler Nr. 3: die Absturzstelle »reparieren«, aber die Primärursache ignorieren.
Es ist verlockend, einen Absturz bei der Zeile std::cout << ... zu sehen und sofort zu denken „also ist das Ausgabeproblem“. Oft ist die Ausgabe nur die erste Stelle, die mit beschädigtem Speicher in Berührung kommt. Wenn Sie das Symptom behandeln, kann der Bug an einer anderen Stelle wieder auftauchen und Sie denken, ein neuer Bug sei entstanden. Meist ist es derselbe alte Bug, nur mit anderem Erscheinungsbild.
Fehler Nr. 4: »es stürzt nicht mehr« als Beweis für die Reparatur ansehen.
Beharrliche Bugs sind tückisch, weil sie lange stillhalten können. Sie ändern leicht die Reihenfolge der Operationen — und UB tritt nicht mehr auf. UB muss sich nicht zuverlässig zeigen; das ist seine Gefahr. Darum braucht man nach der Reparatur eine erneute Prüfung unter Diagnostik und eine Regressionsfestlegung, auch wenn diese nur ein einfacher self_check() ist.
Fehler Nr. 5: nach der Untersuchung keine Artefakte (Szenario und Test) hinterlassen.
Wenn nach der »Reparatur« weder ein minimales Szenario noch ein Kommentar „wie reproduzieren“ noch eine kleine Regressionsprüfung übrig bleiben, war die Untersuchung nur ein einmaliges Heldentum. Schön, aber teuer: beim nächsten Mal leiden Sie wieder heldenhaft. Regressionsdenken heißt, einmal leiden — nicht regelmäßig.
GO TO FULL VERSION