|
The most useful general tool for debugging software is tracing. Put prints in your program that turn on the lights so that you can see what is happening while the program runs.
Never print raw numbers in a trace print. Trace
printf("%d %d\n", x, y);
is worse than useless. It writes a bunch of numbers.
Never print a silly string like
printf("I am here\n");
That is about like writing "taken last Thursday"
on the back of a photograph. (Yes, a person I knew did that.)
The most important rule is to make traces clear, informative and easy to read. The remaining rules offer suggestions for how to do that, but this rule overrides them where appropriate.
Say who is talking. There will typically be many trace prints in your software. Each one should say where it is by giving the name of the function in which it occurs. A typical way to do that in function insert is
printf("insert: …");showing the name of the function as if you are writing a play and this is a line for insert to speak.
Say where this trace occurs in the function. Is it at the beginning? The end? At the top of a loop body?
Provide relevant information. If you are showing the size of something, write "size = ...", where ... tells the size. Be sure to include important information. You will want to see it for diagnosing errors.
Do not run consecutive traces all onto one line. Normally, print a newline (\n) at the end of each trace. Put spaces between different pieces of information.
(This looks ahead to if-statements.)
After you have finished debugging a function, do not get rid of trace prints in that function. Leave them in place. Do not comment-out the traces. Surround each one by a test such as
if(traceLevel > 0) { ... }where traceLevel is a global variable (declared outside of functions). Global variables are shared by all functions that occur after their declaration. Normally, the standards for this course forbid the use any global variables, but a variable to control tracing is an exception. If traceLevel is 0, traces are skipped. You turn traces on by setting traceLevel to a positive integer. For example,
if(traceLevel > 1) { ... }says only to do this trace when the trace level 2 or higher.
You can also have more than one variable to control tracing. You typically have a variable for each module or collection of related modules, so that you can trace only what is happening in those modules.
Experts plan for mistakes. They write traces in the software right from the beginning, and do not take the traces out ever.
It is inefficient for a computer to write a single character into a file. Instead, it is much better to accumulate a bunch of characters (a few thousand) and write them all at once.
Taking that into account, printf stores characters that it prints into an array, called an output buffer, until something causes printf to flush the buffer by writing it out.
If output is being written to a terminal window, the buffer is flushed at the end of each line.
If output is being written into a file, the buffer flushed when the buffer becomes full.
The output buffer is flushed when the program ends normally. If the program ends abnormally, as by a memory fault, whatever is in the output buffer is thrown away.
If a trace is being written to a terminal window, and a trace is written that does not end on a newline character, that trace will not show up until later when a newline character is written. If something causes your program to stop before a newline is written, you can be misled into thinking that the trace was not reached.
If the trace is written into a file, a lot of traced material can be delayed, and you might not see an accurate picture of what is going on in your program.
In C++, you can force the standard output buffer to be flushed by performing statement
fflush(stdout);
Sometimes you want to leave trace prints in a a piece of software, but also want to be able to avoid the overhead of constantly checking if tracing is turned on in a version that is shipped to customers. In a C++ program it is easy to get rid of trace prints automatically using the preprocessor. First, either do or do not define a preprocessor symbol DEBUG. To define DEBUG, write
#define DEBUG
Then
#ifdef DEBUG
if(tracing > 0)
{
...
}
#endif
allows you to avoid compiling the entire debug print,
including the test of whether tracing > 0. Everything
between #ifdef DEBUG and #endif will
be omitted from the program.
An unfortunate drawback of trace prints is that they clutter function definitions, making the definitions more difficult to read. One way to avoid that is to move the trace prints out of the definition. Here is an example.
#ifdef DEBUG
# define TRACE_INSERT1\
if(tracing > 0)\
{\
printf("insert [top]: k = %i\n", k);\
}
#else
# define TRACE_INSERT1 {}
#endif
void insert(int k)
{
TRACE_INSERT1
... (body of insert)
}
(A preprocessor definition can only be one line long. But a backslash at the end of a line causes the end-of-line to be ignored. That is why there are backslashes ending lines in the definiton of TRACE_INSERT1.)
Now each trace print is replaced by one short line inside the function body, so it has minimal impact on the readability of the body.
What is tracing? Answer
What is the purpose of tracing? Answer
What happens if you forget to print a newline character in a trace? Answer
What should a trace print show? Is it reasonable for it to write "I am here"? Answer
How does output buffering affect traces? Answer
|