21.2 Using the Visual Studio debugger


A typical debugging situation you face is that your program crashes. Your first problem is to find the line in your code which precipitates the crash.

First of all, make sure to press F5 so as to run the program in the debugger with all the debugging tools turned on. (In order to run outside the debugger to see how fast it can run, you use Ctrl+F5. ) Breakpoints won't work, for instance, unless you are running a Debug build with the debugging tools turned on. Note that a simple Build Execute will not invoke all the debugging tools, even if you are running the Debug build. You can use the F5 key to invoke the Debug Run when you want to run your program and have it notice the breakpoints.

Finding the problem after a crash

When you are running the Debug version of your program and it crashes, the screen will sometimes “ though not always “ highlight the offending line that caused the crash, and you can fix your bug almost right away. But often the thing that causes your crash will be a small error that causes some complication later on, and then the 'crash line' your screen shows you may be some innocent-looking piece of code or, even less helpful, some assembly language from one of the Windows library functions.

In this case, the way to find the line of your code that caused the crash is to use the Call Stack window. This window will normally open automatically in Visual Studio.NET, and if you don't see it, look for its tab in the tab group at the bottom of the screen. [With Version 6.0, you may need to open the Call Stack window with View Debug Windows Call Stack .] The Calls Tack window will show a list of function calls, with the most recent call at the top, the previous call below that, and so on. As you work down the call stack list, you are working backwards through time. Often the first few calls in the call stack list will be mysteriously named functions that are internal to the MFC libraries. Scan down the list to find the first function name that you recognize as one of your own code's functions. Double-click on this name , and a window will open, highlighting the line of this function which set off the calls higher up in the stack. This is the line that is breaking your code.

Students never seem to remember this, so let's say it again in bold capitals!

WHEN YOUR PROGRAM CRASHES, OPEN THE CALL STACK WINDOW TO FIND THE LINE OF YOUR CODE THAT MADE IT CRASH.

If you place the cursor over any variable in this line, a little onscreen window will show the value of the variable. You can get a more comprehensive view of the variable values by using a Watch window.

Visual Studio pops up some Watch windows automatically for you when you are in the debugger. There are several kinds of variable Watch windows, and possibly the variable you want to look at isn't showing up. If the variable you want isn't in the Watch window, look for the Watch window that has a tab at the bottom saying Locals . And let's repeat that, when execution is paused in the debugger, placing the mouse over a variable in the code near the current execution point will pop up (after a second or two) a tiny label stating the current value of that variable.

You won't always see the values for the watch variables you're interested in. Which variables are accessible depends on which function you are currently inside. Note also that you sometimes need to mouse around a little in order to see the fields and subfields of some complex object. Clicking on a pointer (such as this ) will open up extra watch lines showing the values of the fields in the structure. You may need to click down through several levels before you find the variable you care about.

If you're lucky, you'll find an obviously bogus value in one of your variables. A bogus floating point number usually has an extremely large positive or negative exponent, so that it looks like, say, 1.234098009809809e-123 . A bogus pointer value will very often be the hexadecimal value 0000000c ; other times it may be cdcdcdcd . These bad pointer values are what happens to get put by default into an uninitialized pointer, and this will often be some noticeably regular pattern. Usually a good pointer will have a messy-looking value.

Having a bad pointer value is a very common kind of problem. This happens if there is some pointer in your program that you've forgotten to initialize. When an uninitialized pointer gets dereferenced by a call like pbadpointer- >x() , the program typically crashes inside an MFC library function, and looking at the call stack leads you to a line holding pbadpointer . A good pointer value will be a more random-looking hexadecimal number. Chaos is health.

Breakpoints

Once you find a bad value in one of your variables you need to figure out how the bad value got there. Think about where this variable gets set or gets changed and find that spot in the code. It's useful now to get the program to run to this spot and stop. You can do this by setting a breakpoint . You set a breakpoint by right-clicking on a line of your code and using the context menu that pops up. Or you can just press F9 to set a breakpoint. A line with a breakpoint will have a special mark at its left end.

It's worth knowing that if you want to stop at the very end of a function to examine what happened inside it, you can set a breakpoint at the function's closing bracket } .

Remember that you can only set breakpoints and debug if you are working with the Debug build and not the Release build; you switch between this with the Build Set Active Configuration... dialog. Also remember to use Build Debug Run , because if you simply use Build Execute , the program won't stop at the breakpoints.

Now you can run the program up to the breakpoint. This means that the execution stops right before executing the line with the breakpoint. Usually it's a good idea to set the breakpoint before the troublesome bits of code and step through the code. When you are in the debugger, there are three ways to run your program: you can step into , step over , or run to the next breakpoint . The Visual Studio shortcut keys for these are F11 , F10 , and F5 .

  • To step into with F11 means to execute one line of code and stop, then the next line, then the next and so on. If you step into a loop, you will find yourself repeating the loop over and over, and you'll lose patience. The step out control gets you out “ use the Visual Studio Shortcut key Shift+F10 .

  • If you use the step over with F10 option, you go a line at a time, but you don't go into loops and you also don't go into the code of subfunctions that you call. This moves you forward faster, although sometimes you'll end up stepping over the piece of code you need to test.

  • If you use the F5 run to the next breakpoint option then you simply run to the next breakpoint, wherever it is. If you happen to be stuck tracing inside a loop or a subfunction, you can set a breakpoint past the loop code and use F5 to run the debugger up to that breakpoint. Or you can step out. And, again, if you're interested in seeing what the end result of a function is, you can set a breakpoint at the function's closing bracket.

Getting familiar with a debugger takes some time, and of course the debugger interfaces change from release to release of the compilers. The basic things to understand are, as we just mentioned: call stack, watch variables, breakpoints, step into, step over, step out, and run.

TRACE statements

One other debugging technique that can be useful is a TRACE statement. TRACE uses exactly the same syntax as the C printf statement, but it dumps its output into the standard Debug output, which in Visual Studio will be one of the sheets in the little Output window at the bottom of your Visual Studio screen.

This is useful in a situation where some function gets called over and over, but only now and then does it encounter a bad value “ not bad enough to crash the program, but bad enough to cause a malfunction of some kind. You're interested in figuring out exactly what triggers the malfunction, and you don't want to set a breakpoint and keep having to restart the program over and over. If, for instance, you were interested in comparing a local variable distance to, say, the radius value inside some structure pointed to by pshooter , you might have a line like the following.

 TRACE('distance = %f, radius = %f \n", distance, pshooter->radius()); 

And then you'd press F5 to run your program in Debug mode, adjust the sizes of your program and the Visual Studio window so that you could see them both onscreen, and do things in your program window while watching the values scrolling by inside the Visual Studio window.

When you build the Release version, all the TRACE commands are turned off, so you don't necessarily need to remove them, though you might as well remove or comment them out when you're done using them.

Finding memory leaks

Another useful feature of the debugger is that a fair amount of good information is output to the Debug sheet of the Output window. In particular, if you have any memory leaks, then messages about these will appear when you terminate a program that you've been running inside the debugger. Normally, leak information will not tell you where the leak came from, so it's not so helpful. There is, however, a not-too-well-known trick you can use to make Visual Studio tell you the origin of any memory leak. The trick is to add the following line to your stdafx.h file. See the Pop Framework stdafx.h file for a comment with more information about this.

 #define new DEBUG_NEW 

Testing

It's a good idea to let your program run for a time and see if anything bad happens to it. The Player Autorun feature in Pop was designed with this in mind.

One point about testing to keep in mind is that you should test both the Release build and the Debug build after each new version of your program. The reason for this is that if you have broken the Release build, you need to find this out and fix it before developing any further.

Normally you are going to want to distribute the Release build of the program rather than the Debug build. Be sure and test the Release build as well as the Debug build. When the behaviour of your Release and Debug builds differs , this often means that you have some uninitialized variables inside your program. Why?

When you don't initialize a variable, whatever random bytes happened to be at that RAM location end up getting used for the correct data that you should have put there. If the bytes happen to be all zeroes, often things will run smoothly. But if your random crud happens to be there, the program will crash in an ugly way. A number that should be 0 will instead be something like 1.03 times ten to the 17th power. When you run a program over and over it may be that some uninitialized variable positions keep landing in a 'good' part of the RAM that happens to get wiped to all zeroes before each run. But when you switch from a Debug build to a Release build, the size of your program changes and it occupies a different 'footprint' on the RAM. So now maybe the unitialized variables are getting loaded with junk.

The catch with having a bug that only occurs in the Release build is that you can't pop up the debugger to see what's wrong! What you can sometimes do, however, is to save off the bad situation as a parameter file, start up the Debug build and load the misbehaving parameters, and now use the various Debugger windows to find the problem. If you come across a variable that has an odd value, this is where your trouble is coming from.

In order to find all the bugs in your program, you need to test it a lot. A testing trick that can be useful is to build a function into our program which randomizes all of our program's parameters upon request. This ' monkey on a typewriter' testing will often turn up bugs that might have escaped your notice.

In addition, you need to actually use your program a lot to find bugs “ or to find bad aspects of your user interface. If you create, say, a game program, you should be willing to spend an hour playing with your program to see if it works!

Coding defensively

A basic principle for avoiding bugs is to code defensively. Be paranoid and take into account that some of the parameters fed into a routine might have bad values. Never, for instance, write a line like the following without first checking that x is not zero.

 y = 1.0/x; 

One way to check that x is not zero is to use the assert macro, and insert a line like the following before any line that tries to divide by x .

 assert (x); 

The advantage of using assert is that it's easy to do; the disadvantage is that if an assert statement fails while the program is running, then the program terminates with an error message which will frighten and mystify the user, something like this.

 assertion failed in line 666 of trouble.cpp 

By default assert always carries out the check (although you can turn it off, see the documentation on it). MFC has a version of the macro called ASSERT that we use more commonly in the Pop Framework. ASSERT only carries out the check in the Debug build of your program. This is a good thing if you plan to do plenty of testing before release, as it may be that checking the ASSERT condition takes up valuable time. If you want an assertion check that is also made in the Release build, you can use the standard C macro assert .

In the Debug mode, if an ASSERT line fails, the program halts with an error message. In the Release mode, an ASSERT line is ignored.

To make a solidly usable program, we test thoroughly for ASSERT failures in the Debug mode, and find ways to code in some reasonable non-program-terminating fallback course of action to take when an ASSERT might otherwise be triggered. If you really do want to check for the condition even in the Release version you can use the assert macro, but this is generally a fairly user-unfriendly thing to do. You really do want to find and work around any bad conditions while still coding.

By way of illustration, let's look at how you might avoid problems with division by zero. As a programmer you should never ever divide by something before making sure that it's not zero. Whenever you see a division in your program, do something about it. When you divide by zero the program crashes. To make life a little more confusing, a Windows program that crashes after division by zero will often as not give you an error message saying 'Square root of a negative number.' You don't want your users to see that.

So if, say, x and y are real numbers and n is an integer, instead of writing a line like x = y/n; you might write a line like

 x = y/(n?n:1); 

( A?B:C is the 'compactified C' question mark and colon syntax for an if-then-else statement. If A is true the value is B , otherwise the value is C .)

If the number you are dividing by is a real number u , then we need to avoid dividing by something very close to zero as well as dividing by zero. The problem is that dividing by something very close to zero can also produce a floating point error. To give a nice smooth fallback strategy, we might write something like the following in place of x = y/u .

 #define SMALL_REAL 0.000001  if (fabs(x) < SMALL_REAL)      x = (x>=0.0)?(SMALL_REAL):(-SMALL_REAL);  y = 1/x; 

The double fabs(double v) is the floating point absolute value function. You should not accidentally use the int abs(int n) integer absolute value function instead, because calling this function will round off the argument to an integer; for instance, abs(1.5) is 1 and abs(0.9) is .

Another technique for writing more bug-proof code is that you can handle bad parameter values with the more sophisticated C++ approach known as exception-handling, which uses the try , catch , and throw commands. But we won't cover exception handling in this book.



Software Engineering and Computer Games
Software Engineering and Computer Games
ISBN: B00406LVDU
EAN: N/A
Year: 2002
Pages: 272

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net