WHAT YOU WILL LEARN IN THIS CHAPTER:
How to run your program under the control of the Visual C++ 2010 debugger
How to step through your program a statement at a time
How to monitor or change the values of variables in your programs
How to monitor the value of an expression in your program
The call stack
Assertions and how to use them to check your code
How to add debugging specific code to a program
How to detect memory leaks in a native C++ program
How to use the execution tracing facilities and generate debugging output in C++/CLI programs
If you have been doing the exercises in the previous chapters, you have most likely been battling with bugs in your code. In this chapter you will explore how the basic debugging capabilities built into Visual C++ 2010 can help with this. You will also investigate some additional tools that you can use to find and eliminate errors from your programs, and see some of the ways in which you can equip your programs with specific code to check for errors.
Bugs are errors in your program, and debugging is the process of finding and eliminating them. You are undoubtedly aware by now that debugging is an integral part of the programming process — it goes with the territory, as they say. The facts about bugs in your programs are rather depressing:
Every program you write that is more than trivial will contain bugs that you need to try to expose, find, and eliminate if your program is to be reliable and effective. Note the three phases here — a program bug is not necessarily apparent; even when it is apparent you may not know where it is in your source code; and even when you know roughly where it is, it may not be easy to determine what exactly is causing the problem, and thus eliminate it.
Many programs that you write will contain bugs even after you think you have fully tested them.
Program bugs can remain hidden in a program that is apparently operating correctly — sometimes for years. They generally become apparent at the most inconvenient moments.
Programs beyond a certain size and complexity always contain bugs, no matter how much time and effort you expend testing them. (The measure of size and complexity that guarantees the presence of bugs is not precisely defined, but Visual C++ 2010 and your operating system certainly are in this category!)
It is unwise to dwell on this last point if you are of a nervous disposition, especially if you fly a lot, or are regularly in the vicinity of any process, dependent on computers for proper operation, that can be damaging to your health in the event of failure.
Many potential bugs are eliminated during the compile and link phases, but there are still quite a few left even after you manage to produce an executable module for your program. Unfortunately, despite the fact that program bugs are as inevitable as death and taxes, debugging is not an exact science; however, you can still adopt a structured approach to eliminating bugs. There are four broad strategies you can adopt to make debugging as painless as possible:
Don't re-invent the wheel. Understand and use the library facilities provided as part of Visual C++ 2010 (or other commercial software components you have access to) so that your program uses as much pre-tested code as possible. Note that while this will reduce the likelihood of bugs in your code, libraries, operating systems, and commercial software, components will still contain bugs in general, so your code can share those bugs.
Develop and test your code incrementally. By testing each significant class and function individually, and gradually assembling separate code components after testing them, you can make the development process much easier, with fewer obscure bugs occurring along the way.
Code defensively — which means writing code to guard against potential errors. For example, declare member functions of native C++ classes that don't modify an object as const
. Use const
parameters where appropriate. Don't use "magic numbers" in your code — define const
objects with the required values.
Include debugging code that checks and validates data and conditions in your program from the outset. This is something you will look at in detail later in this chapter.
Because of the importance of ending up with programs that are as bug-free as is humanly possible, Visual C++ 2010 provides you with a powerful armory of tools for finding bugs. Before you get into the detailed mechanics, however, let's look a little closer at how bugs arise.
Of course, the primary originator of bugs in your program is you and the mistakes you make. These mistakes range from simple typos — just pressing the wrong key — to getting the logic completely wrong. I, too, find it hard to believe that I can make such silly mistakes so often, but no one has yet managed to come up with a credible alternative as to how bugs get into your code — so it must be true! Humans are creatures of habit so you will probably find yourself making some mistakes time and time again. Frustratingly, many errors are glaringly obvious to others, but invisible to you — this is just your computer's way of teaching you a bit of humility. Broadly, there are two kinds of errors you can make in your code that result in program bugs:
Syntactic errors — These are errors that result from statements that are not of the correct form; for example, if you miss a semicolon from the end of a statement or use a colon where you should put a comma. You don't have to worry too much about syntactic errors. The compiler recognizes all syntactic errors, and you generally get a fairly good indication of what the error is so it's easy to fix.
Semantic errors — These are errors where the code is syntactically correct, but it does not do what you intended. The compiler cannot know what you intended to achieve with your program, so it cannot detect semantic errors; however, you will often get an indication that something is wrong because the program terminates abnormally. The debugging facilities in Visual C++ 2010 are aimed at helping you find semantic errors. Semantic errors can be very subtle and difficult to find, for example, where the program occasionally produces the wrong results or crashes now and again. Perhaps the most difficult of such bugs arise in multi-threaded programs where concurrent paths of execution are not managed properly.
Of course, there are bugs in the system environment that you are using (Visual C++ 2010 included) but this should be the last place you suspect when your program doesn't work. Even when you do conclude that it must be the compiler or the operating system, nine times out of ten you will be wrong. There are certainly bugs in Visual C++ 2010, however, and if you want to keep up with those identified to date, together with any fixes available, you can search the information provided on the Microsoft Web site related to Visual C++ (http://msdn2.microsoft.com/en-us/visualc/default.aspx
), or better still, if you can afford a subscription to Microsoft Developer Network, you get quarterly updates on the latest bugs and fixes.
It can be helpful to make a checklist of bugs you find in your code for future reference. By examining new code that you write for the kinds of errors you have made in the past, you can often reduce the time needed to debug new projects.
By the nature of programming, bugs are virtually infinite in their variety, but there are some kinds that are particularly common. You may be well aware of most of these, but take a quick look at them anyway.
A useful way of cataloguing bugs is to relate them to the symptoms they cause because this is how you experience them in the first instance. The following list of five common symptoms is by no means exhaustive, and you are certainly able to add to it as you gain programming experience:
SYMPTOM | POSSIBLE CAUSES |
---|---|
Data corrupted | Failure to initialize variable. Exceeding integer type range. Invalid pointer. Error in array index expression. Loop condition error. Error in size of dynamically allocated array. Failing to implement class copy constructor, assignment operator, or destructor. |
Invalid pointer or reference. Missing catch handler. | |
Failure to initialize variable. Infinite loop. Invalid pointer. Freeing the same free store memory twice. Failure to implement, or error in, class destructor. Failure to process unexpected user input properly. | |
Reading using the extraction operator and the | |
Typographical error: Failure to initialize a variable. Exceeding the range of an integer type. Invalid pointer. Omitting break in a switch statement. |
Look at how many different kinds of errors can be caused by invalid pointers and the myriad symptoms that bad pointers can generate. This is possibly the most frequent cause of those bugs that are hard to find, so always double-check your pointer operations. If you are conscious of the ways in which bad pointers arise, you can avoid many of the pitfalls. The common ways in which bad pointers arise are:
Failing to set a pointer to free store memory to null when you delete the space allocated
Returning the address of a local variable from a function
Failing to implement the copy constructor and assignment operator for classes that allocate free store memory
Even if you do all this, there will still be bugs in your code, so now look at the tools that Visual C++ 2010 provides to assist debugging.
So far, although you have been creating debug versions of the program examples, you haven't been using the debugger. The debugger is a program that controls the execution of your program in such a way that you can step through the source code one line at a time, or run to a particular point in the program. At each point in your code where the debugger stops, you can inspect or even change the values of variables before continuing. You can also change the source code, recompile, and then restart the program from the beginning. You can even change the source code in the middle of stepping through a program. When you move to the next step after modifying the code, the debugger automatically recompiles before executing the next statement.
To understand the basic debug capabilities of Visual C++ 2010, you will use the debugger on a program that you are reasonably sure works. You can then just pull the levers to see how things operate. Take a simple example from back in Chapter 4 that uses pointers:
// Ex4_05.cpp // Exercising pointers #include <iostream> using namespace std; int main() { long* pnumber(nullptr); // Pointer declaration & initialization long number1 = 55, number2 = 99; pnumber = &number1; // Store address in pointer *pnumber += 11; // Increment number1 by 11 cout << endl << "number1 = " << number1 << " &number1 = " << hex << pnumber; pnumber = &number2; // Change pointer to address of number2 number1 = *pnumber*10; // 10 times number2 cout << endl << "number1 = " << dec << number1 << " pnumber = " << hex << pnumber
<< " *pnumber = " << dec << *pnumber; cout << endl; return 0; }
If you still have this example on your system, just open the project; otherwise, you need to download the code or enter it again.
When you write a program that doesn't behave as it should, the debugger enables you to work through a program one step at a time to find out where and how it's going wrong, and to inspect the state of your program's data at any time during execution. You'll execute this example one statement at a time and to monitor the contents of the variables that interest you. In this case you want to look at pnumber
, the contents of the location pointed to by pnumber
(which is *pnumber
), number1
, and number2
.
First you need to be sure that the build configuration for the example is set to Win32 Debug rather than Win32 Release (Win32 Debug is the default, unless you've changed it). The build configuration selects the set of project settings for the build operation on your program that you can see when you select the Project/Settings menu option. The current build configuration in effect is shown in the pair of adjacent drop-down lists on the Standard toolbar. To display or remove a particular toolbar you just right-click the toolbar and select or deselect a toolbar in the list. Make sure you check the box against Debug to display the debugging toolbar. It comes up automatically when the debugger is operating, but you should take a look at what it contains before you start the debugger. You can change the build configuration by extending the drop-down list and choosing the alternative. You can also use the Build
You can find out what the toolbar buttons are for by letting the mouse cursor linger over a toolbar button. A tool tip for that button appears that identifies its function.
The Debug
configuration in a project causes additional information to be included in your executable program when you compile it so that the debugging facilities can be used. This extra information is stored in the .pdb
file that will be in the Debug folder for your project. With the Professional version of Visual C++ 2010, the compiler also optimizes the code when compiling the release version of a program. Optimization is inhibited when the debug version is compiled because the optimization process can involve resequencing code to make it more efficient, or even omitting redundant code altogether. Because this destroys the one-to-one mapping between the source code and corresponding blocks of machine code, optimization makes stepping through a program potentially confusing, to say the least.
If you inspect the tooltips for the buttons on the Debug toolbar, you'll get a preliminary idea of what they do — you will use some of them shortly. With the example from Chapter 4, you won't use all the debugging facilities available to you, but you will try out some of the more important features. After you are familiar with stepping through a program using the debugger, you will explore more of the features with a program that has bugs.
You can start the debugger by clicking the leftmost button on the Debug toolbar, by selecting the Debug
A breakpoint is a point in your program where the debugger automatically suspends execution when in debugging mode. You can specify multiple breakpoints so that you can run your program, stopping at points of interest that you select along the way. At each breakpoint you can look at variables within the program and change them if they don't have the values they should. You are going to execute the Ex4_05
program one statement at a time, but with a large program this would be impractical. Usually, you will only want to look at a particular area of the program where you think there might be an error. Consequently, you would usually set breakpoints where you think the error is, and run the program so that it halts at the first breakpoint. You can then single step from that point if you want, where a single step implies executing a single source code statement.
To set a breakpoint at the beginning of a line of source code, you simply click in the grayed-out column to the left of the line for the statement where you want execution to stop. A red circular symbol called a glyph appears showing the presence of the breakpoint at that line. You can remove a breakpoint by clicking the glyph. Figure 11-1 shows the Editor pane with a couple of breakpoints set for Ex4_05
.
When debugging, you would normally set several breakpoints, each chosen to show when the variables that you think are causing a problem are changing. Execution stops before the statement indicated by the breakpoint is executed. Execution of the program can only break before a complete statement and not halfway through it. If you place a cursor in a line that doesn't contain any code (for example, the line above the second breakpoint in Figure 11-1), the breakpoint is set on that line, and the program stops at the beginning of the next executable line.
As I said, you can remove a breakpoint by clicking the red dot. You can also disable a breakpoint by right-clicking the line containing the breakpoint and selecting from the pop-up. You can remove all the breakpoints in the active project by selecting the Debug
A more advanced way of specifying breakpoints is provided through a window you can display by pressing Alt+F9 or by selecting Breakpoints from the list displayed when you select the Windows button on the Debug toolbar — it's at the right end. This window is shown in Figure 11-2.
The Columns button on the toolbar enables you to add more columns to be displayed in the window. For example, you can display the source file name or the function name where the breakpoint is, or you can display what happens when the statement is reached.
You can set further options for a breakpoint by right-clicking the breakpoint line in the Breakpoints window and selecting from the pop-up. As well as setting a breakpoint at a location other than the beginning of a statement, you can set a breakpoint when a particular Boolean expression evaluates to true. This is a powerful tool but it does introduce very substantial overhead in a program, as the expression needs to be re-evaluated continuously. Consequently, execution is slow, even on the fastest machines. You can also set things up so that execution only breaks when the hit count, which is the number of times the breakpoint has been reached, reaches a given value. This is most useful for code inside a loop where you won't want to break execution on every iteration. If you set any condition on a breakpoint, the glyph changes so that a + appears in the center.
A tracepoint is a special kind of breakpoint that has a custom action associated with it. You create a tracepoint by right-clicking the line of code where you want the tracepoint to be set and selecting the Breakpoint
As you see, the tracepoint action can be to print a message and/or run a macro, and you can choose whether execution stops or continues at the tracepoint. The presence of a tracepoint on a source code line where execution does not stop is indicated by a red diamond-shaped glyph. The dialog text explains how to specify the message to be printed. For instance, you could print the name of the current function and the value of pnumber
by specifying the following in the text box:
$FUNCTION, The value of pnumber is {pnumber}
The output produced by this when the tracepoint is reached is displayed in the Output pane in the Visual Studio application window.
When you check the Run a macro:
checkbox, you'll be able to choose from a long list of standard macros that are available.
There are five ways of starting your application in debug mode from the options on the Debug menu, as shown in Figure 11-4:
The Start Debugging option (also available from a button on the Debug toolbar) simply executes a program up to the first breakpoint (if any) where execution will halt. After you've examined all you need to at a breakpoint, selecting the same menu item or toolbar button again will continue execution up to the next breakpoint. In this way, you can move through a program from breakpoint to breakpoint, and at each halt in execution have a look at critical variables, changing their values if you need to. If there are no breakpoints, starting the debugger in this way executes the entire program without stopping. Of course, just because you started debugging in this way doesn't mean that you have to continue using it; at each halt in execution, you can choose any of the possible ways of moving through your code.
The Attach to Process option on the Debug menu enables you to debug a program that is already running. This option displays a list of the processes that are running on your machine and you can select the process you want to debug. This is really for advanced users and you should avoid experimenting with it unless you are quite certain that you know what you are doing. You can easily lock up your machine or cause other problems if you interfere with critical operating system processes.
The Step Into menu item (also available as a button on the Debug toolbar) executes your program one statement at a time, stepping into every code block — which includes every function that is called. This would be something of a nuisance if you used it throughout the debugging process because, for example, it would also execute all the code in the library functions for stream output — you're not really interested in this as you didn't write these routines. Quite a few of the library functions are written in Assembler language — including some of those supporting stream input/output. Assembler language functions execute one machine instruction at a time, which can be rather time consuming, as you might imagine.
The Step Over menu item (also available as a button on the Debug toolbar) simply executes the statements in your program one at a time and runs all the code used by functions that might be called within a statement, such as stream operations, without stopping.
You have a sixth option for starting in debug mode that does not appear on the Debug menu. You can right-click any line of code and select Run to Cursor from the Context menu. This does precisely what it says — it runs the program up to the line where the cursor is and then breaks execution to allow you to inspect or change variables in the program. Whatever way you choose to start the debugging process, you can continue execution using any of the five options you have available from any intermediate breakpoint.
It's time to try it with the example. Start the program using the Step Into option, click the appropriate menu item or toolbar button, or press F11 to begin. After a short pause (assuming that you've already built the project), Visual C++ 2010 switches to debugging mode.
When the debugger starts, two tabbed windows appear below the Editor window. You can choose what is displayed at any time in either window by selecting one of the tabs. You can choose which windows appear when the debugger is started, and they can be customized. The complete list of windows is shown on the Debug
You can also see the breakpoint at line 11 and the tracepoint at line 17. At this point in the execution of the program, you can't choose any variables to look at because none exist at present. Until a declaration of a variable has been executed, you cannot look at its value or change it.
To avoid having to step through all the code in the stream functions that deal with I/O, you'll use the Step Over facility to continue execution to the next line. This simply executes the statements in your main()
function one at a time, and runs all the code used by the stream operations (or any other functions that might be called within a statement) without stopping.
Defining a variable that you want to inspect is referred to as setting a watch for the variable. Before you can set any watches, you must get some variables declared in the program. You can execute the declaration statements by invoking Step Over three times. Use the Step Over menu item, the toolbar icon, or press F10 three times so that the arrow now appears at the start of the line 11:
pnumber = &number1; // Store address in pointer
If you look at the Autos
window now, it should appear as shown in Figure 11-6 (although the value for &number1
may be different on your system as it represents a memory location). Note that the values for &number1
and pnumber
are not equal to each other because the line in which pnumber
is set to the address of number1
(the line that the arrow is pointing at) hasn't yet been executed. You initialized pnumber
as a null pointer in the first line of the function, which is why the address it contains is zero. If you had not initialized the pointer, it would contain a junk value that still could be zero on occasion, of course, because it contains whatever value was left by the last program to use these particular four bytes of memory.
You can display the following five tabs in the bottom left window by selecting from the Debug
The Autos tab shows the automatic variables in use in the current statement and its immediate predecessor (in other words, the statement pointed to by the arrow in the Editor pane and the one before it).
The Locals tab shows the values of the variables local to the current function. In general, new variables come into scope as you trace through a program and then go out of scope as you exit the block in which they are defined. In this case, this window always shows values for number1
, number2
and pnumber
because you have only one function, main()
, consisting of a single code block.
The Threads tab allows you to inspect and control threads in advanced applications.
The Modules tab lists details of the code modules currently executing. If your application crashes, you can determine in which module the crash happened by comparing the address when the crash occurred with the range of addresses in the Address column on this tab.
You can add variables to the Watch1 tab that you want to watch. Just click a line in the window and type the variable name. You can also watch the value of a C++ expression that you enter in the same way as a variable. You can add up to three additional Watch windows via the Debug
Notice that pnumber
has a plus sign to the left of its name in the Autos window. A plus sign appears for any variable for which additional information can be displayed, such as for an array, or a pointer, or a class object. In this case, you can expand the view for the pointer variables by clicking the plus sign. If you press F10 twice more and click the + adjacent to pnumber
, the debugger displays the value stored at the memory address contained in the pointer, as shown in Figure 11-7.
The Autos window automatically provides you with all the information you need, displaying both the memory address and the data value stored at that address. Integer values can be displayed as decimal or hexadecimal. To toggle between the two, right-click anywhere on the Autos tab and select from the pop-up menu. You can view the variables that are local to the current function by selecting the Locals tab. There are also other ways that you can inspect variables using the debugging facilities of Visual C++ 2010.
If you need to look at the value of a single variable, and that variable is visible in the Text Editor window, the easiest way to look at its value is to position the cursor over the variable for a second. A tool tip pops up showing the current value of the variable. You can also look at more complicated expressions by highlighting them and resting the cursor over the highlighted area. Again, a tool tip pops up to display the value. Try highlighting the expression *pnumber*10
a little lower down. Hovering the cursor over the highlighted expression results in the current value of the expression being displayed. Note that this won't work if the expression is not complete; if you miss the *
that dereferences pnumber
out of the highlighted text, for instance, or you just highlight *pnumber*
, the value won't be displayed.
You can change the values of the variables you are watching in the Watch windows, and you can also change values in the Autos and Locals windows. You would use this in situations where a value displayed is clearly wrong, perhaps because there are bugs in your program, or maybe all the code is not there yet. If you set the "correct" value, your program staggers on so that you can test out more of it and perhaps pick up a few more bugs. If your code involves a loop with a large number of iterations, say, 30000, you could set the loop counter to 29995 to step through the last few to verify that the loop terminates correctly. It sure beats pressing F10 30,000 times! Another useful application of the ability to set values for variable during execution is to set values that cause errors. This enables you to check out the error handling code in your program, something almost impossible otherwise.
To change the value of a variable in a Watch window, double-click the variable value that is displayed, and type the new value. If the variable you want to change is an array element, you need to expand the array by clicking the + box alongside the array name and then changing the element value. To change the value for a variable displayed in hexadecimal notation, you can either enter a hexadecimal number, or enter a decimal value prefixed by 0n (zero followed by n), so you could enter a value as A9, or as 0n169. If you just enter 169 it is interpreted as a hexadecimal value. Naturally, you should be cautious about flinging new values into your program willy-nilly. Unless you are sure you know what effect your changes are going to have, you may end up with a certain amount of erratic program behavior, which is unlikely to get you closer to a working program.
You'll probably find it useful to run a few more of the examples you have seen in previous chapters in debug mode. It will enable you to get a good feel for how the debugger operates under various conditions. Monitoring variables and expressions is a considerable help in sorting out problems with your code, but there's a great deal more assistance available for seeking out and destroying bugs. Take a look at how you can add code to a program that provides more information about when and why things go wrong.
For a program involving a significant amount of code, you certainly need to add code that is aimed at highlighting bugs wherever possible and providing tracking output to help you pin down where the bugs are. You don't want to be in the business of single stepping through code before you have any idea of what bugs there are, or which part of the code is involved. Code that does this sort of thing is only required while you are testing a program. You won't need it after you believe the program is fully working, and you won't want to carry the overhead of executing it or the inconvenience of seeing all the output in a finished product. For this reason, code that you add for debugging only operates in the debug version of a program, not in the release version (provided you implement it in the right way, of course).
The output produced by debug code should provide clues as to what is causing a problem, and if you have done a good job of building debug code into your program, it will give you a good idea of which part of your program is in error. You can then use the debugger to find the precise nature and location of the bug, and fix it.
The first way you can check the behavior of your program that you will look at is provided by a C++ library function.
The standard library header cassert
declares the assert()
function that you can use to check logical conditions within your program when a special preprocessor symbol, NDEBUG
, is not defined. The function is declared as:
void assert(int expression);
The argument to the function specifies the condition to be checked, but the effect of the assert()
function is suppressed if a special preprocessor symbol, NDEBUG
, is defined. The symbol NDEBUG
is automatically defined in the release version of a program, but not in the debug version. Thus an assertion checks its argument in the debug version of a program but does nothing in a release version. If you want to switch off assertions in the debug version of a program, you can define NDEBUG
explicitly yourself using a #define
directive. For it to be effective, you must place the #define
directive for NDEBUG
preceding the #include
directive for the cassert
header in the source file:
#define NDEBUG // Switch off assertions in the code #include <cassert> // Declares assert()
If the expression passed as an argument to assert()
is non-zero (i.e. true
), the function does nothing. If the expression is 0 (false
, in other words) and NDEBUG
are not defined, a diagnostic message is output showing the expression that failed, the source file name, and the line number in the source file where the failure occurred. After displaying the diagnostic message, the assert()
function calls abort()
to end the program. Here's an example of an assertion used in a function:
char* append(char* pStr, const char* pAddStr) {// Verify non-null pointers
assert(pStr != nullptr);
assert(pAddStr != nullptr);
// Code to append pAddStr to pStr... }
Calling the append()
function with a null pointer argument in a simple program produced the following diagnostic message on my machine:
Assertion failed: pStr != nullptr, file c:eginning visual c++ 2010examples visual studio project files ryassertion ryassertion ryassertion.cpp, line 10
The assertion also displays a message box offering you the three options shown in Figure 11-8.
Clicking the Abort button ends the program immediately. The Retry button starts the Visual C++ 2010 debugger so you can step through the program to find out more about why the assertion failed. In principle, the Ignore button allows the program to continue in spite of the error, but this is usually an unwise choice as the results are likely to be unpredictable.
You can use any kind of logical expression as an argument to assert()
. You can compare values, check pointers, validate object types, or whatever is a useful check on the correct operation of your code. Getting a message when some logical condition fails helps a little, but in general you will need considerably more assistance than that to detect and fix bugs. Now take a look at how you can add diagnostic code of a more general nature.
Using preprocessor directives, you can add any code you like to your program so that it is only compiled and executed in the debug version. Your debug code is omitted completely from the release version, so it does not affect the efficiency of the tested program at all. You could use the absence of the NDEBUG
symbol as the control mechanism for the inclusion of debugging code; that's the symbol used to control the assert()
function operation in the standard library, as discussed in the last section. Alternatively, for a better and more positive control mechanism, you can use another preprocessor symbol, _DEBUG
, that is always defined automatically in Visual C++ in the debug version of a program, but is not defined in the release version. You simply enclose code that you only want compiled and executed when you are debugging between a preprocessor #ifdef
/#endif
pair of directives, with the test applied to the _DEBUG
symbol, as follows:
#ifdef _DEBUG // Code for debugging purposes... #endif // _DEBUG
The code between the #ifdef
and the #endif
is compiled only if the symbol _DEBUG
is defined. This means that once your code is fully tested, you can produce the release version completely free of any overhead from your debugging code. The debug code can do anything that is helpful to you in the debugging process, from simply outputting a message to trace the sequence of execution (each function might record that it was called for example) to providing additional calculations to verify and validate data, or calling functions providing debug output.
Of course, you can have as many blocks of debug code like this in a source file as you want. You also have the possibility of using your own preprocessor symbols to provide more selectivity as to what debug code is included. One reason for doing this is that some of your debug code may produce voluminous output, and you would only want to generate this when it was really necessary. Another is to provide granularity in your debug output, so you can pick and choose which output is produced on each run. But even in these instances it is still a good idea to use the _DEBUG
symbol to provide overall control because this automatically ensures that the release version of a program is completely free of the overhead of debugging code.
Consider a simple case. Suppose you used two symbols of your own to control debug code: MYDEBUG
that managed "normal" debugging code and VOLUMEDEBUG
that you use to control code that produced a lot more output, and that you only wanted some of the time. The following directives will ensure that these symbols are defined only if _DEBUG
is defined:
#ifdef _DEBUG #define MYDEBUG #define VOLUMEDEBUG #endif
To prevent volume debugging output you just need to comment out the definition of VOLUMEDEBUG
, and neither symbol is defined if _DEBUG
is not defined. Where your program has several source files, you will probably find it convenient to place your debug control symbols together in a header file and then #include
the header into each file that contains debugging code.
Examine a simple example to see how adding debugging code to a program might work in practice.