www.codeworx.org/c++ tuts /Kongos rand() Tutorials, 4: Debugging in C++

Tutorial 4: Debugging in C++

Jedes Programm bzw. Spiel das man programmiert wird nicht von Anfang an laufen. Sicher werden sich irgendwo Fehler, egal ob logischer oder programmtechnischer Natur, eingeschlichen haben. Und da kommt das Debugging ins Spiel.

Normalerweise gibt es ja zwei Konfigurationen, die bei einem neuen Projektsetup erstellt werden. Nämlich die Konfiguration 'Win32 Debug' und 'Win32 Release'. Der Code ändert sich zwischen diesen beiden Konfigs zwar nicht, jedoch wird bei der Debug Konfiguration nicht auf Geschwindigkeit sondern eben auf Fehlerprüfungen gesetzt. Erst wenn die Debugläufe heil überstanden wurden, kann man das Endprodukt als Release weitergeben.

Es gibt unter Visual C++ natürlich den Debugger um Programme zu überprüfen. Wir gehen jedoch einen anderen Weg und werden das Debugging im Code direkt machen, denn zb. in Direct3D - Vollbild - Spielen kann man den Debugger schlecht verwenden.

Methode 1: Asserts

Dier erste Methode die es gibt um Code zu debuggen sind die Asserts. Es gibt mehrere Assert-Makros, jedoch ist nur eins für uns wichtig da alle anderen nur für MFC-Anwendungen gelten.

Der Vorteil von Asserts ist, dass sie leicht zu programmieren sind und im Release-Built einfach verschwinden. Man braucht sie nicht extra entfernen hierfür. Ein Nachteil ist, dass sie doch ein bisschen sehr langsam sind. Das ist jedoch verkraftbar.

Ein Assert-Makro überprüft nur, ob der angegebene Ausdruck wahr also true ist oder nicht. Wenn die übergebene Funktion oder Variable gleich 0 ist, wird das Proramm abgebrochen und es erscheint ein Dialog 'DEBUG ASSERTION FAILED!'. In diesem Dialogfeld wird angegeben, in welcher Quelldatei und in welcher Zeile der Fehler auftrat. Ein Assert-Makro funktioniert also ganz einfach:

   assert( Variable ); 

!!!ACHTUNG!!! Bei der Verwendung von Asserts kann ein Fehler passieren:

   FILE *p; 
   assert( p = fopen("file.txt", "r+") ); 

Im Debug-Built funktioniert alles. Die Datei 'file.txt' wird geöffnet. Jedoch im Release-Built wird ja genau diese Funktion aus dem Programm entfernt. Dadurch wird die Datei nie geöffnet und p bleibt NULL. Dies kann man jedoch verhindern, indem man anstatt:

   assert( Variable ); 

lieber das Makro:

   verify( Variable ); 

Diese Makro wird auch im Release-Built ausgeführt jedoch zeigt es bei einem Fehler keinen Dialog an.

Methode 2: Ausnahmebehandlung oder Exception Handling

Diese Methode zur Behandlung von Fehlersituationen ist nicht umbedingt auf allen Compilern verfügbar. Der VC++ Compiler verfügt jedenfalls über diese Methode.

Diese Methode teilt sich in insgesamt drei Abschnitte auf:

1. der try-Block
2. der throw-Ausdruck
3. der catch-Ausdruck oder auch auch Exception Handler genannt


Ein Beispiel:

void func1() 
{ 
   ... 
   if (g_pVar == NULL) 
   throw 0; 
} 
   
void main() 
{ 
   
   try 
   { 
      func1(); 
   } 
   
   catch (int) 
   { 
   ... 
   } 
} 

Die Erklärung ist ganz einfach: Wenn in der Funktion func1() die Variable g_pVar gleich NULL ist, wirft diese Funktion einen Fehler aus. Dieser Fehler ist hier die Zahl 0 (also ein Wert von Typ int). Dadurch, dass wir die Funktion in einem try-Block untergebracht haben, wird diese Fehlermeldung aufgehoben. Im catch-Block überprüfen wir nun, ob ein Fehler ausgeworfen wurde. Genauer: Wir überprüfen, ob ein Fehler vom Typint ausgeworfen wurde. Wenn dies passiert ist, wird der catch-Block ausgeführt.

Man kann auch mehrere catch-Blöcke hintereinander schreiben:

class CErr { ...}; 
   
try 
{ 
   // Anweisungen 
} 
   
catch(int) { // Fehlerbehandlung } 
catch(char*) { // Fehlerbehandlung } 
catch(CErr) { // Fehlerbehandlung } 
catch(...) { // Fehlerbehandlung } 

Der letzte catch-Block zeigt, dass es auch möglich ist den Platzhalter ... anstelle konkreter Typbezeichnungen anzugeben. Dieser unspezifische Block kann dazu verwendet werden, um all jene Fehlertypen zu behandeln, die von den spezialisierten catch-Blöcken nicht erfasst werden.

Wenn man nun in einem try-Block mehrere int Werte auswerfen kann, weiss der catch-Block ja nicht, wo genau der Fehler auftratt. Um auf den ausgeworfenen Wert Zugriff zu erlangen schreibt man eben dies:

catch(int ErrVal) { // Fehlerbehandlung } 

Nun kann man durch die Variable ErrVal auf den ausgeworfenen int Wert zugreifen.

Erweiterte Deklaration

Um, z.B. in einem Header, den Programmierer wissen zu lassen, dass eine Funktion einen Fehler auswerfen kann, kann man dies explizit kennzeichnen. In einem Header könnte also zb. diese stehen:

int Func1(int x) throw(float); 
int Func2(int x) throw(int, char*); 
int Func3(double y) throw(); 

Dadurch erkennt man, dass Func1 einen float auswerfen kann, Func2 einen int und einen String, die Funktion Func3 aber gar keinen Fehler auswirft. Diese Zusatzangaben bei der Deklaration, müssen dann aber auch bei der Funktionsdefinition stehen.

Diese Art des Debuggen ist nicht nur für den Debug-Built geeignet, sondern auch für die Release-Version. Jedoch sollte man sie nur an Schlüsselstellen verwenden.

Methode 4: Log-Dateien

Die Verwendung von Log-Dateien ist eine meiner liebsten Debugging Methoden. Man deklariert einfach eine globale Log-Datei und schreibt in diese Informationen oder Errorstrings. Um noch zusätzlich die genaue Fehlerquelle zu ermitteln, gibt es unter C++ zwei Makros die uns dabei helfen:

__LINE__ und __FILE__ 

Das Line-Makro wird dabei in eine dezimale Ganzzahlkonstante expandiert, die die Zeilennummer der Quelldatei angibt. Das File-Makro wird in eine Zeichenfolge (eingeschlossen von Anführungszeichen) expandiert, die die Quelldatei angibt. Dadurch kann man die Fehlerquelle sehr genau orten.

!!!ACHTUNG!!! Bei der Verwendung von einer Dateiausgabe, mit z.B. fprintf, sollte die Datei nach der Ausgabe immer wieder geschlossen werden, damit auch wirklich der String noch geschrieben wird. Wird die Datei nicht geschlossen, kann bei einem Abbruch des Programmes, wegen eines Fehlers, Die Log-Datei leer sein.

Ein Beispiel für diese Debugging-Methode findest du zum downloaden in der Zip-Datei weiter unten. Dieser Code stammt aus meiner etwas älteren D3D Engine.

Methode 5: Eine Debug-Klasse

Diese Debug-Klasse ist eine Abwandlung der Debug-Klasse aus dem Artikel von André LaMothe (siehe unten).

Das Ziel ist, mit wenig Code Variablen auf ihre Werte zu überprüfen. Wie könnte man das woll machen? Ganz einfach! Wir entwickeln eine Klasse, die die Adressen dieser Variablen speichern und diese zu bestimmten Zeiten in eine Log-Datei schreiben.

Die Funktionen

CDebug(LPSTR psFile = "debug.txt");

Konstruktor. psFile ist die Datei, in die geschrieben wird

~CDebug();

Destruktor

void EnableOutput();

Erlaubt das Schreiben in die Datei.

void DisableOutput();

Verbietet das Schreiben in die Datei. Wenn nach einem Aufruf von DisableOutput() die Funktion Print() oder UpdateWatches() augerufen wird, wird das Schreiben verhindert.

BOOL OutputState();

Gibt den Status zurück, ob man in die Datei schreiben darf.

SetTimeOutput(BOOL bTime);

Setzt den Status, ob man die Systemzeit in die Datei schreiben soll, bei einem Aufruf von Print() oder UpdateWatches().

BOOL GetTimeOutput();

Gibt den Status zur´ück, ob die Systemzeit geschrieben wird.

void Print(LPSTR psString);

Schreibt den String in die Datei.

void UpdateWatches();

Schreibt alle Variablen, die in der Klasse registriert wurden, in die Datei.

int AddWatch(VOD *pData, int iType, LPSTR psName = " ");

Fügt einen Zeiger einer Variable der Liste hinzu. pData ist dabei der Zeiger auf die Variable. iType der Typ von Variable. psName der Name der zusätzlich in die Datei geschrieben werden soll.

Mögliche Werte für iType sind:

DEBUG_WATCH_CHAR, 
DEBUG_WATCH_UCHAR, 
DEBUG_WATCH_SHORT, 
DEBUG_WATCH_USHORT,    
DEBUG_WATCH_INT, 
DEBUG_WATCH_UINT, 
DEBUG_WATCH_STRING, 
DEBUG_WATCH_FLOAT, 
DEBUG_WATCH_PTR
void DeleteWatches();

Löscht alle Zeiger auf die Variablen.

Download der Source Codes

Links

André LaMothe: Xtreme Debugging: Using the Monochrome Display to Debug Your Apps

Copyright (c) by Kongo

Bei Fragen, Beschwerden, Wünschen schreib an kongo@codeworx.org

Tutorial vom 16.07.2001