Like all control statements, C++ forms loops by adding the looping controls to simple statements or blocks. Consequently, the previous guidelines for debugging logical and run-time errors also apply to loops. However, because loops execute the same code many times, they add new challenges. First, output from manual instrumentation often interleaves with the loop's "normal" output, obscuring both. Second, the problem may not manifest for many iterations, forcing programmers to add extra control code (e.g., an if-statement) or deal with excessive output. While programmers can alleviate these problems with file redirection, it adds another level of complexity to the debugging process. An integrated debugger eases program debugging in these cases.
cerr << "Start Loop " << __FILE__ << " - " << __LINE__ << endl;
for (int i = start; i < max; i++)
{
cerr << "Loop: i = " << i << " " <<__FILE__ << " - " << __LINE__ << endl;
...
}
cerr << "Start Loop " << __FILE__ << " - " << __LINE__ << endl;
int i;
do
{
cerr << "Loop " << __FILE__ << " - " << __LINE__ << endl;
i = some_calculation;
...
cerr << "End: i = " << i << " " <<__FILE__ << " - " << __LINE__ << endl;
} while (i < max);
int i = start;
cerr << "Start Loop: i = " << i << " " <<__FILE__ << " - " << __LINE__ << endl;
while (i < max)
{
cerr << "Loop: i = " << i << " " <<__FILE__ << " - " << __LINE__ endl;
...
i++;
}
Manually instrumenting loops.
Instrumenting basic loops follows a now-familiar pattern. For- and while-loops perform the looping test before entering the loop: if the test is false, the loop doesn't run, so it's helpful to include instrumentation before the loop. The remaining instrumentation traces the loop's execution path or examines values within the loop as needed.
int main()
{
for (int i = 1; i <= 12; i++)
{
for (int j = 1; j <= 12; j++)
cout << setw(5) << i * j;
cout << endl;
}
return 0;
}
int main()
{
for (int i = 1; i <= max_rows; i++)
{
cerr << "outer loop: i = " << i << " " <<__FILE__ << " - " << __LINE__ << endl;
for (int j = 1; j <= max_cols; j++)
{
cerr << "Inner loop: j = " << j << " " <<__FILE__ << " - " << __LINE__ << endl;
cout << setw(5) << i * j;
}
cout << endl;
}
return 0;
}
int main()
{
for (int i = 1; i <= max_rows; i++)
{
for (int j = 1; j <= max_cols; j++)
{
if (i > 100)cerr << "i * j: " << i * j << " " <<__FILE__ << " - " << __LINE__ << endl;
cout << setw(5) << i * j;
}
cout << endl;
}
return 0;
}
(b)
(c)
The problems with instrumenting loops.
The original multtab program and a slightly modified version show two problems that arise from manually instrumenting loops. Real-world problems and their associated programmed solutions are typically unique in many ways, so not all are subject to the instrumentation problems illustrated here.
The unmodified version of the program creates a table of carefully formatted text.
Manually instrumenting loops greatly increases any output they generate. Commingling instrumentation and loop output makes it difficult to distinguish and understand them. Furthermore, sending output to the console is surprisingly slow, so the extra output can greatly increase the program's runtime.
Imagine that max_rows is a large value and that the programmer is investigating an error that only manifests itself after many iterations of the outer loop. In the situation of example (b), the instrumentation generates an enormous amount of output. Example (c) reduces the output but at the expense of more complex instrumentation.
Loops And The Debugger
debugger, tracepoint, action, condition, run to cursor, watching a variable, Watch window, context controls, multtab
A typical integrated debugger provides many features to help programmers debug loops (and later functions). The following figures demonstrate some of these features.
(a)
(b)
Debugger conditions.
Figure 2 illustrates two problems programmers have when instrumenting for-loops. An integrated debugger solves both problems easily.
Setting tracepoints with the actions i = {i} and j = {j} solves the problem described in Figure 2(a). For the loop control variables to have the correct values, the tracepoints, like the instrumentation statements, must follow each for-loop.
When configuring a tracepoint, checking the "Conditions" checkbox and adding the condition i > 100 limits the output it generates. Like the if-statement demonstrated in Figure 2(b), the illustrated tracepoint only generates output when the loop-control variable i is greater than 100. When the programmer closes the configuration dialog, the debugger adds a "+" to the diamond in the gray column, signifying a conditional tracepoint.
The "Run To Cursor" operation.
Position the mouse pointer on the line where debugging begins. In this example, place the cursor on line 7, the outer loop that drives the program through the rows. Right-click and select "Run To Cursor" or press ctrl-F10. The program begins running but pauses at the beginning of line 7, allowing various debugging operations.
Examining variable contents.
The yellow arrow in the grey column indicates the next statement to run. While the program is paused, programmers can examine the values stored in the variables by hovering the mouse pointer over them. Hovering the mouse pointer over i displays a negative value because it hasn't been initialized yet, so it contains a random value. Hovering the pointer over j doesn't display a value because it is only defined in the inner loop.
Watching variables.
Setting a watch on one or more variables is another debugging operation programmers can perform while the program is paused. To watch i, right-click on or just behind it and select "Add Watch." Adding a watch to any variable opens a "Watch" window, generally at the bottom of the debugger, showing a negative value for i because it isn't initialized yet. Add another watch for j.
(a)
(b)
(c)
Stepping through the program.
The debugger has three stepping operations as illustrated. This example demonstrates "Step Into" (also known as Single Step); Chapter 6 describes the other stepping operations. Programmers can select the Step Into operation from the "Debug" menu (a), by pressing F11, or pressing the "Step Into" button (b). The program executes one line of code for each "Step Into" operation. The yellow arrow in the grey bar moves to indicate the stepping operation. The debugger updates the "Watch" window (c) whenever a watched variable's value changes.
Ending the program and debug session.
You can continue single-stepping through the program until it ends, or stop it with one of five debugger controls. The last four are part of the debuggers context controls.
From the menu at the top: Debug → Stop Debugging
Press Continue
Press Stop
Press Step Out
Press Shift-F5
Clearing watches.
You can add and remove watches whenever needed. If you no longer want to watch an item in the watch window, right-click it and select "Delete Watch," or select it and press the Delete key on the keyboard. You can also right-click anywhere in the watch window and select "Clear All" to remove all watches.