All but the most trivial programs contain many functions beyond main. It's easy for programmers, reading their own code, to overconfidently assert when a program calls a function and exactly what argument values it passes. Increasing the program's size, spreading functions across many files, and calling them in branches and loops amplifies the uncertainty.
Function bodies are blocks that can contain any valid C++ statement. Consequently, they are susceptible to the full spectrum of programming errors described previously. However, they share a complicating characteristic with branches and loops: program execution paths enter and exit them. So, when programmers debug functions, it's important to know when the program enters them, when it exits, and what it does while inside. Manual instrumentation can provide this information but not with as much flexibility and control as a debugger.
Functions and the programs using them are too varied to prescribe a rigid instrumenting algorithm. Nevertheless, some broad patterns dominate the process. When debugging functions, the information programmers need to locate and correct an error depends on which kind of error the function harbors. Run-time errors "crash" (abnormally terminate) a program, so locating the function and the statement in the function that causes the error is the first step. Once the bracketing process has narrowed the search, programmers treat the problem as a logical error.
Locating and correcting a logical error typically involves tracking values through the program's operations and comparing them to the corresponding manually-calculated values. The comparison includes the values passed to the function's parameters and to any value it returns. The following figure uses pseudocode to outline the basic debugging patterns and illustrates some common options.
...
cerr << "calling f1 " << endl; // i
f1(a1, a2);cerr << "Returned from f1 " << endl; // ii
...
int calc = f2(a3, a4);cerr << "f2: calc = " << calc << endl; // iii
...
int f2(p3, p4)
{
cerr << __FILE__ << " " << __LINE__ << " " p3 << " " << p4 << endl;
...
int r = ...; // iv
cerr << __FILE__ << " " << __LINE__ << " " << r << endl;return r; // v
}
(a)
(c)
Common function instrumentation.
For both logical and run-time errors, it's often convenient to begin debugging by tracing a program's execution path in and out of each function, examining parameter and returned values as necessary.
Programmers can choose to instrument the function's call or body. The advantage of instrumenting the call, (i) and (ii), is that often many function calls occur in proximity in a single file, making it convenient to instrument the calls. Examining a function's return value may help correct a logical error, and programmers can display the value after the call (iii) or at the end of the function's body.
Once programmers identify a function causing a run-time error, they proceed with the bracketing process described previously. Consequently, an advantage of instrumenting the function body is that the bracketing statements are already in place, ready to narrow the search for the error. This version labels the tracing output with the function's name, which is often familiar to programmers. The instrumentation can also include the parameter values when helpful.
It's often helpful to include the name of the file containing a function when functions are spread over many files, and, for long functions, the line number of the instrumenting statement may be more helpful than the function's name. This example illustrates the __FILE__ and __LINE__ preprocessor macros to add that information to the instrumentation. The return operator can return an arbitrarily complex expression. But if the expression causes a run-time error, the program crashes before the instrumentation can report it. The common solution is to evaluate and save the expression (iv), then return the saved value (v).
Manually instrumenting functions works but is inefficient and inelegant. Nevertheless, it makes a debugger's operations more concrete and engenders a deeper appreciation of its contributions to software development.
Functions can contain any valid C++ statement, implying that all previously described debugger operations (please see the "Review" section at the top of the page) are helpful when debugging functions. However, the stepping operations (introduced with debugging loops) are particularly helpful with functions. Although stepping controls and terminology vary across tools, the operations are typical of all modern debuggers. Once you learn what they do, you can learn how to perform the operations by consulting the tool-specific documentation.
Stepping buttons.
The Visual Studio IDE illustrates some typical debugger controls. Many of the controls, including the stepping buttons, are context-sensitive, meaning they only appear while the debugger is in debug mode (actively running and paused at a program statement). Programmers can actively debug a program in two ways:
Set a breakpoint with the mouse
Right-click a statement, select "Breakpoint" and "Insert Breakpoint" from the pop-up menus, or
Left-click the grey column at the far left
Run the program in debug mode:
Main menu: "Debug" → "Start Debugging," or
Press "Local Windows Debugger" in the pre-debug context menu (f), or
Press F5
Run to cursor
Select the run-to statement by placing the cursor on it
Right-click and select "Run To Cursor," or
Press Ctrl-F10
Programmers can use the stepping operations while the program is paused in debug mode:
Step Into (F11). Some debuggers call this operation single step because it runs a program one statement or instruction at a time. Position the cursor at a function call and press this button to step into the function. For each press, the debugger runs one statement until the function returns. If the call is embedded in a larger statement, it returns to that statement (following the call); otherwise, it returns to the statement following a standalone call.
Step Over (F10). While debugging a program with multiple functions, programmers may want to focus on some but not all of them. For example, if a function is already debugged and verified as correct, a programmer may wish to skip it. Position the cursor at a function call and press step over; the debugger runs all statements in the function without pausing.
Step Out (Shift-F11). Once the debugger is in and stepping through a function, the "Step Out" button runs the function to completion without pausing.
Continue (F5). Run to the next breakpoint or tracepoint; from the main menu: "Debug" → "Continue".
Stop (Shift-F5). Stop the debugger and end program execution; from the main menu: "Debug" → "Stop Debugging."
The debugger becomes increasingly helpful as programs grow in size and complexity. Debugging is a dynamic process: information gleaned from one location informs programmers about the next location and the next value to examine. When debugging with manually inserted instrumentation, this dynamic, ever-shifting focus requires programmers to insert and remove instrumentation and rebuild the program to examine each new location. A modern debugger eases the problem by letting programmers add and remove breakpoints and tracepoints, evaluate any expression, or restart the program without rebuilding it. The following figure uses a simple program to explore different scenarios and demonstrate strategies using stepping operations.
Using a debugger.
Four scenarios demonstrate how programmers can use the stepping operations.
Scenario 1: Examine a sequences of function calls.
The program is paused in debug mode at the function1 call, (i)
Pressing the "Step Into" button shifts the execution path into function1 at (iv)
Each press of the "Step Into" button advances the debugger to the next statement: (v), (vi), and back to (ii)
Programmers can stop debugging by pressing either the "Continue" or "Stop" button, or they can continue debugging, extending the first scenario into the second
Scenario 2: Debugging a function-call chain.
The program is paused in debug mode at the function2 call, (ii)
Pressing the "Step Into" button shifts the execution path into function2 at (vii); stepping three more times moves execution to (viii), (xi), and (x)"
The next "Step Into" press returns execution to (ii), after the call is finished but before cout executes; the next "Step Into" press moves execution to (iii)
A sequence of "Step Into" presses moves execution through (xi), (xii), (xiii), (vii), (viii), (xi), (x), (xiv), and back to (iii)
Pressing "Step Into" finishes (iii) and moves execution to the return statement; two more step operations end the program
Scenario 3: Skipping a previously debugged function.
The program is paused in debug mode at (xiii), the function2 call
Scenario 2 debugged function2; programmers press the "Step Over" button to run function2 without pausing in it
The program again pauses at (xiv)
Scenario 4: Skipping function statements.
The program is paused in debug mode at (xii)
The programmer doesn't want to continue debugging function3; pressing the "Step Out" button runs the remaining function3 statements without pausing in it
The program pauses at (iii), after the call completes but before the cout finishes