next up previous
Next: About this document ...

CSCI.1200 - Computer Science II
Summer, 2002
Worksheet 8
Debugging your Programs

Programming large software projects

There are a number of differences between the kind of programming that students do for courses such as this and kind of programming that is done in the ``real world''.

Good Programming Practices

It is important to develop good programming habits right from the start. Here are some guidelines that you should use whenever you write software, either for a course or for a ``real world'' application.

Debugging your programs

As you start to write larger programs, the process of debugging (finding and fixing logic errors) becomes more complex. A traditional way of debugging a program involves getting more output (via cout statements) to determine just what exactly the program is doing and to inspect the values of variables. Using cout to print out values and notify you of where the program is running is still a good idea, but it is often faster and easier to use the debugger to track down your logic errors.

A debugger is a tool which lets you stop and resume program execution and examine the values stored in memory (like your variables). While it takes a while to learn how to use it, in the long run, it will save you a good deal of time.

Programming errors (bugs) come in a variety of flavors. This worksheet will consider two important categories:

  1. Bugs which produce repeatable run time errors (crashes1) -- These are the easiest to spot and fix. In fact, the MS visual C++ debugger can usually be invoked as soon as the program crashes (unless the bug is so severe it crashes MS Visual C++ or the Windows operating system).

  2. Bugs which cause incorrect output but do not crash the program -- These are often trickier to find.
Be sure to remember: a bug early in the program's run may not be noticed until much later in the program's execution.

Using the MS Visual C++ debugger

To run a debugger effectively, you should learn the following:

  1. How to start the debugger
  2. How to exit the debugger
  3. How to stop the debugger (halting the debugged program) at a specific place in the program by setting a breakpoint
  4. How to run the program within the debugger
  5. How to inspect (and update) values stored in memory locations (i.e., variables) using a watch on the variable
  6. How to examine the call stack

Starting the debugger

There are two ways of starting the MS Visual C++ debugger. One way is to run a program until it crashes. When this happens, a dialog window will come up giving you the option of terminating the run or starting the debugger. If you choose to debug, note that a new instance of Visual Studio is started. The program will be stopped where the bug was detected, not necessarily where the bug occurred. Once you see where the error occurred, you should immediately exit this second running copy of Visual Studio. (The debugger does strange things if you continue debugging in this second copy.)

Another way to run the debugger is to start it yourself, rather than waiting for the program to crash. Unless you have a program that runs a long time before crashing, this is the preferred method. Choose Start Debug from the Build Menu. A pop-up menu will appear with four choices:

Go                F5
Step Into         F11
Run to Cursor     Cntl+F10
Attach to Process ...
The Go command will start executing your program, and it will run to completion, until a memory exception error occurs, or until a breakpoint is reached. (Breakpoints are explained below.) The Step Into command starts executing your program but it will stop at the first executable line in main. This will allow you to step through your program one line at a time. The Run to Cursor command is like the Go command except that it will stop at the line where your cursor is in the source file.

Exiting The Debugger Once you have started the debugger, you can exit the debugger using the short cut shift+F5, or by selecting the exit option in the pull down menu structure.

Setting Breakpoints To tell the debugger that you want it to halt your program at a particular statement, you should set a breakpoint at that statement. The debugger will always halt your program at the breakpoint (even if you choose Go). To set a breakpoint, move the cursor to the line where you want to set the breakpoint and hit the F9 key. A red dot will appear in the left margin. (You can also click on the line with the right mouse button and a popup menu will appear. One of the choices is Insert Breakpoint). Now, when you choose Go from the debug menu, execution will stop whenever the line with the breakpoint is executed. To remove a breakpoint, move your cursor to the line and hit F9.

If you select the Run to Cursor option, the debugger will (temporarily) insert a breakpoint at the line where the cursor is in the source code and will run your program until it reaches that breakpoint. Once there the program will stop and the breakpoint will be automatically removed; thus, you will not stop there again. This can be useful if you want to start a program and set breakpoints to check the program before an error occurs (e.g., errors not triggering a crash).

Running The Program in the Debugger The Go command will either run your program from the beginning if the program is not halted or resume execution from the place where the program was halted. Execution continues until a breakpoint (if you have not set any breakpoint, it will run to completion or a memory exception error). You should NOT attempt to Go after your program has a memory exception; you should fix the problem first and restart the program.

The Step commands will run your program one statement at a time (the debugger automatically inserts a breakpoint after the current statement and removes it after stepping). Usually it is very easy to know what is meant by the next statement, but if the current statement is a function call you may either want to debug the function or just ``skip over'' it. Since this is so useful the debugger supports:

So, each time you step, your program will advance one statement and stop.

Try hitting F11 a few times with a program. Note that the yellow debugger arrow points to the current line, and note that a new window will appear at the bottom with the names of all of the currently active variables and their current values. Try hitting F11 a few times and notice what is happening. Pay particular attention to the values of the variables. (When the program is at a particular statement, that statement has not yet been executed, so the values of the variables are those prior to the execution of the statement.)

Stepping through a long program is pretty tedious; you can immediately go to a particular statement in your code by moving the cursor to that line and then hitting Run to cursor (Cntl+F10).

Alert: Avoid stepping into a cin, cout or new statement or a statement that calls a library function like strcpy; the debugger will not be able to find the code and will start displaying machine code, which is probably incomprehensible to you. You can recover from this by hitting shift+F11 to step out or if you can't figure out what the debugger is doing, you can quit debugging by hitting shift+F5. You can also advance to the next step by opening the window that displays your source code (use the window menu to open the window with your source code), moving the cursor to the line after the line that caused the problem and hitting Cntl+F10 (Run to cursor).

Setting Watch on a Variable

If you overwrite array bounds, you can inadvertently change the value of a variable, and such errors are often very difficult to find. Consider this program.

#include <iostream.h>
int main()
{
    int i, x, y[3];
    x = 17;
    for (i=0;i<=3;i++)
        y[i] = i * 100;
    cout << x << endl;
    return 0;
}

You would expect this program to print 17, but it prints 300.2 It contains an error which is easy to make and often difficult to find (you probably found the error immediately in this tiny program). One of the problems with such errors is that they can occur far from where they are detected. You can use the debugger to detect such errors by setting a Watch on a variable via the Quick Watch menu option (Shift+F9). A window will pop up, and you can enter your variable name in the top of the window. The variable name will appear in whichever watch tab is selected in the lower right corner. If you click on the little left arrow, the watch window will become full screen width and you will see both the variable name and the current value of that variable. If you like, it is possible (in the watch window) for you to have the debugger change the value of the variable (this is sometimes useful for debugging).

Viewing the Call Stack

The call stack is a list of all of the currently active functions in your program. For example, suppose you are running a program in which the function main() calls FctnOne() and FctnOne calls FctnTwo(), and FctnTwo calls FctnThree(). Each of these functions has its own arguments and its own variables. There are times when you want to view the value of variables of functions in the call stack. To do this, bring up the Call Stack window. There are several ways to do this. One way is to choose Debug Windows from the View menu. One of the options on this menu is called Call Stack. The keyboard shortcut to invoke the call stack window is Alt 7. Once you have the call stack window, you can examine the values of any of the variables by moving the cursor to the appropriate function and clicking the right mouse button. A pop-up menu appears. Choose Go to Code (it's the top choice).

This is particularly useful when your program crashes in a library function like strcat. When the program crashes, the assembler code for the library function appears on your window. This is normally of little use to you, but you want to see where in your code the problem occurred, so you work your way down the call stack until you come to code that you wrote.

Exercise 1: Copy the following code into a project and run it. You can find the code in /dept/cs/cs2/download/debugger.cpp

#include <iostream>
using namespace std;
int FctnOne();
void FctnTwo(char *);
int main()
{
    int x;
    x = FctnOne();
    cout << "x is " << x << endl;
    return 0;
}
int FctnOne()
{
    char *a = "Hello";
    FctnTwo(a);
    return 0;
}
void FctnTwo(char *s)
{
    char *t = " World";
    strcat(s,t);
    cout << s << endl;
}

Run it in the debugger until it crashes. The code for strcat() will pop up. Open the Call Stack window if it is not already open. it should look something like this.

strcat() line 169
FctnTwo(char * 0x0046c024 ??_C@_05DPEH@Hello?$AA@) line 21 + 13 bytes
FctnOne() line 15 + 9 bytes
main() line 8 + 5 bytes
mainCRTStartup() line 206 + 25 bytes
KERNEL32! 77f1ba06()
If you right click on the line FctnTwo(char... and choose Go To Code, the debugger will display the code for the function FctnTwo and the current values of all of the variables will be displayed at the bottom. Note that pointers values are displayed in hexidecimal, but you can tell if a pointer is NULL because its value will be 0x00000000. Uninitialized pointers often seem to have the value 0xcccccccc. You can display the value of each variable in each function at the time of the crash. This includes s and t in FctnTwo, a in FctnOne and x in main.

Alert: The quiz for this worksheet involves your demonstrating use of the debugger without looking at the worksheet. You should be able to do all of the following quickly and easily, using the appropriate accelerator keys rather than pulling down menus. If you cannot do these, you will fail the quiz.

You must memorize all of the following accelerator keys for the quiz.

Set a breakpoint F9
Run to a breakpoint or completion F5
Step, going into functions F11
Step, skipping over functions F10
Step out of a function Shift-F11
Stopping the debugger Shift-F5
Restarting the Program Cntl-Shift-F5
Open Call Stack Window Alt-7

The string class

Many students have had difficulty with the char * implementation of character strings. Students are apt to use the assignment operator = or the equals comparison operator == when they should use the functions strcpy() and strcmp(), and they do not allocate new memory or delete unneeded memory properly. There is a class string which avoids many of these problems. For the remainder of this course, you may use this class if you wish.

To use this class, you need to include the following statement
#include <string> You can then declare instances of the string class like this:
string s1;
or like this
string s2("This is a string");
Here are some member functions for this class.

void string::assign(char *s)
void string::append(char *s) // like strcat
void string::append(string s)// like strcat
int string::size() // like strlen
int string::length() // like strlen
char *string::data() // returns a pointer to the string value

The == operator works like strcmp() and the = operator works like strcpy()

There are numerous other member functions and operators as well.

Here is some sample code.

#include <string>
#include <iostream.h>
using namespace std;

int main()
{
    string s1;
	string s2("This is a string");
	s1.assign("The quick brown fox");
	s2.append(" with another string appended");
	cout << s1.data() << endl;  // prints The quick brown fox
	cout << s2.data() << endl;  
	    // prints this is a string with another string appended
	s1.assign("ABCDEFG");
	string s3("ABC");
	s3.append("DEFG");
	if (s1 == s3) cout << "TRUE";  // prints TRUE
	else cout << "FALSE";
	cout << endl;
	int n = s1.size();
	cout << "The length of s1 is " << n << endl;  // prints 7
	s1.append(s2);
	cout << s1.data() << endl;
	return 0;
}



next up previous
Next: About this document ...
Paul Lalli 2002-05-17