Debugging
hints
In
an ideal environment, you have an excellent debugger available that easily
makes the behavior of your program transparent so you can quickly discover
errors. However, most debuggers have blind spots, and these will require you to
embed code snippets in your program to help you understand what’s going
on. In addition, you may be developing in an environment (such as an embedded
system, which is where I spent my formative years) that has no debugger
available, and perhaps very limited feedback (i.e. a one-line LED display). In
these cases you become creative in the ways you discover and display
information about the execution of your program. This section suggests some
techniques for doing this.
Debugging
flags
If
you hard-wire your debugging code into a program, you can run into problems.
You start to get too much information, which makes the bugs difficult to
isolate. When you think you’ve found the bug you start tearing out
debugging code, only to find you need to put it back in again. You can solve
these problems with two types of flags: preprocessor debugging flags and
run-time debugging flags.
Preprocessor
debugging flags
By
using the preprocessor to
#define
one or more debugging flags (preferably in a header file), you can test a flag
using an
#ifdef
statement and conditionally include debugging code. When you think your
debugging is finished, you can simply
#undef
the flag(s) and the code will automatically be removed (and you’ll reduce
the size and run-time overhead of your executable file).
It
is best to decide on names for debugging flags before you begin building your
project so the names will be consistent. Preprocessor flags are traditionally
distinguished from variables by writing them in all upper case. A common flag
name is simply DEBUG (but be careful you don’t use NDEBUG, which is
reserved in C). The sequence of statements might be:
#define DEBUG // Probably in a header file
//...
#ifdef DEBUG // Check to see if flag is defined
/* debugging code here */
#endif // DEBUG
Most
C and C++ implementations will also let you
#define
and
#undef
flags from the compiler command line, so you can re-compile code and insert
debugging information with a single command (preferably via the makefile, a
tool that will be described next). Check your local documentation for details.
Run-time
debugging flags
In
some situations it is more convenient to turn debugging flags on and off during
program execution, especially by setting them when the program starts up using
the command line. Large programs are tedious to recompile just to insert
debugging code.
To
turn the debugger on and off dynamically, create
bool
flags:
//: C03:DynamicDebugFlags.cpp
#include <iostream>
#include <string>
using namespace std;
// Debug flags aren't necessarily global:
bool debug = false;
int main(int argc, char* argv[]) {
for(int i = 0; i < argc; i++)
if(string(argv[i]) == "--debug=on")
debug = true;
bool go = true;
while(go) {
if(debug) {
// Debugging code here
cout << "Debugger is now on!" << endl;
} else {
cout << "Debugger is now off." << endl;
}
cout << "Turn debugger [on/off/quit]: ";
string reply;
cin >> reply;
if(reply == "on") debug = true; // Turn it on
if(reply == "off") debug = false; // Off
if(reply == "quit") break; // Out of 'while'
}
} ///:~
This
program continues to allow you to turn the debugging flag on and off until you
type “quit” to tell it you want to exit. Notice that it requires
that full words be typed in, not just letters (you can shorten it to letter if
you wish). Also, a command-line argument can optionally be used to turn
debugging on a startup – this argument can appear anyplace in the command
line, since the startup code in
main( )
looks at all the arguments. The testing is quite simple because of the
expression:
This
takes the
argv[i]
character array and creates a
string,
which then can be easily compared to the right-hand side of the
==.
The above program searches for the entire string
--debug=on.
You can also look for
--debug=
and then see what’s after that, to provide more options. The chapter on
the
string
class later in the book will show you how to do that.
Although
a debugging flag is one of the relatively few areas where it makes a lot of
sense to use a global variable, there’s nothing that says it must be that
way. Notice that the variable is in lower case letters to remind the reader it
isn’t a preprocessor flag.
Turning
variables and expressions into strings
When
writing debugging code, it is tedious to write print expressions consisting of
a character array containing the variable name, followed by the variable.
Fortunately, Standard C includes the
stringize
operator ‘
#’,
which was used earlier in this chapter. When you put a
#
before an argument in a preprocessor macro, the preprocessor turns that
argument into a character array. This, combined with the fact that character
arrays with no intervening punctuation are concatenated into a single character
array, allows you to make a very convenient macro for printing the values of
variables during debugging:
#define
PR(x) cout << #x " = " << x << "\n";
If
you print the variable
a
by calling the macro
PR(a),
it will have the same effect as the code:
cout
<< "a = " << a << "\n";
This
same process works with entire expressions. The following program uses a macro
to create a shorthand that prints the stringized expression and then evaluates
the expression and prints the result:
//: C03:StringizingExpressions.cpp
#include <iostream>
using namespace std;
#define P(A) cout << #A << ": " << (A) << endl;
int main() {
int a = 1, b = 2, c = 3;
P(a); P(b); P(c);
P(a + b);
P((c - a)/b);
} ///:~
You
can see how a technique like this can quickly become indispensible, especially
if you have no debugger (or must use multiple development environments). You
can also insert an
#ifdef
to cause
P(A)
to be defined as “nothing” when you want to strip out debugging.
The
C assert( ) macro
In
the standard header file
<cassert>
you’ll
find
assert( ),
which is a convenient debugging macro. When you use
assert( ),
you give it an argument that is an expression you are “asserting to be
true.” The preprocessor generates code that will test the assertion. If
the assertion isn’t true, the program will stop after issuing an error
message telling you what the assertion was and that it failed. Here’s a
trivial example:
//: C03:Assert.cpp
// Use of the assert() debugging macro
#include <cassert> // Contains the macro
using namespace std;
int main() {
int i = 100;
assert(i != 100);
} ///:~
The
macro originated in Standard C, so it’s also available in the header file
assert.h.
When
you are finished debugging, you can remove the code generated by the macro by
placing the line:
in
the program before the inclusion of
<cassert>,
or by defining NDEBUG on the compiler command line. NDEBUG is a flag used in
<cassert>
to
change the way code is generated by the macros.
Later
in this book, you’ll see some more sophisticated alternatives to
assert(
)
.
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru