Debugging in C++

Debugging is the process of finding logic errors in your program. A logic error is an error that the compiler will not find (an error that the compiler will find is a syntax error). For example, consider the following program:

#include <iostream.h>

void main ()
{
    if (5 < 7)
        cout << "5 is greater than 7" << endl;
    else
        cout << "5 is less than 7" << endl;
}

This program will compile, and when run will print out the string "5 is greater than 7". Obviously this is incorrect, and is an example of a logic error.  The compiler cannot help you find logic errors.

How to Debug

There are several ways to debug a C++ program. Here's a list, and we'll talk about each of them:

    1. Check the program logic by hand
    2. Insert cout statements in the program
    3. Use a symbolic debugger

Check the program by hand:

This is the quickest way to look for logic errors, and should be the first thing you try. This can range from just looking at your source code to actually stepping through the source code by hand.

To step through the source code by hand, you take a printout of your program's source code, and a pad of paper. You write down the initial values of the variables, and then step through your program one line at a time, making changes to the variables as indicated by the source code. Often, when you see what actually happens when your program runs, you'll realize that you did something silly to cause the bug.

Insert cout statements in the program:

You can also insert cout statements in the program to see what the values of variables are and to see how far the program gets. This method is particularly good for finding infinite loops.

When using cout statements for debugging, you must make sure to place "<< endl" at the end of every cout statement. That forces the screen display to be updated. If you don't do that, but use "<< '\n'" instead, you may have lines of output that don't appear on the screen. This is particularly important when you're tracking down a program crash.

Use a symbolic debugger:

A symbolic debugger is a program that allows you to run your program and stop it at any line. You can examine the contents of variables, and then tell the program to continue running. A symbolic debugger is particularly useful for tracking down program crashes, as many debuggers will show you the line of the source code that caused the crash.

See below for a discussion of how to use the symbolic debugger on Franklin's Unix system.

Types of Logic Errors

There are many types of logic errors. Here are some hints as to what a particular problem might be:

The program crashes (in Unix you'll get a message saying "Segmentation Fault", or maybe "coredump"). A program crash means you overwrote memory that doesn't belong to you, or tried to execute memory that wasn't meant to execute. Common causes include writing past the end of an array, writing before the beginning of an array, copying too many characters into a character array, trying to delete memory that either wasn't allocated or has already been deleted, trying to dereference a pointer that doesn't point anywhere, etc.

The program hangs (forcing you to use Control-C to stop it). This is most often caused by an infinite loop. Infinite loops are not always obvious, but some common causes include having the wrong terminating condition in a loop, having a series of function calls that becomes recursive (function A calls function B calls function C calls function A). If you're using cin to read into an integer variable, and the user types in a non-integer, you can experience symptoms similar to an infinite loop.

You get output that's different from what you expect. No common causes here. Your best bet is to start with looking at the source code to see if there are obvious problems, then put in some cout statements to see if the variables have the values you'd expect, and finally use a symbolic debugger to step through line by line.

Using a Symbolic Debugger

Before you can use a symbolic debugger, you must compile your program with the debug flag. This tells the compiler to insert debugging information the debugger will use. On Franklin's system, you do that by using the -g flag when compiling your program. So if your source file is named main.cpp, you would compile it like this:

g++ -g main.cpp

Once you have a program compiled with the -g flag, you can use gdb, the debugger available on Franklin's system. You start it by passing it the name of the program to run:

gdb a.out

gdb will print out some copyright information and then leave you at the gdb prompt:

(gdb)

At this prompt you can use various gdb commands.

Breakpoints: Breakpoints tell gdb to stop executing your program when it gets to a certain line of code or a certain function. For example, to tell gdb to stop at line 15 in your program, you would type in (what you type is in italics):

(gdb) break 15

To stop at a function called calcTime, you would type:

(gdb) break calcTime

Running: Running a program can take several forms. The easiest is to just tell gdb to run your program:

(gdb) run

This starts executing your program, and will not stop until your program ends, crashes, or a breakpoint is hit.

Once you've hit a breakpoint, you may want to execute single lines of your program. There are two primary ways of doing this. One way is to execute a single line of the program and move to the next sequentially numbered line. If the current line is a function call, this will execute the function and then place you on the next line of code. You do this by typing:

(gdb) next

The other way is if you want to step into function calls. You do this by typing:

(gdb) step

Step will, if the current line is a function call, place you on the first line of that function.

To continue running a program once a breakpoint has been hit, type:

(gdb) cont

This will run the program until the next breakpoint is hit, the program ends, or the program crashes.

Examining Variables: Once you've hit a breakpoint, you probably will want to see what values your variables contain. You do this by typing (assuming that there is a variable x we want to examine):

(gdb) print x

Examining the Call Stack: The call stack contains the list of function calls made to get to where you're at in the program. Examining the stack is most often useful when you're tracking down a coredump. Often the problem isn't with the line where the coredump happens, but in the calling function.

To examine the call stack, type:

(gdb) bt

To examine variables in the calling function, type:

(gdb) up

And then use the print command normally. To get back to where you were at, type:

(gdb) down

If you want to quit the debugger, you type:

(gdb) quit

Getting Help

gdb has many more options than discussed here. You can use the help command like this:

(gdb) help

This will give you a listing of subjects you can get help on.

Using gdb to diagnose program crashes

The following technique should be used anytime you have a program which 'crashes'.   Do not ask for help with a program crash until you have done the following!

  1. Compile the program with -g so you can use gdb
  2. Start gdb with the name of your program (i.e. gdb a.out)
  3. At the (gdb) prompt, type run and hit Enter.
  4. When your program crashes, gdb will tell you the line of code that had a problem.   Often, this line of code will be inside the C++ standard library.  The problem is not in the C++ standard library, it is in your code; most likely you are passing something bad to the standard library.
  5. At the (gdb) prompt, type bt to get a list of function calls made before the crash.  The top of the list should be the line where the program crashed.  The next line is where that function was called from, and so on down the list.  Find the first line of the stack trace that is inside code you wrote.
  6. At the (gdb) prompt, type up and hit Enter.  This will take you to the line of code that called the function that crashed.  Continue typing up and hitting Enter until you get to code you wrote. 
  7. This is, probably, where the problem is.  Use the print command to look at the contents of variables you are passing to the function that crashed.  If you are in a class, use print this to make certain that the class is not null.