Debugging


Debugging Objects With Turbo Debugger

Alex Lane


Alex Lane is the Senior Technical Writer in the Languages Business Unit at Borland International. This article was excerpted from Turbo C++ by Example (M&T Books, 1990) and has been reprinted with the permission of M&T Books (415/366-3600). Thanks to Mark Weaver for his help in coding the program.

The C++ language allows programmers to use object-oriented programming (OOP) techniques in program development. A programmer's tools, especially the debugger, must cope with object and class hierarchies and inspection of objects and classes. OOP aside, debugging technology itself must keep the programmer productive. This article demonstrates Turbo Debugger's capabilities in supporting OOP by debugging a sample C++ program.

About Turbo Debugger

Turbo Debugger is a source-level debugger designed for Borland language programmers, yet it can be used by programmers using other compiler products as well. Among other features, the current release (version 2.0) supports the expanded memory specification (EMS) for debugging large programs, assembler/CPU access when needed, support for 80386 and third-party debugging hardware, and TSR and device driver debugging. Turbo Debugger's features also include:

The Test Program

The sample program in this article roughly simulates an ant colony. In this souped-up version of Conway's "Game of Life," an ant not only appears, lives and dies, it also moves and behaves in specific ways, depending on what type of ant it is: Queen or Worker.

The rules of the anthill are: All ants have an energy store that is decremented in each cycle of the program. If the level falls to zero, the ant dies. All worker ants have a life span and an age that increments each cycle. The ant dies when its age exceeds its life span. When an ant dies, it disappears from the screen. The Queen does not age, but she can die of hunger. When all ants are dead, the simulation is over.

Workers feed the Queen by foraging for food and bringing it back to the Queen. The Queen is restricted to one corner of the screen and can only eat food and lay eggs. The eggs hatch into worker ants after a few cycles.

Listing 1 contains the header file for the program. Listing 2 shows the class definitions, supporting functions and main() function.

After watching the program run awhile, you'll notice a few annoying characteristics. First, from time to time, ants seem to stop dead in their tracks. Second, the count of worker ants at the bottom left of the screen never goes down. Third, if we run the program for a long time with a large amount of food on the screen, the queen "dies" (her energy goes to zero) for no apparent reason, even though moments before, she had plenty of energy.

Running Turbo Debugger

For the sake of brevity, I won't describe the exact keystrokes or mouse actions needed to perform various actions. (You can perform virtually all actions within the debugger by a set of keystrokes or a set of equivalent mouse actions. Function keys also provide shortcuts for some common actions, such as toggling breakpoints or tracing through code.)

The anthill program is packaged in one file for convenience, so finding parts of code in the 800-plus lines is relatively easy. Normally, source code is scattered among a number of files, and under such conditions, hunting for a specific routine is a challenge. Browsing through the class hierarchy can help.

The class is a basic object-oriented concept. You can think of a class in C++ as a template for a data type that has private, so-called "member" functions available to it. A class can inherit the data and member functions of another class, or from multiple classes (see Figure 1) . Classes in a hierarchy can share a common member function defined in some base class, or they can each define their own version of a function. When many classes have their own version of a member function, declaring the function as virtual assures that the appropriate class's function is executed in any given context. Letting objects invoke their own form of a function having a common name is polymorphism, and lends a great deal of flexibility to OOP.

Debugging The Anthill

First, let's tackle the problem with the queen dying for no apparent reason. By calling up the Class hierarchy window shown in Figure 1, we can examine relationships among classes in the program. The pane in the window's upper right-hand portion displays a hierarchy tree for all objects and classes in the program module. The tree shows that both Worker and Queen classes are derived from the more generic Ant class. Ant, in turn, is derived from both Mover and Consumer via multiple inheritance. If you move the highlight bar down to Ant, the lower right pane displays a reverse tree showing the parents of the highlighted class; in this case, Mover and Consumer are shown as parents of Ant. The left pane diplays a "flat" alphabetical list of all classes in the program module.

Selecting the Queen from the hierarchy opens a Class inspection window, shown in Figure 2. This window's upper pane shows all of the member data of the Queen class; the lower pane shows the member functions for the class. (To make the window less crowded, you can toggle a 'Show Inherited' flag to No, and you'll see only the member data and functions of the Queen class.) Selecting the line

void doTheAntThing ()
brings up a function inspection window, containing the starting address of this member function. Selecting again brings up another window, shown in Figure 3, containing the function's source code. The line we're interested in is visible in this window.

We set a breakpoint at the line

if (nEnergy<1)
so the breakpoint fires only when an expression we enter evaluates as true. Figure 4 shows a filled in dialog box, which will fire the breakpoint when the expression

nEnergy <= 0
is true. We then run the program, setting the queen's initial energy level to 20,000, and the amount of food on the screen to 1,000. After a minute or so, the breakpoint fires. We inspect the queen's energy level and find that it's negative (In our run, it was -31,744.) The solution becomes apparent. This looks like integer overflow (the variable nEnergy is probably an int, and if we look back to the Class Inspection window in Figure 2, we confirm that it is). The easiest way to fix this problem is to change the nEnergy variable to a long integer (unsigned integer won't do, because a freak addition might result in 0, again killing the queen). We choose, however, to fix the problem by modifying the queen's eating habits; if the Queen goes to eat and finds her energy is high, she won't eat. This characteristic is implemented as a member function to the Queen class (Listing 3) .

The remaining two problems might be related, because when a worker ant dies, the ant should disappear and the global count of workers should decrease. Since the count never decrements, maybe the ants aren't dying. If they are dying, why are they visible, and why doesn't the count change?

We set a breakpoint at the line

thisAnt->doTheAntThing ();
in the code that processes the Worker ant list in main(). We execute to that line and begin to trace through the code. Our first call is to the function Worker::doTheAntThing(), which immediately calls the function Ant::doTheAntThing(). Here, the ant is aged, and its energy is decremented. The ant is also checked to see if it has died of old age or starvation. If we accientally trace past the line that reads

if ( nEnergy < 1 || nAge >
     nLifeSpan )
we can undo our trace using reverse execution (Alt-F4). Reverse execution lets you move backward through code when an expected path is not taken, or when you want to cover all possible paths through a statement (for example, a switch statement).

Here, let's "kill" the ant by setting the energy level to zero using the Evaluate/modify dialog box shown in Figure 5. Now, the call to die() executes Ant::die(), which sets the bDead flag to TRUE, clears nEnergy, and erases the ant.

We then return to Worker::doTheAntThing(), which evaluates an if statement that, if true, executes moveTowardQueen(), and if false, executes lookForFood()! Using the hierarchy browser to find the code for these functions, we see that both functions move and redraw the ant! In other words, after the ant is dead and erased, another function draws it again. Once doTheAntThing() is finished, an if statement deletes the dead ant object back in main(). We can solve the problem of non-disappearing ants simply by moving the call to doTheAntThing() after the if statement. This way, the ant doesn't get to move again once it's dead.

Finally, we attack the problem of decrementing the global number of workers. Using Turbo Debugger's file browsing capability to look at ANTHILL.CPP, we search for all occurrences of die() and find three occurrences: the one in Ant::doTheAntThing(), which calls die(), another call from a Queen member function, and an explicit call from Worker::doTheAntThing() to Ant::die().

Notice that the call from Ant::doTheAntThing() merely calls die(), and yet the code executed was Ant::die(). Since the object originating the call is a Worker object, the call to die() should execute the Worker class's die() member function, not the Ant's. This call didn't happen because the die() member function is not declared virtual. Making die() virtual will result in die() calling the correct version of the function, in this case, Worker: :die().

Summary

After implementing the code changes we've identified, we rerun the program and note that the queen does not gorge herself, and that workers disappear quietly when their time comes. Our debugging session has been a success.

Turbo Debugger can help programmers achieve better productivity through its enabling features, such as informative window displays, breakpoints, reverse execution and an easy-to-use interface. Turbo Debugger makes debugging object-oriented programs easier by offering the user access to the program's different parts to examine the data and member functions of various classes.