Chapter 8

Debugging Your Programs, Part I

In This Chapter

arrow Avoiding introducing errors needlessly

arrow Creating test cases

arrow Peeking into the inner workings of your program

arrow 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.

Identifying Types of Errors

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.

tip.eps An errant program is like a derailed train in another way — once the program starts heading down the wrong path, it never jumps back onto the track.

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.

Avoiding Introducing Errors

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.

Coding with style

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.

tip.eps I recommend that as a beginner you mimic the style you see in this book. You can change it later when you’ve gained some experience of your own.

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.

tip.eps You can run the Source Code Formatter plug-in that comes with Code::Blocks. With the file you are working on open and the project active, select Plugins⇒ Source Code Formatter (AStyle). This will reformat the current file, using the standard indentation rules.

remember.eps C++ doesn’t care about indentation. All whitespace is the same to it. Indentation is there to make the resulting program easier to read and understand.

Establishing variable naming conventions

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:

  • The first letter is lowercase and indicates the type of the variable. n for int, c for char, b for bool. You’ll see others in later chapters. This is very helpful when you’re using the variable because you immediately know its type.
  • Names of variables are descriptive. I’ve made it a rule: No variables with vague names like x or y. I’m too old — I need something that I can recognize when I try to read my own program tomorrow or next week or next year.
  • Multiple word names use uppercase at the beginning of each word with no underscores between words. I save underscores for a particular application, which I describe in Chapter 12.

I expand on these rules in chapters involving other types of C++ objects (such as functions in Chapter 11 and classes in Chapter 19).

Finding the First Error with a Little Help

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;

tip.eps The moral to this story is Pay attention to warnings! A warning almost always indicates a problem in your program. You shouldn’t even start to test your programs until you get a clean build: no errors and no warnings. If that’s not possible, at least convince yourself that you understand the reason for every warning generated.

Finding the Run-Time Error

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?

Formulating test data

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.

tip.eps Having every statement execute at least once is called full statement coverage and is considered the minimum acceptable testing criteria. The chance of programming mistakes making it into the field is just too high if you don’t execute every statement at least once under test conditions.

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

Executing the test cases

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.

tip.eps The value of 132 degrees is not completely unreasonable. That’s why it’s important to decide what the expected results are before you start. Otherwise, reasonable but incorrect results can slip by undetected.

Seeing what’s going on in your program

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.

remember.eps Keep the original expression as a comment so you don’t forget where you came from.

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.

tip.eps Another solution would be to use decimal variables that can retain fractional values. You’ll see that solution in Chapter 14.

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.

warning.eps Notice that, after making the change, I started over from the beginning, supplying all four test cases — not just the values that didn’t work properly the first time. Any changes to the calculation invalidate all previous tests.

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

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