Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 122 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
122
Dung lượng
2,32 MB
Nội dung
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 get to 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 > Configuration Manager menu option. The Standard toolbar is shown in Figure 10-1. Figure 10-1 You can find out what the toolbar buttons are for by letting the mouse cursor linger over a toolbar but- ton. 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. The ‘release’ configuration omits this information as it represents overhead that you wouldn’t want in a fully tested program. With the Professional or Enterprise versions of Visual C++ 2005, the compiler also optimizes the code when com- piling 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 omit- ting 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. The debug toolbar is shown in Figure 10-2. Figure 10-2 If you inspect the tooltips for the buttons on this toolbar, you 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 famil- iar with stepping through a program using the debugger, you explore more of the features with a pro- gram that has bugs. You can start the debugger by clicking the leftmost button on the Debug toolbar, by selecting the Debug > Start Debugging menu item, or by pressing F5. I suggest that use the toolbar for the example. The debugger has two primary modes of operation —it works through the code by single stepping (which is essentially executing one statement at a time), or runs to a particular point in the source code. The point in the source where the debugger is to stop is determined either by where you have placed the cursor or, more usefully, a designated stopping point called a breakpoint. Check out how you define breakpoints. Setting Breakpoints A breakpoint is a point in your program where the debugger automatically suspends execution. You can specify multiple breakpoints so that you can run your program, stopping at points of interest that you 570 Chapter 10 13_571974 ch10.qxp 1/20/06 11:46 PM Page 570 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 par- ticular 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 number 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 and you can remove a break- point by double-clicking the glyph. Figure 10-3 shows the Editor pane with a couple of breakpoints set for Ex4_05. Figure 10-3 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 10-3), 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 double-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 > Delete All Breakpoints menu item or by pressing Ctrl-Shift-F9. Note that this removed breakpoints from all files in the project, even if they’re not currently open in the Editor pane. You can also disable all breakpoints by selection the Debug > Disable All Breakpoints menu item. 571 Debugging Techniques 13_571974 ch10.qxp 1/20/06 11:46 PM Page 571 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 10-4. Figure 10-4 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 win- dow and selecting from the pop-up. As well as setting a breakpoint at a location other than the begin- ning 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 arrange that execution only breaks when the hit count, which is the number of the point 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 trace- point by right-clicking the line where you want the tracepoint to be set and selecting the Breakpoint > When Hit menu item from the pop-up. You’ll see the dialog window shown in Figure 10-5. 572 Chapter 10 13_571974 ch10.qxp 1/20/06 11:46 PM Page 572 Figure 10-5 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, shown in Figure 10-6. 573 Debugging Techniques 13_571974 ch10.qxp 1/20/06 11:46 PM Page 573 Figure 10-6 1. The Start Debugging option (also available from a button on the Debug toolbar) simply exe- cutes a program up to the first breakpoint (if any) where execution will halt. After you’ve exam- ined 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 debug- ging 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 Start With Application Verifier option is for run-time verification of native C++ code. The Application Verifier is an advanced tool for identifying errors due to incorrect handle and criti- cal section usage and corruption of the heap. I won’t be discussing this in detail in this book. 3. 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. 4. The Step Into menu item (also available as a button on the Debug toolbar) executes your pro- gram 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. 5. 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 run all the code used by functions that might be called within a statement such as stream operations without stopping. 574 Chapter 10 13_571974 ch10.qxp 1/20/06 11:46 PM Page 574 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, so 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++ 2005 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 can be customized. The complete list of windows is shown on the Debug | Windows menu drop-down. The Autos window on the left shows current values for automatic variables in the context of the function that is currently executing. The Call Stack window on the right identifies the function calls currently in progress but the Output tab in the same window is probably more interesting in this example. In the Editor pane, you’ll see that the opening brace of your main() function is highlighted by an arrow to indicate that this is the current point in the program’s execution. This is shown in Figure 10-7. Figure 10-7 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 breakpoint. 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. 575 Debugging Techniques 13_571974 ch10.qxp 1/20/06 11:46 PM Page 575 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 declara- tion 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 10-8 (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 occasions, of course, because it contains whatever value was left by the last program to use these particular four bytes of memory. Figure 10-8 The Autos window has five tabs, including the Autos tab that is currently displayed, and the informa- tion they show is as follows: ❑ 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. 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 > Windows > Watch menu item. 576 Chapter 10 13_571974 ch10.qxp 1/20/06 11:46 PM Page 576 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 10-9. Figure 10-9 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++ 2005. 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 win- dow, 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 The Watch windows also allow you to change the values of the variables you are watching. 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 use- ful 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 577 Debugging Techniques 13_571974 ch10.qxp 1/20/06 11:46 PM Page 577 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 we have seen in previous chapters in debug mode. It enables 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 work- ing, and you won’t want to carry the overhead of executing it or the inconvenience of seeing all the out- put 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 func- tion is declared as: void assert(int expression); The argument to the function specifies the condition to be checked, but the effect of the assert() func- tion is suppressed if a special preprocessor symbol, NDEBUG, is defined. The symbol NDEBUG is automati- cally 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. To be effective, you must place the #define directive for NDEBUG preceding the #include directive for the <cassert> header in the source file: 578 Chapter 10 13_571974 ch10.qxp 1/20/06 11:46 PM Page 578 #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 != 0); assert(pAddStr != 0); // Code to append pAddStr to pStr } Calling the append() function with a null pointer argument in a simple program produced the follow- ing diagnostic message on my machine: Assertion failed: pStr != 0, file c:\beginning visual c++.net\examples\testassert\ testassert \ testassert.cpp, line 11 The assertion also displays a message box offering you the three options shown in Figure 10-10. Figure 10-10 Clicking the Abort button ends the program immediately. The Retry button starts the Visual C++ 2005 debugger so you can step through the program to find out more about why the assertion failed. In prin- ciple, 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. 579 Debugging Techniques 13_571974 ch10.qxp 1/20/06 11:46 PM Page 579 [...]... 45 6D 69 {141} normal block at 0x00355E90, Data: 45 6D 69 6C 15 CD 15 6C 12 79 bytes CD CD bytes 79 20 bytes 20 4D long CD CD CD CD CD CD CD CD CD long 53 74 65 69 6E 62 65 63 6B long 69 6C 6C 65 72 and ends with: {120} normal block at 0x003559D8, 8 bytes long Data: 44 69 63 6B 65 6E 73 00 {119} normal block at 0x003559A0, 8 bytes long Data: 43 68 61 72 6C 65 ... 0x003559A0, 8 bytes long Data: 43 68 61 72 6C 65 73 00 {118} normal block at 0x00355 968 , 11 bytes long Data: 49 76 6F 72 20 48 6F 72 74 6F 6E {117} normal block at 0x00355930, 7 bytes long Data: 48 6F 72 74 6F 6E 00 {1 16} normal block at 0x003558F8, 5 bytes long Data: 49 76 6F 72 00 Object dump complete 599 Chapter 10 The objects reported as being left in the free... delete[] pName; 60 1 Chapter 10 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 as you do for a native C++ code You have... 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++ 2005 Debugging Dynamic Memor y 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... 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++ 2005 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... 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 Press any key to continue 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... 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... 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 Autos window in Figure 10- 16 Figure 10- 16 588 Debugging Techniques 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... 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 60 3 Chapter 10 You can also write output using the Debug::Print() function that comes in two overloaded versions: Function Description... #ifdef FUNCTION_TRACE // Trace function calls cout . from the options on the Debug menu, shown in Figure 10 -6. 573 Debugging Techniques 13_571974 ch10.qxp 1/20/ 06 11: 46 PM Page 573 Figure 10 -6 1. The Start Debugging option (also available from. of searching. You can always add more debug code if you get really stuck. 5 86 Chapter 10 13_571974 ch10.qxp 1/20/ 06 11: 46 PM Page 5 86 Change the statement in the Editor window to what it should be and. pointed to by this. You can see this in the Autos window in Figure 10- 16. Figure 10- 16 588 Chapter 10 13_571974 ch10.qxp 1/20/ 06 11: 46 PM Page 588 How can copying from the object to another array corrupt