Chapter 13

Debugging Your Programs, Part 2

In This Chapter

arrow Debugging a multifunction program

arrow Performing a unit test

arrow Using predefined preprocessor commands during debug

This chapter expands upon the debugging techniques introduced in Chapter 8 by showing you how to create debugging functions that allow you to navigate your errors more quickly.

C++ functions represent further opportunities both to excel and to screw up. On the downside are the errors that are possible only when your program is divided into multiple functions. However, dividing your programs into functions allows you to examine, test, and debug each function without regard to how the function is being used in the outside program. This allows you to create a much more solid program.

Debugging a Dys-Functional Program

To demonstrate how dividing a program into functions can make the result easier to read and maintain, I created a version of the SwitchCalculator program in which the calculator operation has been split off as a separate function (which it would have been in the first place if I had only known about functions back then). Unfortunately, I introduced an error during the process that didn’t show up until I performed some testing. I saved this error as CalculatorError1:

  // CalculatorError1 - the SwitchCalculator program
//                    but with a subtle error in it
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

// prototype declarations
int calculator(char cOperator, int nOper1, int nOper2);

int main(int nNumberofArgs, char* pszArgs[])
{
    // enter operand1 op operand2
    int  nOper1;
    int  nOper2;
    char cOperator;
    cout << "Enter 'value1 op value2' "
         << "where op is +, -, *, / or %:" << endl;
    cin >> nOper1 >> cOperator >> nOper2;

    // echo what the user entered followed by the
    // results of the operation
    cout << nOper1 << " "
         << cOperator << " "
         << nOper2 << " = "
         << calculator(cOperator, nOper1, nOper2)
         << endl;

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

// calculator -return the result of the cOperator
//             operation performed on nOper1 and nOper2
int calculator(char cOperator, int nOper1, int nOper2)
{
    int nResult = 0;
    switch (cOperator)
    {
        case '+':
            nResult = nOper1 + nOper2;
        case '-':
            nResult = nOper1 - nOper2;
            break;
        case '*':
        case 'x':
        case 'X':
            nResult = nOper1 * nOper2;
            break;
        case '/':
            nResult = nOper1 / nOper2;
            break;
        case '%':
            nResult = nOper1 % nOper2;
            break;
        default:
            // didn't understand the operator
            cout << " is not understood";
    }
    return nResult;
}

The beginning of this program starts the same as its SwitchCalculator precursor except for the addition of the prototype declaration for the newly created calculator() function. Notice how much cleaner main() is here: It prompts the user for input and then echoes the output along with the results from calculator(). Very clean.

The calculator() function is also simpler than before; all it does is perform the computation specified by cOperator. Gone is the irrelevant code that prompts the user for input and displays the results.

All that’s left to do is test the results.

Performing unit level testing

Breaking a program down into functions not only allows you to write your program in pieces, but also it allows you to test each function in your program separately. In this function version of the SwitchCalculator program, I need to test the calculator() function by providing all possible inputs (both legal and illegal) to the function.

First, I generate a set of test cases for calculator(). Clearly, I need a test for each case in the switch statement. I will also need some boundary conditions, such as “how does the function respond when asked to divide by zero?” Table 13-1 outlines some of the cases I need to test.

1301

It turns out that I’m lucky in this case — the calling function main() allows me to provide any input to the function that I want. I can send each of these test cases to calculator() without modifying the program. That isn’t usually the case — very often the function is only invoked from the main program in certain ways. In such cases, I must write a special test module that puts the function I’m testing through its paces by passing the various test cases to it and recording the results.

warning.eps Why do you need to write extra debug code? What do you care if the function doesn’t handle a case properly if that case never occurs in the program? You care because you don’t know how the function will be used in the future. Once written, a function tends to take on a life of its own beyond the program that it was written for. A useful function might be used in dozens of different programs that invoke the function in all sorts of different ways that you may not have thought of when you first wrote the function. In addition, such bugs are often exploited by hackers.

The following shows the results for the first test case:

  Enter 'value1 op value2'
where op is +, -, *, / or %:
10 + 20
10 + 20 = -10
Press Enter to continue …

Already something seems to be wrong. What now?

Outfitting a function for testing

Like most functions, calculator() doesn’t perform any I/O of its own. This makes it impossible to know for sure what’s going on within the function. I addressed this problem in Chapter 8 by adding output statements in key places within the program. Of course, in Chapter 8, you didn’t know about functions, but now you do.

It turns out that it’s easier to create an error function that prints out everything you might want to know. You can then just copy and paste calls to this test function in key spots. This approach is quicker and less error-prone than making up a unique output statement for each different location.

C++ provides some help in creating and calling such debug functions. The preprocessor defines several special symbols shown in Table 13-2.

Table 13-2 Predefined Symbols Useful in Creating Debug Functions

Symbol

Type

Value

__LINE__

int

The line number within the current source-code module

__FILE__

const char*

The name of the current module

__DATE__

const char*

The date that the module was compiled (not the current date)

__TIME__

const char*

The time that the module was compiled (not the current time)

__func__

const char*

The name of the current function

__FUNCTION__

const char*

The name of the current function (GCC only)

__PRETTY_FUNCTION__

const char*

The extended name of the current function (GCC only)

technicalstuff.eps You haven’t yet seen the type const char* (which makes its debut in Chapter 16). For now, take my word that this is the type of a character string contained in double quotes for example, "Stephen Davis is a great guy" — used in the upcoming code.

You can see how the predefined preprocessor commands from Table 13-2 are used in the following version of the calculator() function outfitted with calls to a newly created debugger function printErr(). The following code segment is taken from the program CalculatorError2, which is in the online material:

  void printErr(int nLN, char cOperator, int nOp1, int nOp2)
{
    cout << "On line " << nLN
         << ": '" << cOperator
         << "' operand 1 = " << nOp1
         << " and operand 2 = " << nOp2
         << endl;
}

// calculator -return the result of the cOperator
//             operation performed on nOper1 and nOper2
int calculator(char cOperator, int nOper1, int nOper2)
{
    printErr(__LINE__, cOperator, nOper1, nOper2);
    int nResult = 0;
    switch (cOperator)
    {
        case '+':
            printErr(__LINE__, cOperator, nOper1, nOper2);
            nResult = nOper1 + nOper2;
        case '-':
            printErr(__LINE__, cOperator, nOper1, nOper2);
            nResult = nOper1 - nOper2;
            break;
        case '*':
        case 'x':
        case 'X':
            printErr(__LINE__, cOperator, nOper1, nOper2);
            nResult = nOper1 * nOper2;
            break;
        case '/':
            printErr(__LINE__, cOperator, nOper1, nOper2);
            nResult = nOper1 / nOper2;
            break;
        case '%':
            printErr(__LINE__, cOperator, nOper1, nOper2);
            nResult = nOper1 % nOper2;
            break;
        default:
            // didn't understand the operator
            cout << " is not understood";
    }
    return nResult;
}

The printErr() function displays the value of the operator and the two operands. It also displays the line number that it was called from. The line number is provided by the C++ preprocessor in the form of the __LINE__ symbol. Printing the line number with the error messages tells me how to differentiate the debug output from the program’s normal output.

You can see how this works in practice by examining the output from this newly outfitted version of the program:

  Enter 'value1 op value2'
where op is +, -, *, / or %:
10 + 20
On line 50: '+' operand 1 = 10 and operand 2 = 20
On line 55: '+' operand 1 = 10 and operand 2 = 20
On line 58: '+' operand 1 = 10 and operand 2 = 20
10 + 20 = -10
Press any key to continue …

Figure 13-1 shows the display of the program within the Code::Blocks editor, including the line numbers along the left side of the display.

9781118823873-fg1301.tif

Figure 13-1: The view of the calculator () function in the Code::Blocks editor showing the line numbers.

Immediately after I input “10 + 20” followed by the Enter key, the program calls the printErr() function from line 50. That’s correct because this is the first line of the function. Checking the values, you can see that the input appears to be correct: cOperator is ‘+’, nOper1 is 10, and nOper2 is 20 just as you expect.

The next call to printErr() occurred from line 55, which is the first line of the addition case, again just as expected. The values haven’t changed, so everything seems okay.

The next line is completely unexpected. For some reason, printErr() is being called from line 58. This is the first line of the subtraction case. For some reason, control is falling through from the addition case directly into the subtraction case.

And then I see it! The break statement is missing at the end of the addition case. The program is calculating the sum correctly but then falling through into the next case and overwriting that value with the difference.

First, I add the missing break statement. I do not remove the calls to printErr()— there may be other bugs in the function, and I’ll just end up putting them back. There’s no point in removing these calls until I am convinced that the function is working properly.

Returning to unit test

The updated program generates the following output for the addition test case:

  Enter 'value1 op value2'
where op is +, -, *, / or %:
10 + 20
On line 49: '+' operand 1 = 10 and operand 2 = 20
On line 54: '+' operand 1 = 10 and operand 2 = 20
10 + 20 = 30
Press Enter to continue …

This matches the expected results from Table 13-1. Continuing through the test cases identified in this table, everything matches until I get to the case of 10 / 0 — to which I get the output shown in Figure 13-2. The output from the printErr() shows that the input is being read properly, but the program crashes soon after line 68.

9781118823873-fg1302.tif

Figure 13-2: The CalculatorError program terminates with a mysterious error message when I enter ‘10 / 0’.

It’s pretty clear that the program is, in fact, dying on line 69 when it performs division by zero. I need to add a test to intercept that case and tell the program not to perform the division if the value of nOper2 is zero.

Of course, this begs the question: What value should I return from the function if nOper2 is zero? The “Expected Result” case in Table 13-1 says that we don’t care what gets returned when dividing by zero as long as the program doesn’t crash. That being the case, I decide to return 0. However, I need to document this case in the comments to the function.

With that addition to the function, I start testing again from the top.

remember.eps You need to restart back at the beginning of your test cases each time you modify the function.

The function generates the expected results in every case. Now I can remove the printErr() functions. The completed calculator() function (included in the CalculatorError4 program in the online material) appears as follows:

  // calculator -return the result of the cOperator
//             operation performed on nOper1 and nOper2
//             (In the case of division by zero or if it
//             cannot understand the operator, the
//             function returns a zero.)
int calculator(char cOperator, int nOper1, int nOper2)
{
    int nResult = 0;
    switch (cOperator)
    {
        case '+':
            nResult = nOper1 + nOper2;
            break;
        case '-':
            nResult = nOper1 - nOper2;
            break;
        case '*':
        case 'x':
        case 'X':
            nResult = nOper1 * nOper2;
            break;
        case '/':
            if (nOper2 != 0)
            {
                nResult = nOper1 / nOper2;
            }
            break;
        case '%':
            nResult = nOper1 % nOper2;
            break;
        default:
            // didn't understand the operator
            cout << " is not understood";
    }
    return nResult;
}

This version of the calculator() function does not suffer from the error that made the original version incapable of adding properly.Also, this updated version includes a test in the division case: If nOper2, the divisor, is zero, the function does not perform a division that would cause the program to crash but leaves the value of nResult unchanged, as its initial value of 0.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.128.145