Chapter 13
In This Chapter
Debugging a multifunction program
Performing a unit test
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.
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.
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.
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.
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?
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) |
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.
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.
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.
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.
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.
3.133.128.145