
Teach Yourself Visual C++ 6 in 21 Days
 
- E -
Using the Debugger and Profiler
by Jon Bates
Creating Debugging and Browse Information
A large part of application development is actually debugging your program. All
software development is a tight cycle of application design, implementation, and
debugging.
Visual C++ has an extensive debugging environment and a range of debugging tools
that really help with program development. You can quickly identify problems, watch
the contents of variables, and follow the flow of programs through your own code
and the MFC code.
Tools such as the Spy++ program can show you the messages passed between Windows
and your application and let you spy on applications to see which user interface
controls and Window styles they use.
Using Debug and Release Modes
There are two main compiler configurations that you can set to build your application:
Debug and Release mode. You can change these modes by clicking the Project menu and
selecting the Settings option or by pressing Alt+F7, which will display the Project
Settings dialog box (see Figure E.1). The main project settings are shown at the
top level and can be changed by selecting the options listed in the combo box. When
one setting is selected, changes that you make to any options on the tabs on the
right will be set against that configuration. When you build the application, it
will be built using your current configuration settings, or you can select All Configurations
to build and make changes to all configurations simultaneously.
FIGURE E.1. The
C/C++ tab of the Project Settings dialog box.
Both Release and Debug configurations are supplied whenever you
create a new project; they produce very different object code. When configured for
Debug mode, your build will produce a large and fairly slow executable program. This
is because lots of debugging information is included in your program and all the
compiler optimizations are disabled.
When you compile the same program in Release mode, you'll see a small, fast executable
program, but you won't be able to step through its source code or see any debugging
messages from it.
Normally, when developing an application, you leave the compiler set to Debug
mode so that you can easily spot and debug problems that arise in your code.
When you've finished your application and are preparing to release it, you can set
the configuration to Release mode and produce a small, fast program for your
users.
RELEASE MODE TESTING
You should always fully test your application after rebuilding it in Release mode
before sending it to users. Bugs can arise from things such as leaving proper program
code in ASSERT macros (discussed later this chapter), which are then removed, or
because of the effect of some speed and memory optimizations.
Setting Debug Options and Levels
You can set a variety of debugging options and levels from the C/C++ tab of the
Project Settings dialog box. This dialog page is available from the Project menu
by selecting the Settings option (or by pressing Alt+F7) and then selecting the C/C++
tab.
With the General Category selected, the following items are available:
- Warning Level. This is the level of compiler warning messages given during
compilation. You can set it to any of the values shown in Table E.1. The default
level is Level 3, which is quite sensitive, although many good C++ programmers insist
on using Level 4 to get the most warning of potential problems from the compiler.
Level 1 and no warnings (None) should be used only in special circumstances because
they indicate only severe warnings (or none at all).
LEVEL 4 WARNINGS
At level 4, you'll find that Microsoft's own AppWizard-generated code gives warnings
(although usually only about unused function parameters that can be safely ignored).
- Warnings as Errors. When you check this, warning messages are shown as
errors that then stop the compiler.
- Generate Browse Info. When you check this, the compiler generates information
that can be used to help you locate functions, symbols, and class relationships shown
in a Browse window (discussed in the next section). Unfortunately, generating this
useful information increases the compilation time quite a bit for large projects
(where you most need it).
- Debug Info. This lets you specify the level of debugging information generated
by the compiler, as shown in Table E.2.
- Optimizations. In Debug mode, you would normally leave these disabled
because they interfere with the debugging process and take longer to compile. However,
in Release mode you can decide whether to Maximize Speed or Minimize Size of your
application (or a default that compromises to get the best of both).
- Preprocessor Definitions. This specifies manifest definitions that are
defined when your program is compiled. You can use these definitions in conjunction
with the #ifdef, #else, and #endif preprocessor commands to compile sections of code
in specific configurations. The _DEBUG definition is set by default when in Debug
mode. You can use this to compile Debug mode-only code in your application like this:
int a = b * c / d + e;
#ifdef _DEBUG
CString strMessage;
strMessage.Format("Result of sum was %d",a);
AfxMessageBox(strMessage);
#endif
- The message box code is then compiled and run when your application is built
in Debug mode. When you switch to Release mode, the code isn't compiled into your
executable.
- Project Options. The compiler itself runs as a console-based application
and converts your Developer Studio options into several flags to be passed on the
command line. You can add additional flag settings for more obscure compiler settings
that don't have a user interface switch directly into this edit box.
TABLE E.1. COMPILER WARNING LEVELS.
Level |
Warnings Reported |
None |
None |
Level 1 |
Only the most severe |
Level 2 |
Some less severe messages |
Level 3 |
Default level (all reasonable warnings) |
Level 4 |
Very sensitive (good for perfectionists) |
Table E.2. Debug info settings.
Setting |
Debugging Information Generated |
None |
Produces no debugging information--usually reserved for Release modes. |
Line Numbers Only |
This generates only line numbers that refer to the source code for functions and
global variables. However, compile time and executable size are reduced. |
C 7.0-Compatible |
This generates debugging information that is compatible with Microsoft C 7.0. It
places all the debugging information into the executable files and increases their
size, but allows full symbolic debugging. |
Program Database |
This setting produces a file with a .pdb extension that holds the maximum level of
debugging information, but doesn't create the Edit and Continue information. |
Program Database for Edit and Continue |
This is the default and usual debug setting. It produces a .pdb file with the highest
level of debugging and creates the information required for the new Edit and Continue
feature. |
Creating and Using Browse Information
You can use the Source Browser tool to inspect your source code in detail. This
tool can be invaluable if you are examining someone else's code or coming back to
your own code after you haven't viewed it for awhile.
To use the Source Browser, you must compile the application with the Generate
Browse Info setting checked, in the C/C++ tab of the Project Settings dialog box.
To run the tool, press Alt+F12 or click the Tools menu and select the Source Browser
option. (The first time you run the tool, it will ask you to compile the browser
information.)
The first dialog box the Source Browser presents requests an Identifier to browse
for (as shown in Figure E.2). This identifier can be a class name, structure name,
function name, or global or local variable name in your application. After you have
entered an identifier, the OK button is enabled, and you can browse for details about
that identifier.
FIGURE E.2. The
Browse dialog box requesting a symbol to browse.
Select Query offers various options for details pertaining to your chosen symbol.
You can choose from any of the following:
- Definitions and References. This option shows you all the files that have refer-ences
to the specified identifier and whether they are references to the identifier (places
where it is used in the code) or definitions (places where the identifier is defined),
as shown in Figure E.3. The line numbers are listed along with the filenames in each
file. By double-clicking one of the references or definitions, the code to which
it refers will be loaded and shown in the Developer Studio editor at that specific
position. This is very useful for tracking all the places that a specific variable
or function is used.
FIGURE E.3. Source
Browser showing definitions and references.
- File Outline. This option shows you all the classes, data, functions, macros,
and types that are defined in the specified filename (identifier), as shown in Figure
E.4. You can filter each type in or out by pressing relevant buttons along the top
of the browser window.
FIGURE E.4. The
file outline display of the source browser.
- Base Classes and Members. This arguably is one of the most useful options of
the source browser. By specifying a class as the identifier, all the classes' hierarchy
and member functions and variables at each hierarchy level are displayed (see Figure
E.5). You can also set the filter options to show only certain types of member functions
and variables.
FIGURE E.5. The
Base Classes and Members view of the source browser.
- Derived Classes and Members. This view is also very useful and shows all the
classes that are derived from the specified class, along with their own member functions
and variables. You can also use the browser with the MFC classes to gain more insight
into the MFC implementation, as shown with the MFC CWnd class in Figure E.6.
FIGURE E.6. The
Derived Classes and Members view of the Source Browser showing CWnd-derived classes.
- Call Graph. The Call Graph option shows you all the functions that are called
by a specified identifier and the files in which they are defined and implemented.
This lets you quickly track the potential flow of a program.
- Callers Graph. The corresponding Callers Graph option shows you all the functions
that call the specified identifier. You can use this to track the possible callers
of your specified function.
Using Remote and Just-in-Time Debugging
The debugger includes tools that let you debug a program running on a remote machine
(even over the Internet via TCP/IP). This can be useful if you want to test your
application in a different environment other than your development machine. To do
this, you must have exactly the same versions of the .dll and .exe files on both
machines. After loading the project, you can debug it via a shared directory from
the remote machine by changing the Executable for Debug Session edit box to the path
and filename of your local .exe file (located in the Project Settings dialog box
under the Debug tab).
You must also add a path to the .exe file in the Remote Executable Path and File
Name edit box at the bottom of the Debug tab, leaving the Working Directory blank.
You can then start the remote debugger monitor on the remote computer by running
the MSVCMON.EXE program and connecting to it by clicking the Build menu and selecting
the Debugger Remote Connection option.
From the Remote Connection dialog box you can choose Local for a shared directory
debug session or Remote to debug via a TCP/IP connection. (You can set the address
by clicking Settings.) This will connect to the remote monitor that will start the
remote debugging session.
Installing the Remote Debugger Files
You will also need the following files to run the remote debugger monitor on the
remote machine: MSVCMON.EXE, MSVCRT.DLL, TLN0T.DLL, DM.DLL, MSVCP5O.DLL, and MSDIS100.DLL.
These files can be found in your installed ...\Microsoft Visual Studio\Common\MSDev98\bin
subdirectory.
Just-in-time debugging lets you debug a program that was run normally (not through
the debugger) and then developed a problem. If you have Visual C++ installed on a
machine and this option is enabled, any program that develops a fault will be loaded
into a new Developer Studio session ready for debugging and show the code that caused
the crash.
This often raises a chuckle when Developer Studio itself crashes and then proceeds
to load another session of itself, offering you an assembly code view of where the
crash took place in the original for you to debug. It can be very useful to debug
your own applications when they crash unexpectedly (usually in a demonstration to
your boss). You can enable this option by clicking the Tools menu and selecting Options
to display the Options dialog box. Then select the Debug tab and ensure that the
Just-in-Time debugging check box is checked.
The OLE RPC debugging option on this tab is also very useful when developing COM
and DCOM applications because it lets the debugger traverse a function call into
another out-of-process program or .dll and lets another debugger take over for the
other process. It then hands control back when returning from the remote function
and works across networks and different computers.
Tracing and Single Stepping
One of the most useful features of the Visual C++ debugging environment is the
interactive single stepping. This feature lets you step through the code one line
at a time and examine the contents of variables as you go. You can also set breakpoints
so that the program runs until it reaches a breakpoint and then stops at that point,
letting you step from that point until you want to continue running.
Trace statements and assertions are also very useful tools for finding program
faults. Trace statements let you display messages and variables from your program
in the output window as it runs through trace statements. You can use assertions
to cause the program to stop if a condition isn't TRUE when you assert that it should
be.
Using the TRACE Macro
You can add TRACE macros to your program at various places to indicate that various
parts of the code have been run or to display the contents of variables at those
positions. The TRACE macros are compiled into your code in the debug configuration
and displayed in the Output window on the Debug tab, when you run your program through
the debugger.
You can safely leave in the TRACE macros when you perform a release build because
these macros are automatically excluded from the destination object.
You can display simple messages or output variable contents by passing a format
string as the first parameter to the TRACE macro. This format string is exactly the
same as you would pass to a printf() or CString::Format() function. You can specify
various special formatting codes such as %d to display a number in decimal, %x to
display a number in hexadecimal, or %s to display a string. The following parameters
should then correspond to the order of the formatting codes. For example, the code
int nMyNum = 60;
char* szMyString = "This is my String";
TRACE("Number = %d, or %x in hex and my string is: %s\n",
nMyNum, szMyString);
will result in this output trace line:
Number = 60, or 3c in hex and my string is
ÂThis is my String
Listing E.1 shows the TRACE macro used to display the contents of an array before
and after sorting by a very inefficient but simple sort algorithm.
If you want to try the code shown in Listing E.1, you can use the AppWizard to
build a simple SDI framework. Simply add the code above the OnNewDocument() member
function of your document class and then call it by adding a DoSort() call into your
OnNewDocument() function.
You can run the application through the debugger (click Build, select Start Debug,
and choose Go from the pop-up menu) to see the output trace.
You must ensure that the output window is visible (click the View menu and select
Output) when the tabbed output window is shown (same as the compiler output). Ensure
that the Debug tab is selected.
LISTING E.1. LSTE_1.CPP--A SIMPLE SORT ROUTINE TO DEMONSTRATE DEBUGGING
TECHNIQUES.
1: void Swap(CUIntArray* pdwNumbers,int i)
2: {
3: UINT uVal = pdwNumbers->GetAt(i);
4: pdwNumbers->SetAt(i, pdwNumbers->GetAt(i+1));
5: pdwNumbers->SetAt(i+1,uVal);
6: }
7:
8: void DoSort()
9: {
10: CUIntArray arNumbers;
11: for(int i=0;i<10;i++) arNumbers.Add(1+rand()%100);
12:
13: TRACE("Before Sort\n");
14: for(i=0;i<arNumbers.GetSize();i++)
15: TRACE("[%d] = %d\n",i+1,arNumbers[i]);
16:
17: BOOL bSorted;
18: do
19: {
20: bSorted = TRUE;
21: for(i=0;i<arNumbers.GetSize()-1;i++)
22: {
23: if (arNumbers[i] > arNumbers[i+1])
24: {
25: Swap(&arNumbers,i);
26: bSorted = FALSE;
27: }
28: }
29: } while(!bSorted);
30:
31: TRACE("After Sort\n");
32: for(i=0;i<arNumbers.GetSize();i++)
33: TRACE("[%d] = %d\n",i+1,arNumbers[i]);
34: }
Listing E.1 sorts an array of random numbers (between 1 and 100), generated in
line 11. Lines 13 to 15 then print out the contents of the array before sorting by
TRACE statements. Lines 17 through 29 sort the array by swapping pairs of numbers
that are in the wrong order (by calling the Swap() function in line 25). The Swap()
function (lines 1 to 6) takes a pointer to the array and a position and then swaps
the two numbers at that position.
After sorting, the contents of the array are again printed in the output window
by the TRACE statements in lines 31 to 33.
The trace output of this program appears in the Output window of Developer Studio,
as shown in Table E.3.
TABLE E.3. OUTPUT FROM THE SORTING PROGRAM.
BEFORE SORT
|
AFTER SORT
|
[1] = 42 |
[1] = 1 |
[2] = 68 |
[2] = 25 |
[3] = 35 |
[3] = 35 |
[4] = 1 |
[4] = 42 |
[5] = 70 |
[5] = 59 |
[6] = 25 |
[6] = 63 |
[7] = 79 |
[7] = 65 |
[8] = 59 |
[8] = 68 |
[9] = 63 |
[9] = 70 |
[10] = 65 |
[10] = 79 |
Using the ASSERT and VERIFY macros
You can use the ASSERT macro to ensure that conditions are TRUE. ASSERT is passed
one parameter that is either a TRUE or FALSE expression. If the expression is TRUE,
all is well. If the expression is FALSE, your program will stop and the Debug Assertion
Failed dialog box will be displayed (see Figure E.7), prompting you to Abort the
program, Retry the code, or Ignore the assertion. It also shows the program, source
file, and line number where the assertion failed. If you choose Abort, the debugging
session is terminated. Retry is probably the most useful option because the compiler
will then show you the code where the ASSERT macro has failed, enabling you to figure
out what went wrong. If you already know or don't care about the assertion, you can
choose Ignore and continue running the program, which might then result in a more
fatal error.
FIGURE E.7. The
Debug Assertion Failed dialog box helps you track down bugs.
A common use of ASSERT is to ensure that input parameters to functions are correct.
For example, you can make the Sort() function (shown in Listing E.1) more robust
by checking its input parameters. To check the input parameters, add ASSERT macros
at the top of the Sort() function like this:
ASSERT(pdwNumbers);
ASSERT(i>=0 && i<10);
This will ensure that the pointer to the numbers array isn't zero and that the
position to swap is between 0 and 9. If either of these is incorrect, the Debug Assertion
Failed dialog box is displayed. This technique helps you track down errors caused
by passing faulty parameters to functions. It is a good practice to use the ASSERT
macro to check that the values passed to each of your functions conform to your expectations.
Another macro, ASSERT_VALID, can be used with CObject-derived classes such as
most MFC classes. This performs a more thorough check on the object and its contents
to ensure the entire object is in a correct and valid state. You can pass a pointer
to the object to be checked like this:
ASSERT_VALID(pdwNumbers);
Another ASSERT macro is ASSERT_KINDOF, which is used on CObject-derived classes
to check that they are of the correct class type. For example, you can check that
a pointer to your view object is of the correct view class like this:
ASSERT_KINDOF(CYourSpecialView,pYView);
The Assertion Failed dialog box will be displayed if it isn't of the correct class
type or any of its derivatives.
You must be careful not to put any code that is needed for normal program operation
into ASSERT macros because they are excluded in the release build. A common source
of release mode errors that are hard to track down is coding like this:
int a = 0;
ASSERT(++a > 0);
if (a>0) MyFunc();
In the debug build, this code will increment the integer a in the ASSERT line
and then call MyFunc() in the following line because a is greater than zero. When
your sales team is eager to demonstrate your new application, you might think it
works fine because there aren't any Debug mode problems. So you recompile it in Release
mode and hand it over to your sales department, which demonstrates it to a customer,
whereupon it crashes badly. It crashes because the ++a isn't performed--the release
mode excludes ASSERT lines.
The VERIFY macro helps with this problem. VERIFY works like ASSERT, and in Debug
mode it throws the same Assertion Failed dialog box if the expression is zero. However,
in release mode the expression is still evaluated, but a zero result won't display
the Assertion dialog box. You will tend to use VERIFY when you always want to perform
an expression and ASSERT when you only want to check while debugging. Therefore,
replacing ASSERT in the previous example with VERIFY, as shown in the following example,
will enable the release build to work properly:
VERIFY(++a > 0);
You are more likely to use VERIFY to check return codes from functions:
VERIFY(MyFunc() != FALSE);
Using Breakpoints and Single Stepping the Program
The use of single stepping and breakpoints is probably the most effective debugging
tool for tracking down the majority of problems. The support for various types of
breakpoints and the single-stepping information available is very sophisticated in
Visual C++; I can only hope to give you a taste of the power of this debugging tool.
The key to single stepping is breakpoints. You can set a breakpoint anywhere in
your code and then run your program through the debugger. When the breakpoint is
reached, the code will be displayed in the editor window at the breakpoint position,
ready for you to single step or continue running.
You can add a breakpoint by selecting the specific code line (clicking the editor
cursor onto the line in the editor window) and then either clicking the Breakpoint
icon in the Build minibar (see Figure E.8) or by pressing F9. Alternatively, most
sophisticated breakpoints can be added or removed by clicking the Edit menu and selecting
the Breakpoints option to display the Breakpoints dialog box (see Figure E.9). When
you add a breakpoint, it's displayed as a small red circle next to the line you have
specified. Breakpoints can be set only against valid code lines, so sometimes the
Developer Studio will move one of your breakpoints to the closest valid code line
for you.
FIGURE E.8. Adding
a breakpoint to your code via the Build minibar toolbar or the F9 key.
FIGURE E.9. Adding
a breakpoint using the Breakpoints dialog box.
You can toggle the breakpoint on or off by clicking the Breakpoint (hand shaped)
icon or remove it by clicking the Remove or Remove All buttons on the Breakpoints
dialog box. You can leave them in position but disable them by clicking the check
mark to the left of each breakpoint listed in the Breakpoints dialog box. Clicking
there again will show the check and re-enable the breakpoint.
When you have set your breakpoint(s), you can run the code through the debugger
by choosing Build, Start Debug, Go. Alternatively, you can use the shortcut by clicking
the Go icon (to the left of the Breakpoint icon on the Build minibar toolbar--refer
to Figure E.8) or by pressing the F5 key.
The program will run as normal until it reaches the breakpoint, where it will
stop and display an arrow against the line with the breakpoint. At that point, you
can use the Debug toolbar to control the single stepping process, as shown in Figure
E.10.
FIGURE E.10. The
debugger stopped at a breakpoint ready for single stepping with the Debug toolbar.
When stopped in the debugger, you can see the contents of most variables merely
by moving the cursor over them in the editor window. Their contents are then displayed
in a ToolTip at the cursor position. More detailed contents are shown by dragging
the variables into the Watch window, as discussed in detail in the next section.
You can single step through the code using the four curly brace icons shown on
the Debug toolbar or by clicking the Debug menu and choosing one of the step options.
The available step options are shown in Table E.4. You can find these on the Debug
menu and the Debug toolbar.
TABLE E.4. STEP OPTIONS AVAILABLE IN SINGLE STEPPING.
Icon/Step Option |
Shortcut Key |
Effect When Selected |
Step Into |
F11 |
The debugger will execute the current line and if the cursor is over a function call,
it will enter that function. |
Step Over |
F10 |
Like Step Into except when over a function call line, it will run that function at
normal speed and then stop when it returns from the function, giving the effect of
stepping over it. |
Step Out |
Shift+F11 |
The debugger will run the rest of the current function at normal speed and stop when
it returns from the function to the calling function. |
Run to Cursor |
Ctrl+F10 |
The debugger will run until it reaches your specified cursor position. You can set
this position by clicking the line you want to run to. |
Go |
F5 |
Continue running the program at normal speed until the next breakpoint is encountered. |
Stop Debugging |
Shift+F5 |
This stops the debugger and returns to editing mode. |
Restart |
Ctrl+Shift+F5 |
This option restarts the program from the beginning, stopping at the very first line
of code. |
Break Execution |
|
This option stops a program running at normal speed in its tracks. |
Apply Code
Changes |
Alt+F10 |
This option lets you compile the code after making changes during a debugging session
and then continue debugging from where you left off. |
By using these options, you can watch the flow of your program and see the contents
of the variables as they are manipulated by the code. The yellow arrow in the Editor
window will always show the next statement to be executed.
The next sections describe some of the debugging windows you can use when you
are stopped in the debugger.
Using Edit and Continue
A great new feature of Visual C++ 6 is the capability to Edit and Continue. This
means that you can change or edit the code while you are stopped in the debugger.
After editing, you'll notice the Debug menu's Apply Code Changes option becomes enabled
(as well as the corresponding debug toolbar icon). You can then select the Apply
Code Changes option (or toolbar button) to compile your new code changes and then
continue debugging the new changed code. By using this new feature, you can fix bugs
while debugging and continue the debug run from the same place in the code with the
same variable settings, which can be very useful when debugging large and complex
programs.
Watching Program Variables
The Watch and Variables windows are shown in Figure E.11. These windows display
the contents of variables when stopped in the debugger. You can view these windows
by clicking the View menu and selecting them from the Debug Windows pop-up menu or
by clicking the icons from the toolbar.
FIGURE E.11. The
Watch window displays contents of variables while debugging.
The Variables window always shows the local variables of the function displayed
in the Context combo box at the top of the window. To get to your current function,
you can drop this combo box list to display all the functions that were called in
turn. This is the call stack and shows your current context within the program by
showing the list of functions that have been called in order to get to the program's
currently executing function where the debugger has stopped. When you select a different
function, the relevant local variables are shown for that function level.
You can expand any object pointers shown by clicking the plus symbol next to the
pointer name. The special C++ this pointer is always shown for class member functions
and can be opened to show all the member variables for the current object.
The Watch window lets you enter variable names from the keyboard or drag variable
names from the editor window (after selecting and inverting them with the mouse point).
The values that are held in the displayed variables are shown until they go out of
scope (that is, aren't relevant to the function currently being debugged).
You can also enter simple casts and array indexes in the Watch window to show
related values. Right-clicking the mouse can switch the displayed values between
hexadecimal and decimal display. As you step through the program, the values shown
in the Watch and Variable windows are updated accordingly so that you can track how
the program changes the variables.
Other Debugger Windows
Other debugging display windows are available by clicking the View menu and selecting
them from the Debug Windows pop-up menu or alternatively by clicking the various
icons shown to the right of the Debug toolbar. These windows are
- QuickWatch. By clicking a variable in the listing and choosing QuickWatch or
pressing Shift+F9, you can display the contents of the select variable. You can also
enter variables directly and then click the Add Watch button to transfer them into
the main Watch window.
- Registers. The Registers window displays the current values in your CPU's register
set. This probably isn't too useful to you unless you are tracking machine or assembly
code-level problems.
- Memory. The Memory window displays the memory from the application's address
space in columns that represent the address, the hex values, and the character values
for each 8 bytes. You can change this display to show Byte, Short, or Long values
by right-clicking to display the appropriate context menu options.
- Call Stack. The Call Stack window shows the list of functions that were called
in order to get to your current function and the parameter values that were passed
to each function. This can be very useful to investigate how the program flow reached
a specific function. By double-clicking any of the listed functions, you can display
the position where the function call was made in the code, shown by the Editor window.
- Where source code isn't available, function entries are shown as follows:
KERNEL32! bff88f75()
- If you click these entries, you'll be shown assembly code rather than C++ code.
- Disassembly. By selecting the Disassembly toolbar button or menu option, you
can toggle between displaying the C++ code mixed with assembly code or just C++ code.
Where the source code is unavailable, only assembly code is shown.
Additional Debugging Tools
Along with the integrated debugging tools are several nonintegrated but very useful
tools. You can start these by clicking the Tools menu and selecting the specific
tool option from the menu.
These tools generally let you track operating-specific items such as Windows messaging,
running processes, and registered OLE objects to enhance your available information
while debugging your application.
Using Spy++
Spy++ is undoubtedly one of the most useful of these tools. With Spy++, you can
see the hierarchical relationships of parent to child windows, the position and flags
settings for windows, and base window classes. You can also watch messages as they
are sent to a window.
When you first run Spy++, it shows all the windows on the current desktop, their
siblings, and the base Windows class of each object (see Figure E.12). The view shown
in Figure E.12 has been scrolled to shown the standard Microsoft Windows CD Player.
Spy++ shows you all the buttons and combo boxes, which are windows in their own right
as child windows of the main CD Player window.
FIGURE E.12. The
Spy++ initial view of the Windows desktop showing the CD Player portion.
If you click the Spy menu, you are shown the following options:
- Messages. You might find that the Messages view is probably one of the most useful
options because you can use it to watch messages that are sent to any window (including
your own application). You can also filter these messages so that you don't receive
an avalanche of Mouse Movement messages.
- To use messages, select this option to display the Message Options dialog box
shown in Figure E.13. You can then drag the finder tool over any window in the system,
displaying the details of the window as it moves. Spy++ also highlights the selected
window, so you can see frame and client windows. When you've located the window you
want to view, just let go of the tool. At this point you can use the other tabs to
set filtering options and output formatting options. When you're finished, click
OK to close the Message Options box.
FIGURE E.13. Using
the Spy++ Message Options Finder to locate windows.
- The output shown in Figure E.14 are the messages produced from using a normal
SDI application's toolbar. As you can see, with no filtering you'll receive many
mouse movements and cursor check messages, but you can also see the familiar WM_LBUTTONUP
message with its position parameters.
FIGURE E.14. Windows
Messages for a toolbar logged by Spy++.
- Windows. The Windows view is the view shown in Figure E.12 of the layout and
structure of the Windows desktop. If you double-click any of these windows, you'll
be shown a property sheet containing all the selected windows' positioning information
and flag settings. To update this information, you must click the Windows menu and
choose Refresh.
- Processes. You can view all the running programs with the Processes view. These
can be opened to show each thread and any windows attached to those threads.
- Threads. The Threads option shows the same details without the processes level
of hierarchy, so you can see every thread running on your machine and the windows
that each thread owns.
Spy++ is too sophisticated to cover in its entirety here, but as a tool for understanding
the structure of Windows hierarchies and messaging, it is unsurpassed. You can glean
a lot of valuable knowledge just by looking at commercial applications with Spy++.
It is also a wonderful tool for debugging messaging problems in your own application
to ensure that your windows are getting the correct messages and to see how these
messages are sequenced.
Process Viewer
You can see all the processes in more detail than shown in Spy++ with the Process
Viewer (PView95.exe). You can start this application from your system's main Windows
Start menu from Programs under the Microsoft Visual Studio 6.0 Tools option (or similar
program group). This application lists the processes running on your machine and
lets you sort them by clicking any of the column headers. You can then click a process
to display all its threads. Figure E.15 shows Process Viewer running with the Developer
Studio application (MSDEV.EXE) selected and all its many threads displayed.
FIGURE E.15. The
Process Viewer showing MSDEV.EXE and its threads.
The OLE/COM Object Viewer
The OLE/COM Object Viewer tool shows you all the registered OLE/COM objects on
your system, including ActiveX controls, type libraries, embeddable objects, automation
objects, and many other categories.
You can even create instances of various objects and view their interfaces in
detail. The OLE/COM Object Viewer is very useful if you are developing an OLE/COM
application or looking for an elusive ActiveX control.
The MFC Tracer
Using the MFC Tracer tool shown in Figure E.16, you can stop the normal tracing
or add specific Windows trace output to the normal program trace output. When you
select this tool, you are shown a set of check boxes that you can check or uncheck
to include that tracing option.
FIGURE E.16. The
MFC Tracer tool options.
You can add Windows messages, database messages, OLE messages, and many other
levels of trace output to help track down elusive problems. These messages are then
generated by the MFC code for the various selected flags.
You can even turn off the standard tracing generated by your application by unchecking
the Enable Tracing option.
 
© Copyright, Macmillan Computer Publishing. All
rights reserved.
|