1. Trang chủ
  2. » Công Nghệ Thông Tin

Beginning Visual C plus plus phần 6 doc

123 169 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 123
Dung lượng 2,52 MB

Nội dung

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 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com #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 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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 consider- ably more assistance than that to detect and fix bugs. Tale a look at how you can add diagnostic code of a more general nature. Adding Your Own Debugging Code Using preprocessor directives, you can arrange to 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 sym- bol, _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 the symbol, as follows: #ifdef _DEBUG // Code for debugging purposes #endif // _DEBUG The code between the #ifdef and the #endif is only 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 if some of your debug code produced voluminous output, so 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 over- head 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. You can arrange that these symbols are defined only if _DEBUG is defined: #ifdef _DEBUG #define MYDEBUG #define VOLUMEDEBUG #endif 580 Chapter 10 13_571974 ch10.qxp 1/20/06 11:46 PM Page 580 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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. Try It Out Adding Code for Debugging To explore these and some general debugging approaches, take an example of a program that, while simple, still contains quite a few bugs that you can find and eliminate. Thus you must regard all the code in the remainder of this chapter as suspect, particularly because it will not necessarily reflect good pro- gramming practice. For experimenting with debugging operations, start by defining a class that represents a person’s name and then proceed to test it in action. There is a lot wrong with this code, so resist the temptation to fix the obviously erroneous code here; the idea is to exercise the debugging operations to find them. However, in practice a great many bugs are very evident as soon as you run a program. You don’t necessarily need the debugger or additional code to spot them. Create an empty Win32 console application, Ex10_01. Next, add a header file, Name.h, to which you’ll add the definition of the Name class. The class represents a name by two data members that are pointers to strings storing a person’s first and second names. If you want to be able to declare arrays of Name objects you must provide a default constructor in addition to any other constructors. You want to be able to compare Name objects, so you should include overloaded operators in the class to do this. You also want to be able to retrieve the complete name as a single string for convenience. You can add a definition of the Name class to the Name.h file as follows: // Name.h – Definition of the Name class #pragma once // Class defining a person’s name class Name { public: Name(); // Default constructor Name(const char* pFirst, const char* pSecond); // Constructor char* getName(char* pName) const; // Get the complete name size_t getNameLength() const; // Get the complete name length // Comparison operators for names bool operator<(const Name& name) const; bool operator==(const Name& name) const; bool operator>(const Name& name) const; private: char* pFirstname; char* pSurname; }; 581 Debugging Techniques 13_571974 ch10.qxp 1/20/06 11:46 PM Page 581 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com You can now add a Name.cpp file to the project to hold the definitions for the member functions of Name. The constructor definitions are shown here: // Name.cpp – Implementation of the Name class #include “Name.h” // Name class definitions #include “DebugStuff.h” // Debugging code control #include <cstring> // For C-style string functions #include <cassert> // For assertions #include <iostream> using namespace std; // Default constructor Name::Name() { #ifdef CONSTRUCTOR_TRACE // Trace constructor calls cerr << “\nDefault Name constructor called.”; #endif pFirstname = pSurname = “\0”; } // Constructor Name::Name(const char* pFirst, const char* pSecond): pFirstname(pFirst), pSurname(pSecond) { // Verify that arguments are not null assert(pFirst != 0); assert(pSecond != 0); #ifdef CONSTRUCTOR_TRACE // Trace constructor calls cout << “\nName constructor called.”; #endif } Of course, you don’t particularly want to have Name objects that have null pointers as members, so the default constructor assigns empty strings for the names. You have used your own debug control symbol, CONSTRUCTOR_TRACE, to control output that traces constructor calls. Add the definition of this symbol to the DebugStuff.h header a little later. You could put anything at all as debug code here, such as dis- playing argument values, but it is usually best to keep it as simple as your debugging requirements allow; otherwise, your debug code may introduce further bugs. Here you just identify the constructor when it is called. You have two assertions in the constructor to check for null pointers being passed as arguments. You could have combined these into one, but by using a separate assertion for each argument, you can iden- tify which pointer is null (unless they both are, of course). You might also want to check that the strings are not empty in an application by counting the characters prior to the terminating ‘\0’ for instance. However, you should not use an assertion to flag this. This sort of thing could arise as a result of user input, so ordinary program checking code should be added to deal with errors that may arise in the normal course of events. It is important to recognize the difference between bugs (errors in the code) and error conditions that can be expected to arise during normal oper- ation of a program. The constructor should never be passed a null pointer, but a zero length name could 582 Chapter 10 13_571974 ch10.qxp 1/20/06 11:46 PM Page 582 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com easily arise under normal operating conditions (from keyboard input, for example). In this case it would probably be better if the code reading the names were to check for this before calling the Name class con- structor. You want errors that arise during normal use of a program to be handled within the release ver- sion of the code. The getName() function requires the caller to supply the address of an array that accommodate the name: // Return a complete name as a string containing first name, space, surname // The argument must be the address of a char array sufficient to hold the name char* Name::getName(char* pName) const { assert(pName != 0); // Verify non-null argument #ifdef FUNCTION_TRACE // Trace function calls cout << “\nName::getName() called.”; #endif strcpy(pName, pFirstname); // copy first name pName[strlen(pName)] = ‘ ‘; // Append a space // Append second name and return total return strcpy(pName+strlen(pName)+1, pSurname); } Here you have an assertion to check that the pointer argument passed is not null. Note that you have no way to check that the pointer is to an array with sufficient space to hold the entire name. You must rely on the calling function to do that. You also have debug code to trace when the function is called. Having a record of the complete sequence of calls up to the point where catastrophe strikes can sometimes pro- vide valuable insights as to why and how the problem arose. The getNameLength() member is a helper function that enables the user of a Name object to determine how much space must be allocated to accommodate a complete name: // Returns the total length of a name size_t Name::getNameLength() const { #ifdef FUNCTION_TRACE // Trace function calls cout << “\nName::getNameLength() called.”; #endif return strlen(pFirstname)+strlen(pSurname); } A function that intends to call getName() is able use the value returned by getNameLength() to deter- mine how much space is needed to accommodate a complete name. You also have trace code in this member function. In the interests of developing the class incrementally, you can omit the definitions for the overloaded comparison operators. Definitions are only required for member functions that you actually use in your program, and in your initial test program you keep it very simple. 583 Debugging Techniques 13_571974 ch10.qxp 1/20/06 11:46 PM Page 583 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com [...]... 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 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,... 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... destructor, a copy constructor, and the assignment operator The class should be declared as: class Name { public: Name(); Name(const char* pFirst, const char* pSecond); Name(const Name& rName); ~Name(); char* getName(char* pName) const; int getNameLength() const; // Default constructor // Constructor // Copy constructor // Destructor // Get the complete name // Get the complete name length // Comparison... http://www.simpopdf.com Simpo PDF 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... memory leaks can be detected The functions declared in ctrdbg.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... list of the objects in the free store at the end of the program The output generated by the free store debug facility starts with: Detected memory leaks! Dumping objects -> {143} normal block at 0x00355F08, Data: < > CD CD CD {142} normal block at 0x00355EC8, Data: 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...Chapter 10 You can define the preprocessor symbols control whether or not the debug code is executed in the DebugStuff.h header: Simpo PDF // DebugStuff.h - Debugging control Merge and Split Unregistered Version - http://www.simpopdf.com #pragma once #ifdef _DEBUG #define CONSTRUCTOR_TRACE #define FUNCTION_TRACE // Output constructor call trace // Trace function calls #endif Your control symbols... http://www.simpopdf.com #ifdef CONSTRUCTOR_TRACE // Trace constructor calls cout TraceError) Debug::WriteLine(L”FunC error ”); Debug::Assert(value < 4); Trace::WriteLine(L”Ending FunC”); Trace::Unindent(); } private: int value; static TraceSwitch^ sw = gcnew TraceSwitch(L”Trace Switch”, L”Controls trace output”); }; int main(array ^args) { // Direct output to the command... looks Figure 10-12 Because the context is a function that is a member of the Name class, the Autos window displays the this pointer that contains the address of the current object The pSurname pointer contains a weird address, 0xcccccccc, that corresponds to 34359738 36 in decimal! Because I have rather less than 3 billion bytes of memory, it looks a bit unlikely, and the debugger recognizes that pSurname . Name class #pragma once // Class defining a person’s name class Name { public: Name(); // Default constructor Name(const char* pFirst, const char* pSecond); // Constructor char* getName(char*. assertions #include <iostream> using namespace std; // Default constructor Name::Name() { #ifdef CONSTRUCTOR_TRACE // Trace constructor calls cerr << “ Default Name constructor called.”; #endif pFirstname. 0); assert(pSecond != 0); #ifdef CONSTRUCTOR_TRACE // Trace constructor calls cout << “ Name constructor called.”; #endif } Of course, you don’t particularly want to have Name objects that have null

Ngày đăng: 12/08/2014, 10:21

TỪ KHÓA LIÊN QUAN