Chapter 8
In This Chapter
Avoiding introducing errors needlessly
Creating test cases
Peeking into the inner workings of your program
Fixing and retesting your programs
You may have noticed that your programs often don’t work the first time you run them. In fact, I have seldom, if ever, written a nontrivial C++ program that didn’t have some type of error the first time I tried to execute it.
This leaves you with two alternatives: You can abandon a program that has an error, or you can find and fix the error. I assume that you want to take the latter approach. In this chapter, I first help you distinguish between types of errors and show you how to avoid errors in the first place. Then you get to find and eradicate two bugs that originally plagued the Conversion program in Chapter 3.
Two types of errors exist — those that C++ can catch on its own and those that the compiler can’t catch. Errors that C++ can catch are known as compile-time or build-time errors. Build-time errors are generally easier to fix because the compiler points you to the problem, if you can understand what the compiler’s telling you. Sometimes the description of the problem isn’t quite right (it’s easy to confuse a compiler), but you start to understand better how the compiler thinks as you gain experience.
Errors that C++ can’t catch don’t show up until you try to execute the program during the process known as unit testing. During unit testing, you execute your program with a series of different inputs, trying to find inputs that make it crash. (You don’t want your program to crash, of course, but it’s always better that you — rather than your user — find and correct these cases.)
The errors that you find by executing the program are known as run-time errors. Run-time errors are harder to find than build-time errors because you have no hint of what’s gone wrong except for whatever errant output the program might generate.
The output isn’t always so straightforward. For example, suppose that the program lost its way and began executing instructions that aren’t even part of the program you wrote. (That happens a lot more often than you might think.) An errant program is like a train that’s jumped the track — the program doesn’t stop executing until it hits something really big. For example, the CPU may just happen to execute a divide-by-zero operation — this generates an alarm that the operating system intercepts and uses as an excuse to terminate your program.
Not all run-time errors are quite so dramatic. Some errant programs stay on the tracks but generate the wrong output (almost universally known as “garbage output”). These are even harder to catch since the output may seem reasonable until you examine it closely.
In this chapter, you debug a program that has both a compile-time error and a run-time error — not the “jump off the track and start executing randomly” variety but more of the “generate garbage” kind.
The easiest and best way to fix errors is to avoid introducing them into your programs in the first place. Part of this is just a matter of experience, but adopting a clear and consistent programming style helps.
We humans have a limited amount of CPU power between our ears. We need to direct what CPU cycles we do have toward the act of creating a working program. We shouldn’t get distracted by things like indentation.
This makes it important that you be consistent in how you name your variables, where you place the opening and closing braces, how much you indent, and so on. This is called your coding style. Develop a style and stick to it. After a while, your coding style becomes second nature. You’ll find that you can code your programs in less time — and you can read the resulting programs with less effort — if your coding style is clear and consistent. This translates into fewer coding errors.
When you’re working on a program with several programmers, it’s just as important that you all use the same style to avoid a Tower of Babel effect with conflicting and confusing styles. Every project that I’ve ever worked on had a coding manual that articulated (sometimes in excruciating detail) exactly how an if statement was to be laid out, how far to indent for case, and whether to put a blank line after the break statements, to name just a few examples.
Fortunately, Code::Blocks can help. The Code::Blocks editor understands C++. It will automatically indent the proper number of spaces for you after an open brace, and it will outdent when you type in the closed brace to align statements properly.
There is more debate about the naming of variables than about how many angels would fit on the head of a pin. I use the following rules when naming variables:
I expand on these rules in chapters involving other types of C++ objects (such as functions in Chapter 11 and classes in Chapter 19).
My first version of the Conversion program appeared as follows (it appears online as ConversionError1):
//
// Conversion - Program to convert temperature from
// Celsius degrees into Fahrenheit:
// Fahrenheit = Celsius * (212 - 32)/100 + 32
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;
int main(int nNumberofArgs, char* pszArgs[])
{
// enter the temperature in Celsius
int nCelsius;
cout << "Enter the temperature in Celsius: ";
// convert Celsius into Fahrenheit values
int nFahrenheit;
nFahrenheit = 9/5 * nCelsius + 32;
// output the results (followed by a NewLine)
cout << "Fahrenheit value is: ";
cout << nFahrenheit << 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;
}
During the build step, I get my first indication that there’s a problem — Code::Blocks generates the following warning message:
In function 'int main(int char**)':
warning: 'nCelsius' is used uninitialized in this function
=== Build finished: 0 errors, 1 warnings ===
How bad can this be? After all, it’s just a warning, right? So I decide to push forward and execute the program anyway.
Sure enough, I get the following meaningless output without giving me a chance to enter the Celsius temperature:
Enter the temperature in Celsius:
Fahrenheit value is:110
Press Enter to continue …
Referring to the prompt, I can see that I have forgotten to input a value for nCelsius. The program proceeded forward calculating a Fahrenheit temperature based upon whatever garbage happened to be in nCelsius when it was declared.
Adding the following line immediately after the prompt gets rid of the warning and solves the first problem:
cin >> nCelsius;
Once all the warnings are gone, it’s time to start testing. Good testing requires an organized approach. First, you decide the test data that you’re going to use. Next, you determine what output you expect for each of the given test inputs. Then you run the program and compare the actual results with the expected results. What could be so hard?
Determining what test data to use is part engineering and part black art. The engineering part is that you want to select data such that every statement in your program gets executed at least once. That means every branch of every if statement and every case of every switch statement gets executed at least once.
This simple program has only one path and contains no branches.
The black art is looking at the program and determining where errors might lie in the calculation. For some reason, I just assume that every test should include the key values of 0 and 100 degrees Celsius. To that, I will add one negative value and one value in the middle between 0 and 100. Before I start, I use a handy-dandy conversion program to look up the equivalent temperature in Fahrenheit, as shown in Table 8-1.
Table 8-1 Test Data for the Conversion Program
Input Celsius |
Resulting Fahrenheit |
0 |
32 |
100 |
212 |
-40 |
-40 |
50 |
122 |
Running the tests is simply a matter of executing the program and supplying the input values from Table 8-1. The first case generates the following results:
Enter the temperature in Celsius: 0
Fahrenheit value is: 32
Press Enter to continue …
So far, so good. The second data case generates the following output:
Enter the temperature in Celsius: 100
Fahrenheit value is: 132
Press Enter to continue …
This doesn’t match the expected value. Houston, we have a problem.
What could be wrong? I check over the calculations and everything looks fine. I need to get a peek at what’s going on in the calculation. A way to get at the internals of your program is to add output statements. I want to print the values going into each of the calculations. I also need to see the intermediate results. To do so, I break the calculation into its parts that I can print.
This version of the program is available online as ConversionError2.
This version of the program includes the following changes:
// nFahrenheit = 9/5 * nCelsius + 32;
cout << "nCelsius = " << nCelsius << endl;
int nFactor = 9 / 5;
cout << "nFactor = " << nFactor << endl;
int nIntermediate = nFactor * nCelsius;
cout << "nIntermediate = " << nIntermediate << endl;
nFahrenheit = nIntermediate + 32;
cout << "nFahrenheit = " << nFahrenheit << endl;
I display the value of nCelsius to make sure that it got read properly from the user input. Next, I try to display the intermediate results of the conversion calculation in the same order that C++ will. First to go is the calculation 9 / 5, which I save into a variable I name nFactor (the name isn’t important). This value is multiplied by nCelsius, the results of which I save into nIntermediate. Finally, this value will get added to 32 to generate the result, which is stored into nFahrenheit.
By displaying each of these intermediate values, I can see what’s going on in my calculation. Repeating the error case, I get the following results:
Enter the temperature in Celsius: 100
nCelsius = 100
nFactor = 1
nIntermediate = 100
nFahrenheit = 132
Fahrenheit value is: 132
Press Enter to continue …
Right away I see a problem: nFactor is equal to 1 and not 9 / 5. Then the problem occurs to me; integer division rounds down to the nearest integer value. Integer 9 divided by integer 5 is 1.
I can avoid this problem by performing the multiply operation before the divide operation. There will still be a small amount of integer round-off, but it will only amount to a single degree.
The resulting formula appears as follows:
nFahrenheit = nCelsius * 9/5 + 32;
Now, when I rerun the tests, I get the following:
Enter the temperature in Celsius: 0
Fahrenheit value is: 32
Press Enter to continue …
Enter the temperature in Celsius: 100
Fahrenheit value is: 212
Press Enter to continue …
Enter the temperature in Celsius: -40
Fahrenheit value is: -40
Press Enter to continue …
Enter the temperature in Celsius: 50
Fahrenheit value is: 122
Press Enter to continue …
This matches the expected values from Table 8-1.
3.145.35.247