Chapter 11. Debugging Techniques

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.

UNDERSTANDING DEBUGGING

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.

Program Bugs

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.

Common Bugs

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.

Unhandled exceptions

Invalid pointer or reference.

Missing catch handler.

Program hangs or crashes

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.

Stream input data incorrect

Reading using the extraction operator and the getline() function.

Incorrect results

Typographical error: = instead of ==, or i instead of j, etc.

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 initialize a pointer when you declare it

  • 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.

BASIC DEBUGGING OPERATIONS

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;
}
                                                       
BASIC DEBUGGING OPERATIONS

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

BASIC DEBUGGING OPERATIONS

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

BASIC DEBUGGING OPERATIONS

Setting Breakpoints

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.

FIGURE 11-1

Figure 11.1. FIGURE 11-1

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

FIGURE 11-1

Advanced Breakpoints

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.

FIGURE 11-2

Figure 11.2. 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.

Setting Tracepoints

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

Setting Tracepoints
FIGURE 11-3

Figure 11.3. FIGURE 11-3

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.

Starting Debugging

There are five ways of starting your application in debug mode from the options on the Debug menu, as shown in Figure 11-4:

FIGURE 11-4

Figure 11.4. FIGURE 11-4

  1. 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.

  2. 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.

  3. 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.

  4. 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

FIGURE 11-4
FIGURE 11-5

Figure 11.5. FIGURE 11-5

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.

Inspecting Variable Values

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.

FIGURE 11-6

Figure 11.6. FIGURE 11-6

You can display the following five tabs in the bottom left window by selecting from the Debug

FIGURE 11-6
  • 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

    FIGURE 11-6

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.

FIGURE 11-7

Figure 11.7. 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.

Viewing Variables in the Edit Window

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.

Changing the Value of a Variable

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.

ADDING DEBUGGING CODE

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.

Using Assertions

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.

FIGURE 11-8

Figure 11.8. 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.

Adding Your Own Debugging Code

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.

DEBUGGING A PROGRAM

When the debugger starts, you get a message box indicating you have an unhandled exception. In the debugger, you have a comprehensive range of facilities for stepping through your code and tracing the sequence of events. Click Break in the dialog that indicates there is an unhandled exception to halt execution. The program is at the point where the exception occurred and the code currently executing is in the Editor window. The exception is caused by referring to a memory location way outside the realm of the program, so a rogue pointer in the program is the immediate suspect.

The Call Stack

The call stack stores information about functions that have been called and are still executing because they have not returned yet. As you saw earlier, the Call Stack window shows the sequence of function calls outstanding at the current point in the program. Refer to Figure 11-9.

FIGURE 11-9

Figure 11.9. FIGURE 11-9

The sequence of function calls outstanding runs from the most recent call at the top, the library function strcat(), down to the Kernel32 calls at the bottom of the window in Figure 11-9. Each function was called directly or indirectly by the one below it, and none of those displayed have yet executed a return. The Kernel32 lines are all system routines that start executing prior to our main() function. Your interest is the role of your code in this, and you can see from the second line down in the window that the Name class constructor was still in execution (had not returned) when the exception was thrown. If you double-click on that line, the Editor window displays the code for that function, and indicates the line in the source code being executed when the problem arose, which in this case is:

strcpy(pSurname, pSecond);

This call caused the unhandled exception to be thrown — but why? The original problem is not necessarily here; it just became apparent here. This is typical of errors involving pointers. Double-click on the second line in the Call Stack window referencing the Name constructor call, and then take a look at the Locals window showing the values in the variables in the context of the Name constructor that is presently displayed in the Editor pane. Figure 11-10 shows how it looks.

FIGURE 11-10

Figure 11.10. FIGURE 11-10

Because the context is a function that is a member of the Name class, the Locals window displays the this pointer that contains the address of the current object. The pSurname pointer contains a weird address, 0xcccccccc, and it has been flagged with <Bad Ptr>.The debugger recognizes that pSurname has got to be a rogue pointer and has marked it as such. If you look at pFirstname, this is also in a mess. At the point where you are in the code (copying the surname) the first name should already have been copied, but the contents are rubbish.

The culprit is in the preceding line. Hasty copying of code has resulted in allocating memory for pFirstname for a second time, instead of allocating space for pSurname. The copy is to a junk address, and this causes the exception to be thrown. Don't you wish you had checked what you did properly? The line should be:

pSurname = new char[strlen(pSecond)+1];

It is typically the case that the code causing a bad pointer address is not the code where the error makes itself felt. In general it may be very far away. Just examining the pointer or pointers involved in the statement causing the error can often lead you directly to the problem, but sometimes it can involve a lot of searching. You can always add more debug code if you get really stuck.

Change the statement in the Editor window to what it should be and recompile the project with the change included. You can then restart the program inside the debugger after it has been recompiled by clicking the button on the Debug toolbar, but surprise, surprise, you get another unhandled exception. This undoubtedly means more pointer trouble, and you can see from the output in the Console window that the last function call was to getNameLength():

Name constructor called.
Name::getName called.
The name is Horton
Name::getNameLength called.

The output for the name is definitely not right; however, you don't know where exactly the problem is. Restarting and stepping through the program once more should provide some clues.

Note

You can change the way numeric values are displayed in the Debugger windows. The default is to display values in hexadecimal format but you can change this to a decimal by right-clicking in a Debugger window and unchecking Hexadecimal Display. The change will apply to all Debugger windows.

Step Over to the Error

The getNameLength() function is currently displayed in the Editor pane and the debugger has indicated that the following line is where the problem arose:

return strlen(pFirstname)+strlen(pSurname);

In the Call Stack window, you can see that the program is in the getNameLength() function member, which merely calls the strlen() library function to get the overall length of the name. The strlen() function is unlikely to be at fault, so this must mean there is something wrong with part of the object. The Locals window showing the variables in the context of this function shows that the current object has been corrupted, as you can see in Figure 11-11.

FIGURE 11-11

Figure 11.11. FIGURE 11-11

The current object is pointed to by this, and by clicking the plus symbol alongside this, you can see the data members. It's the pSurname member that is the problem. The address it contains should refer to the string "Horton," but it clearly doesn't. Further, the debugger has flagged it as a bad pointer.

On the assumption that this kind of error does not originate at the point where you experience the effect, you can go back, restart the program, and single step through, looking for where the Name object gets messed up. You can select Step Over or press F10 to restart the application, and single step through the statements by repeatedly pressing F10. After executing the statement that defines the myName object, the Locals window for the main() function shows that it has been constructed successfully, as you can see in Figure 11-12.

FIGURE 11-12

Figure 11.12. FIGURE 11-12

Executing the next statement that outputs the name corrupts the object myName. You can clearly see that this is the case from the Locals window for main() in Figure 11-13.

FIGURE 11-13

Figure 11.13. FIGURE 11-13

On the reasonable assumption that the stream output operations work okay, it must be your getName() member doing something it shouldn't. Restart the debugger once more, but this time use Step Into when execution reaches the output statement. When execution is at the first statement of the getName() function, you can step through the statements in the getName() function using Step Over. Watch the Context window as you progress through the function. You will see that everything is fine until you execute the statement:

strcpy(pName+strlen(pName)+1, pSurname);    // Append second name after the space

This statement causes the corruption of pSurname for the current object, pointed to by this. You can see this in the Locals window in Figure 11-14.

FIGURE 11-14

Figure 11.14. FIGURE 11-14

How can copying from the object to another array corrupt the object, especially because pSurname is passed as an argument for a const parameter? You need to look at the address stored in pName for a clue. Compare it with the address contained in the this pointer. The difference is only 20 bytes — they could hardly be closer really! The address calculation for the position in pName is incorrect, simply because you forgot that copying a space to overwrite the terminating '' in the pName array means that strlen(pName) can no longer calculate the correct length of pName. The whole problem is caused by the statement:

pName[strlen(pName)] = ' ';          // Append a space

This is overwriting the '' and thus making the subsequent call to strlen() produce an invalid result.

This code is unnecessarily messy anyway; using the library function strcat() to concatenate a string is much better than using strcpy(), as it renders all this pointer modification unnecessary. You should rewrite the statement as:

strcat(pName, " ");                  // Append a space

Of course, the subsequent statement also needs to be changed to:

return strcat(pName, pSurname);      // Append second name and return total

With these changes you can recompile and give it another go. The program appears to run satisfactorily, as you can see from the output:

Name constructor called.
Name::getName() called.
The name is Ivor Horton
Name::getNameLength() called.
Name::getName() called.
The name is Ivor Horton

Getting the right output does not always mean that all is well, and it certainly isn't in this case. You get the message box displayed by the debug library shown in Figure 11-15 indicating that the stack is corrupted.

FIGURE 11-15

Figure 11.15. FIGURE 11-15

The following code shows where the problem lies:

int main(int argc, char* argv[])
{
  Name myName("Ivor", "Horton");                 // Try a single object

  // Retrieve and store the name in a local char array
  char theName[10];
  cout << "
The name is " << myName.getName(theName);

  // Store the name in an array in the free store
  char* pName = new char[myName.getNameLength()];
  cout << "
The name is " << myName.getName(pName);

  cout << endl;
  return 0;
}

Both the shaded lines are in error. The first shaded line provides an array of 10 characters to store the name. In fact, 12 are required: 10 for the two names, one for the space, and one for '' at the end. The second shaded line should add 1 to the value returned by the getNameLength() function to allow for the '' at the end. Thus, the code in main() should be:

int main(int argc, char* argv[])
{
  Name myName("Ivor", "Horton");                 // Try a single object

  // Retrieve and store the name in a local char array
  char theName[12];
  cout << "
The name is " << myName.getName(theName);

  // Store the name in an array in the free store
  char* pName = new char[myName.getNameLength()+1];   cout << "
The name is " << myName.getName(pName);

  cout << endl;
  return 0;
}

There's a more serious problem in the definition getNameLength() member of the class. It omits to add 1 for the space between the first and second names, so the value returned is always one short. You can detect that something is wrong in the Locals window because the value for pName shows up in red. The definition of the getNameLength() function should be:

int Name::getNameLength() const
{
#ifdef FUNCTION_TRACE
  // Trace function calls
  cout << "
Name::getNameLength() called.";
#endif
  return strlen(pFirstname)+strlen(pSurname)+1;
}

That's not the end of it by any means. You may have already spotted that your class still has serious errors, but press on with testing to see if they come out in the wash.

TESTING THE EXTENDED CLASS

Based on the output, everything is working, so it's time to add the definitions for the overloaded comparison operators to the Name class. I'll assume this is a new Win32 console project, Ex11_02. To implement the comparison operators for Name objects, you can use the comparison functions declared in the cstring header. Start with the "less than" operator:

// Less than operator
bool Name::operator<(const Name& name) const
{
  int result = strcmp(pSurname, name.pSurname);
  if(result < 0)
    return true;
if(result == 0 && strcmp(pFirstname, name.pFirstname) < 0)
    return true;
  else
    return false;
}

You can now define the > operator very easily in terms of the < operator:

// Greater than operator
bool Name::operator>(const Name& name) const
{
  return name > *this;
}

For determining equal names, you use the strcmp() function from the standard library again:

// Equal to operator
bool Name::operator==(const Name& name) const
{
  if(strcmp(pSurname, name.pSurname) == 0 &&
                                         strcmp(pFirstname, name.pFirstname) == 0)
    return true;
  else
    return false;
}

Now you can extend the test program by creating an array of Name objects, initializing them in some arbitrary way, and then comparing the elements of the array using your comparison operators for a Name object. Here's main() along with a function, init(), to initialize a Name array:

// Ex11_02.cpp : Extending the test operation
#include <iostream>
#include "Name.h"
using namespace std;

// Function to initialize an array of random names
void init(Name* names, int count)
{
  char* firstnames[] = { "Charles", "Mary", "Arthur", "Emily", "John"};
  int firstsize = sizeof (firstnames)/sizeof(firstnames[0]);
  char* secondnames[] = { "Dickens", "Shelley", "Miller", "Bronte", "Steinbeck"};
  int secondsize = sizeof (secondnames)/sizeof(secondnames[0]);
  char* first = firstnames[0];
  char* second = secondnames[0];

  for(int i = 0 ; i<count ; i++)
  {
    if(i%2)
      first = firstnames[i%firstsize];
    else
      second = secondnames[i%secondsize];
names[i] = Name(first, second);
  }
}

int main(int argc, char* argv[])
{
  Name myName("Ivor", "Horton");                 // Try a single object

  // Retrieve and store the name in a local char array
  char theName[12];
  cout << "
The name is " << myName.getName(theName);

  // Store the name in an array in the free store
  char* pName = new char[myName.getNameLength()+1];
  cout << "
The name is " << myName.getName(pName);

  const int arraysize = 10;
  Name names[arraysize];                          // Try an array

  // Initialize names
  init(names, arraysize);

  // Try out comparisons
  char* phrase = nullptr;                         // Stores a comparison phrase
  char* iName = nullptr;                          // Stores a complete name
  char* jName = nullptr;                          // Stores a complete name

  for(int i = 0; i < arraysize ; i++)             // Compare each element
  {
    iName = new char[names[i].getNameLength()+1]; // Array to hold first name
    for(int j = i+1 ; j<arraysize ; j++)          // with all the others
    {
      if(names[i] < names[j])
        phrase = " less than ";
      else if(names[i] > names[j])
        phrase = " greater than ";
      else if(names[i] == names[j])     // Superfluous - but it calls operator==()
        phrase = " equal to ";

      jName = new char[names[j].getNameLength()+1]; // Array to hold second name
      cout << endl << names[i].getName(iName) << " is" << phrase
           << names[j].getName(jName);
    }
  }

  cout << endl;
  return 0;
}
                                                          
TESTING THE EXTENDED CLASS

The init() function picks successive combinations of first and second names from the array of names to initialize the array Name objects. Names repeat after 25 have been generated, but you need only 10 here.

Finding the Next Bug

The Build output includes the following message:

warning C4717: 'Name::operator>' : recursive on all control paths, function will
cause runtime stack overflow

This is a strong indication that all is not well, but let's pretend that we didn't look that closely and assume that if the program builds okay, it must run okay.

If you start the program under the control of the debugger using the Start Debugging button on the Debug toolbar it fails again. The message box shown in Figure 11-16 is displayed.

FIGURE 11-16

Figure 11.16. FIGURE 11-16

The message box indicates you have exceeded the capacity of the stack memory available and if you select the Break button the Call Stack window tells you what is wrong. You have successive calls of the operator>() function so it must be calling itself, as the missed message said it would. If you look at the code, you can see why: a typo. The single line in the body of the function should be:

bool Name::operator>(const Name& name) const
{
return name < *this;
}

You can fix that, recompile, and try again. This time it works correctly, but unfortunately the class is still defective. It has a memory leak that exhibits no symptoms here, but in another context could cause mayhem. Memory leaks are hard to detect ordinarily, but you can get some extra help from Visual C++ 2010.

DEBUGGING DYNAMIC MEMORY

Allocating memory dynamically is a potent source of bugs, and perhaps the most common bugs in this context are memory leaks. Just to remind you, a memory leak arises when you use the new operator to allocate memory, but you never use the delete operator to free it again when you are done with it. Apart from just forgetting to delete memory that you have allocated, you should particularly be aware that non-virtual destructors in a class hierarchy can also cause the problem because they can cause the wrong destructor to be called when an object is destroyed, as you have seen. Of course, when your program ends, all the memory is freed; however, while it is running, it remains allocated to your program. Memory leaks present no obvious symptoms much of the time, maybe never in some cases, but memory leaks are detrimental to the performance of your machine because memory is being occupied to no good purpose. Sometimes, it can result in a catastrophic failure of the program when all available memory has been allocated.

For checking your program's use of the free store, Visual C++ 2010 provides a range of diagnostic routines; these use a special debug version of the free store. These are declared in the header crtdbg.h. All calls to these routines are automatically removed from the release version of your program, so you don't need to worry about adding preprocessor controls for them.

Functions for Checking the Free Store

Here's an overview of what's involved in checking free store operations and how memory leaks can be detected. The functions declared in crtdbg.h check the free store using a record of its status stored in a structure of type _CrtMemState. This structure is relatively simple, and is defined as:

typedef struct _CrtMemState
{
  struct _CrtMemBlockHeader* pBlockHeader; // Ptr to most recently allocated block
  unsigned long lCounts[_MAX_BLOCKS];      // Counter for each type of block
  unsigned long lSizes[_MAX_BLOCKS];// Total bytes allocated in each block type
  unsigned long lHighWaterCount;    // The most bytes allocated at a time up to now
  unsigned long lTotalCount;        // The total bytes allocated at present
} _CrtMemState;

You won't be concerned directly with the details of the state of the free store because you are using functions that present the information in a more readable form. There are quite a few functions involved in tracking free store operations, but you will only look at the five most interesting ones. These provide you with the following capabilities:

  • To record the state of the free store at any point

  • To determine the difference between two states of the free store

  • To output state information

  • To output information about objects in the free store

  • To detect memory leaks

Here are the declarations of these functions together with a brief description of what they do:

void _CrtMemCheckpoint(_CrtMemState* state);

This stores the current state of the free store in a _CrtMemState structure. The argument you pass to the function is a pointer to a _CrtMemState structure in which the state is to be recorded.

int _CrtMemDifference(_CrtMemState* stateDiff,
                      const _CrtMemState* oldState,
                      const _CrtMemState* newState);

This function compares the state specified by the third argument, with a previous state that you specify in the second argument. The difference is stored in a _CrtMemState structure that you specify in the first argument. If the states are different, the function returns a non-zero value (true); otherwise, 0 (false) is returned.

void _CrtMemDumpStatistics(const _CrtMemState* state);

This dumps information about the free store state specified by the argument to an output stream. The state structure pointed to by the argument can be a state that you recorded using _CrtMemCheckpoint() or the difference between two states produced by _CrtMemDifference().

void _CrtMemDumpAllObjectsSince(const _CrtMemState* state);

This function dumps information on objects allocated in the free store, since the state of the free store is specified by the argument; this has been recorded by an earlier call in your program to _CrtMemCheckpoint(). If you pass null to the function, it dumps information on all objects allocated since the start of execution of your program.

int _CrtDumpMemoryLeaks();

This is the function you need for the example as it checks for memory leaks and dumps information on any leak that is detected. You can call this function at any time, but a very useful mechanism can cause the function to be called automatically when your program ends. If you enable this mechanism, you get automatic detection of any memory leaks that occurred during program execution, so let's see how you can do that.

Controlling Free Store Debug Operations

You control free store debug operations by setting a flag, _crtDbgFlag, which is of type int. This flag incorporates five separate control bits, including one to enable automatic memory leak checking. You specify these control bits using the following identifiers:

_CRTDBG_ALLOC_MEM_DF

When this bit is on, it turns on debug allocation so the free store state can be tracked.

_CRTDBG_DELAY_FREE_MEM_DF

When this is on, it prevents memory from being freed by delete, so that you can determine what happens under low-memory conditions.

_CRTDBG_CHECK_ALWAYS_DF

When this is on, it causes the _CrtCheckMemory() function to be called automatically at every new and delete operation. This function verifies the integrity of the free store, checking, for example, that blocks have not been overwritten by storing values beyond the range of an array. A report is output if any defect is discovered. This slows execution but catches errors quickly.

_CRTDBG_CHECK_CRT_DF

When this is on, the memory used internally by the run-time library is tracked in debug operations.

_CRTDBG_LEAK_CHECK_DF

Causes leak checking to be performed at program exit by automatically calling _CrtDumpMemoryLeaks(). You only get output from this if your program has failed to free all the memory that it allocated.

By default, the _CRTDBG_ALLOC_MEM_DF bit is on, and all the others are off. You must use the bitwise operators to set and unset combinations of these bits. To set the _crtDbgFlag flag you pass a flag of type int to the _CrtDbgFlag() function that implements the combination of indicators that you require. This puts your flag into effect and returns the previous status of _CrtDbgFlag. One way to set the indicators you want is to first obtain the current status of the _crtDbgFlag flag. Do this by calling the _CrtSetDbgFlag() function with the argument _CRTDBG_REPORT_FLAG as follows:

int flag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG);              // Get current flag

You can then set or unset the indicators by combining the identifiers for the individual indicators with this flag using bitwise operators. To set an indicator on, OR the indicator identifier with the flag. For example, to set the automatic leak checking indicator on, in the flag, you could write:

flag |= _CRTDBG_LEAK_CHECK_DF;

To turn an indicator off, you must AND the negation of the identifier with the flag. For example, to turn off tracking of memory that is used internally by the library, you could write:

flag &= ~_CRTDBG_CHECK_CRT_DF;

To put your new flag into effect, you just call _CrtSetDbgFlag() with your flag as the argument:

_CrtSetDbgFlag(flag);

Alternatively, you can OR all the identifiers for the indicators that you want together, and pass the result as the argument to _CrtSetDbgFlag(). If you just want to leak check when the program exits, you could write:

_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF|_CRTDBG_ALLOC_MEM_DF);

If you need to set a particular combination of indicators, rather than setting or unsetting bits at various points in your program, this is the easiest way to do it. You are almost at the point where you can apply the dynamic memory debugging facilities to our example. You just need to look at how you determine where free store debugging output goes.

Free Store Debugging Output

The destination of the output from the free store debugging functions is not the standard output stream; by default it goes to the debug message window. If you want to see the output on stdout you must set this up. There are two functions involved in this: _CrtSetReportMode(), which sets the general destination for output, and _CrtSetReportFile(), which specifies a stream destination. The _CrtSetReportMode() function is declared as:

int _CrtSetReportMode(int reportType, int reportMode);

There are three kinds of output produced by the free store debugging functions. Each call to the _CrtSetReportMode() function sets the destination specified by the second argument for the output type specified by the first argument. You specify the report type by one of the following identifiers:

_CRT_WARN

Warning messages of various kinds. The output when a memory leak is detected is a warning.

_CRT_ERROR

Catastrophic errors that report unrecoverable problems.

_CRT_ASSERT

Output from assertions (not output from the assert() function that I discussed earlier).

The crtdbg.h header defines two macros, ASSERT and ASSERTE, that work in much the same way as the assert() function in the standard library. The difference between these two macros is that ASSERTE reports the assertion expression when a failure occurs, whereas the ASSERT macro does not.

You specify the report mode by a combination of the following identifiers:

_CRTDBG_MODE_DEBUG

This is the default mode, which sends output to a debug string that you see in the Debug window when running under control of the debugger.

_CRTDBG_MODE_FILE

Output is to be directed to an output stream.

_CRTDBG_MODE_WNDW

Output is presented in a message box.

_CRTDBG_REPORT_MODE

If you specify this, the _CrtSetReportMode() function just returns the current report mode.

To specify more than one destination, you simply OR the identifiers using the | operator. You set the destination for each output type with a separate call of the _CrtSetReportMode() function. To direct the output when a leak is detected to a file stream, you can set the report mode with the following statement:

CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);

This just sets the destination generically as a file stream. You still need to call the _CrtSetReportFile() function to specify the destination.

The _CrtSetReportFile() function is declared as:

_HFILE _CrtSetReportFile(int reportType, _HFILE reportFile);

The second argument here can either be a pointer to a file stream (of type _HFILE), which I will not go into further, or it can be one of the following identifiers:

_CRTDBG_FILE_STDERR

Output is directed to the standard error stream, stderr.

_CRTDBG_FILE_STDOUT

Output is directed to the standard output stream, stdout.

_CRTDBG_REPORT_FILE

If you specify this argument, the _CrtSetReportFile() function will just return the current destination.

To set the leak detection output to the standard output stream, you can write:

_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDOUT);

The debugging output can be quite voluminous, so you might want to redirect the output stream to a file. You can redirect a standard stram to a file using the freopen_s() function that is defined in the cstdio header. It has the following prototype:

errno_t freopen(File** pFile, const char* filepath,  const char* mode,
                                                                   File* stream);

pFile is a pointer to a file pointer in which the function will store the address of the file. filepath is the path to the file to which the stream is to be redirected. mode is the access mode permitted for the file and can be any of the following:

MODE

EFFECT

"r"

Opens the existing file specified by filepath for reading. If the file does not exist, the function will fail and return a non-zero error code.

"w"

Opens the file specified by filepath file for writing. If the file does not exist it will be created.

"a"

Opens the file specified by filepath for appending data after the EOF marker. If the file does not exist, it will be created.

"r+"

Opens the existing file specified by filepath for reading and writing.

"w+"

Opens an empty file specified by filepath for reading and writing. If the file exists, any contents will be overwritten.

"a+"

Opens the file specified by filepath for reading and appending data, overwriting the existing EOF marker. If the file does not exist, it will be created.

stream is the stream to be redirected to the file identified by filepath.

Here's how you could redirect stdout to a file with the name debug_out.txt:

FILE *pOut(nullptr);
  errno_t err = freopen_s(&pOut, "debug_out.txt", "w", stdout );

   if(err)
    cout << "error on freopen" << endl;
   else
     cout << "successfully reassigned" << endl;

Supplying just the file name as the second argument uses the current default file path, so the file will be created in the same folder as the source file.

You now have enough knowledge of the free store debug routines to try out leak detection in your example.

DEBUGGING C++/CLI PROGRAMS

Life is simpler with C++/CLI programming. None of the complications of corrupted pointers or memory leaks arise in programs written for the CLR, so this reduces the debugging problem substantially, compared to native C++, at a stroke. You set breakpoints and tracepoints in a CLR program exactly the same way that you do for a native C++ code. You have a specific option that applies to C++/CLI code for preventing the debugger from stepping through library code. If you select the Tools

DEBUGGING C++/CLI PROGRAMS
FIGURE 11-17

Figure 11.17. FIGURE 11-17

Checking the option highlighted in Figure 11-17 ensures that the debugger only steps through your source statements and executes the library code normally.

Using the Debug and Trace Classes

The Debug and Trace classes in the System::Diagnostics namespace are for tracing execution of a program for debugging purposes. The capabilities provided by the Debug and Trace classes are identical; the difference between them is that Trace functions are compiled into release builds, whereas Debug functions are not. Thus you can use Debug class functions when you are just debugging your code, and Trace class functions when you want to obtain Trace information in release versions of your code for performance monitoring or diagnostic and maintenance purposes. You also have control over whether the compile includes trace code in your program.

Because the functions and other members in the Debug and Trace classes are identical, I'll just describe the capability in terms of the Debug class.

Generating Output

You can produce output using the Debug::WriteLine() and Debug::Write() functions that write messages to an output destination; the difference between these two functions is that the WriteLine() function writes a newline character after the output whereas the Write() function does not. They both come in four overloaded versions; I'll use the Write() function as the example, but WriteLine() versions have the same parameter lists:

FUNCTION

DESCRIPTION

Debug::Write(String^ message)

Writes message to the output destination.

Debug::Write(String^ message, String^ category)

Writes category followed by message to the output destination. A category name is used to organize the output.

Debug::Write(Object^ value)

Writes the string returned by value->ToString() to the output destination.

Debug::Write(Object^ value, String^ category)

Writes category followed by the string returned by value->ToString() to the output destination.

The WriteIf() and WriteLineIf() are conditional versions of the Write() and WriteLine() functions in the Debug class:

FUNCTION

DESCRIPTION

Debug::WriteIf(bool condition, String^ message)

Writes message to the output destination if condition is true; otherwise, no output is produced.

Debug::WriteIf(bool condition, String^ message, String^ category)

Writes category followed by message to the output destination if condition is true; otherwise, no output is produced.

Debug::WriteIf(bool condition, Object^ value)

Writes the string returned by value->ToString() to the output destination if condition is true; otherwise, no output is produced.

Debug::WriteIf(bool condition, Object^ value, String^ category)

Writes category followed by the string returned by value->ToString() to the output destination if condition is true; otherwise, no output is produced.

As you see, the WriteIf() and WriteLineIf() functions have an extra parameter of type bool at the beginning of the parameter list for the corresponding Write() or WriteLine() function, and the argument for this determines whether or not output occurs.

You can also write output using the Debug::Print() function that comes in two overloaded versions:

FUNCTION

DESCRIPTION

Print(String^ message)

This writes message to the output destination followed by a newline character.

Print(String^ format, ...array<Object^>^ args)

This works in the same way as formatted output with the Console::WriteLine() function. The format string determines how the arguments that follow it are presented in the output.

Setting the Output Destination

By default the output messages are sent to the Output window in the IDE, but you can change this by using a listener. A listener is an object that directs debug and trace output to one or more destinations. Here's how you can create a listener and direct debug output to the standard output stream:

TextWriterTraceListener^ listener = gcnew TextWriterTraceListener(Console::Out);
Debug::Listeners->Add(listener);

The first statement creates a TextWriterTraceListener object that directs the output to the standard output stream, which is returned by the static property Out in the Console class. (The In and Error properties in the Console class return the standard input stream and standard error stream respectively.) The Listeners property in the Debug class returns a collection of listeners for debug output, so the statement adds the listener object to the collection.

Alternatively, you could add a ConsoleTraceListener to direct output to the console screen, as follows:

ConsoleTraceListener^ myOutput = gcnew ConsoleTraceListener();
Debug::Listeners->Add(myOutput);

You could add other listeners that additionally direct output elsewhere (to a file perhaps).

Note

The Trace and Debug classes share the same collection of listeners, so if you add a listener as described above for either the Debug class or the Trace class, it will also apply to output from both classes.

Indenting the Output

You can control the indenting of the debug and trace messages. This is particularly useful in situations where functions are called at various depths. By indenting the output at the beginning of a function and removing the indent before leaving the function, the debug or trace output is easily identified and you'll be able to see the depth of function call from the amount of indentation of the output.

To increase the current indent level for output by one (one indent unit is four spaces by default), you call the static Indent() function in the Debug class like this:

Debug::Indent();                       // Increase indent level by 1

To reduce the current indent level by one you call the static Unindent() function:

Debug::Unindent();                     // Decrease indent level by 1

The current indent level is recorded in the static IndentLevel property in the Debug class, so you can get or set the current indent level through this property. For example:

Debug::IndentLevel = 2*Debug::IndentLevel;

This statement doubles the current level of indentation for subsequent debug output.

The number of spaces in one indent unit is recorded in the static IndentSize property in the Debug class. You can retrieve the current indent size and change it to a different value. For example:

Console::WriteLine(L"Current indent unit = {0}", Debug::IndentSize);
Debug::IndentSize = 2;                 // Set indent unit to 2 spaces

The first statement simply outputs the indent size and the second statement sets it to a new value. Subsequent calls to Indent()increases the current indentation by the new size, which is two spaces.

Controlling Output

Trace switches provide you with a way to switch any of the debug or trace output on and off. You have two kinds of trace switches you can use:

  • The BooleanSwitch reference class objects provide you with a way to switch segments of output on or off depending on the state of the switch.

  • The TraceSwitch reference class objects provide you with a more sophisticated control mechanism because each TraceSwitch object has four properties that correspond to four control levels for output statements.

You could create a BooleanSwitch object to control output as a static class member like this:

public ref class MyClass
{
private:
  static BooleanSwitch^ errors =
                           gcnew BooleanSwitch(L"Error Switch", L"Controls error
output");

public:
  void DoIt()
  {
    // Code...

    if(errors->Enabled)
      Debug::WriteLine(L"Error in DoIt()");

    // More code...
  }
  // Rest of the class...
};

This shows the errors object as a static member of MyClass. The first argument to the BooleanSwitch constructor is the display name for the switch that is used to initialize the DisplayName property, and the second argument sets the value of the Description property for the switch. There's another constructor that accepts a third argument of type String^ that sets the Value property for the switch.

The Enabled property for a Boolean switch is of type bool and is false by default. To set it to true, you just set the property value accordingly:

errors->Enabled = true;

The DoIt() function in MyClass outputs the debug error message only when the errors switch is enabled.

The TraceSwitch reference class has two constructors that have the same parameters as the BooleanSwitch class constructors. You can create a TraceSwitch object like this:

TraceSwitch^ traceCtrl =
                        gcnew TraceSwitch(L"Update", L"Traces update operations");

The first argument to the constructor sets the value of the DisplayName property, and the second argument sets the value of the Description property.

The Level property for a TraceSwitch object is an enum class type, TraceLevel, and you can set this property to control trace output to any of the following values:

VALUE

DESCRIPTION

TraceLevel::Off

No trace output.

TraceLevel::Info

Output information, warning, and error messages.

TraceLevel::Warning

Output warning and error messages.

TraceLevel::Error

Output Error messages.

TraceLevel::Verbose

Output all messages.

The value you set determines the output produced. To get all messages, you set the property as follows:

traceCtrl->Level = TraceLevel::Verbose;

You determine whether a particular message should be issued by your trace and debug code by testing the state of one of four properties of type bool for the TraceSwitch object:

PROPERTY

DESCRIPTION

TraceVerbose

Returns the value true when all messages are to be output.

TraceInfo

Returns the value true when information messages are to be output.

TraceWarning

Returns the value true when warning messages are to be output.

TraceError

Returns the value true when error messages are to be output.

You can see from the significance of these property values that setting the Level property also sets the states of these properties. If you set the Level property to TraceLevel::Warning for instance, TraceWarning and TraceError is set to true and TraceVerbose and TraceInfo are set to false.

To decide whether to output a particular message, you just test the appropriate property:

if(traceCtrl->TraceWarning)
  Debug::WriteLine(L"This is your last warning!");

The message is output only if the TraceWarning property for traceCtrl is true.

Assertions

The Debug and Trace classes have static Assert() functions that provide a similar capability to the native C++ assert() function. The Assert() function in the Debug class only works in debug builds. The Assert() function in the Trace class works in release builds. The first argument to the Debug::Assert() function is a bool value or expression that causes the program to assert when the argument is false. The call stack is then displayed in a dialog as shown in Figure 11-18.

FIGURE 11-18

Figure 11.18. FIGURE 11-18

Figure 11-18 shows an assertion that is produced by the next example. When the program asserts, the call stack presented in the dialog shows the line numbers in the code and the names of the functions that are executing at that point. Here there are four functions executing, including main().

You have three courses of action after an assertion. Clicking the Abort button ends the program immediately; clicking the Ignore button allows the program to continue; and clicking the Retry button gives you the option of executing the program in debug mode.

The following four overloaded versions of the Assert() function are available:

FUNCTION

DESCRIPTION

Debug::Assert(bool condition)

When condition is false, a dialog displays showing the call stack at that point.

Debug::Assert(bool condition, String^ message)

As above, but with message displayed in the dialog above the call stack information.

Debug::Assert(bool condition, String^ message, String^ details)

As the preceding version, but with details displayed additionally in the dialog.

Debug::Assert(bool condition, String^ message, String^ msgFormat, Object[] args)

When condition is false, it asserts as in the preceding version, but the String.Format() function is also called with msgFormat and args as arguments to produce additional output in the dialog.

The message argument and other output is directed to the message box that results from an assertion by the DefaultTraceListener object that is in the listeners collection that is shared by both the Debug and Trace classes. You can add your own listeners to direct output to other destinations.

Seeing it working is the best aid to understanding, so I put together an example that demonstrates debug and trace code in action.

Getting Trace Output in Windows Forms Applications

It's often useful to be able to output trace information from a Windows Forms application. There are a couple of barriers in the way of doing this: Firstly, the output can be voluminous, particularly if you want to trace the execution of functions that execute very frequently, such as those dealing with mouse events, and secondly and more significantly, there is no console with a Windows Forms application to receive the output. You can get around this by adding some code to the constructor of the class encapsulating the form for the application, which reassigns output for Console::Out stream to a file. I won't explain every detail of the class objects involved and how this code works at this point but I'll just offer it here as something you can plug into a Windows Forms application:

FileStream^ fs = gcnew FileStream(L"Trace.txt", FileMode::Create);
StreamWriter^ sw = gcnew StreamWriter(fs);
sw->Autoflush = true;
Console::SetOut(sw);

The first statement creates a stream that encapsulates the file Trace.txt that will be created in the current directory. You can specify a full path if you want; just set the first argument to the FileStream constructor as L"CDebug OutputMyTrace.txt", for example. The next two statements set up an object that can write to the stream, fs, and make it flush data to the stream automatically. The last statement reassigns Console::Out to the StreamWriter object. To compile this code successfully you must add a using directive for the System::IO namespace to the header file containing the class definition for the form.

With this code in place you can put Console output statements anywhere in the code for the form to trace the execution sequence or to record data values. You can inspect the trace file with any simple text editor, such as Notepad.

SUMMARY

Debugging is a big topic, and Visual C++ 2010 provides many debugging facilities beyond what I have discussed here. If you are comfortable with what I have covered in this chapter, you should have little trouble expanding your knowledge of the debug capabilities through the Visual C++ 2010 documentation. Searching on "debugging" should generate a rich list of further information.

With the basics of debugging added to your knowledge of C++, you are ready for the big one: Windows programming!

WHAT YOU LEARNED IN THIS CHAPTER

TOPIC

CONCEPT

Assertions

You can use the assert() library function that is declared in the cassert header to check logical conditions in your native C++ program that should always be true.

_DEBUG symbol

The preprocessor symbol _DEBUG is automatically defined in the debug version of a native C++program. It is not defined in the release version.

Debugging code

You can add your own debugging code by enclosing it between an #ifdef/#endif pair of directives testing for _DEBUG. Your debug code is then included only in the debug version of the program.

crtdbg.h header

The crtdbg.h header supplies declarations for functions to provide debugging of free store operations.

_crtDbgFlag flag

By setting the _crtDbgFlag appropriately, you can enable automatic checking of your program for memory leaks.

Debug output

To direct output messages from the free store debugging functions, you call the _CrtSetReportMode() and _CrtSetReportFile() functions.

C++/CLI debugging

Debugging operations using breakpoints and trace points in C++/CLI programs are exactly the same as in native C++ programs.

C++/CLI classes for debugging

The Debug and Assert classes defined in the System::Diagnostics namespace provide functions for tracing execution and generating debugging output in CLR programs.

Assertions in C++/CLI programs

The static Assert() function in the Debug and Trace classes provides an assertion capability in CLR programs.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.230.30