Ten Ways to Avoid Adding Bugs to Your Program - The Part of Tens - C++ For Dummies (2014)

C++ For Dummies (2014)

Part VI

The Part of Tens

image

image Visit www.dummies.com for great Dummies content online.

In this part…

· Avoiding bugs

· Preventing hacking

· Visit www.dummies.com for great Dummies content online.

Chapter 29

Ten Ways to Avoid Adding Bugs to Your Program

In This Chapter

arrow Enabling all warnings and error messages

arrow Using a clear and consistent coding style

arrow Limiting the visibility

arrow Adding comments to your code while you write it

arrow Single-stepping every path at least once

arrow Avoiding overloaded operators

arrow Heap handling

arrow Using exceptions to handle errors

arrow Declaring destructors to be virtual

arrow Avoiding multiple inheritance

In this chapter, I look at several ways to minimize errors, as well as ways to make debugging the errors that are introduced easier.

Enable All Warnings and Error Messages

The syntax of C++ allows for a lot of error checking. When the compiler encounters a construct that it can’t decipher, it has no choice but to generate an error message. Although the compiler attempts to sync back up with the next statement, it does not attempt to generate an executable program.

Disabling warning and error messages is a bit like unplugging the Check Engine light on your car dashboard because it bothers you: Ignoring the problem doesn’t make it go away. If your compiler has a Syntax Check from Hell mode, enable it.

Don’t start debugging your code until you remove or at least understand all warnings generated during compilation. Enabling all warning messages if you then ignore them does you no good. If you don’t understand the warning, look it up. What you don’t know will hurt you.

Adopt a Clear and Consistent Coding Style

Coding in a clear and consistent style not only enhances the readability of your program but also results in fewer coding mistakes. Remember, the less brain power you have to spend deciphering C++ syntax, the more you have left over for thinking about the logic of the program at hand. A good coding style enables you to do the following with ease:

· Differentiate class names, object names, and function names.

· Know something about the object based on its name.

· Differentiate preprocessor symbols from C++ symbols (that is, #defined objects should stand out).

· Identify blocks of C++ code at the same level (this is the result of consistent indentation).

In addition, you need to establish a standard module header that provides information about the functions or classes in the module, the author (presumably, that’s you), the date, the version of the compiler you’re using, and a modification history.

Finally, all programmers involved in a single project should use the same style. Trying to decipher a program with a patchwork of different coding styles is confusing.

image You can let Code::Blocks maintain your source code style for you. Select Settings⇒Editor and then select Source Formatter on the left. From the menu on the right, you can select your preferred source code style. Now select Pluggins⇒Source Code Formatter to completely reformat your modules.

Limit the Visibility

Limiting the visibility of class internals to the outside world is a cornerstone of object-oriented programming. The class is responsible for its own internals; the application is responsible for using the class to solve the problem at hand.

Specifically, limited visibility means that data members should not be accessible outside the class — that is, they should be marked as protected. (Another storage class, private, is not discussed in this book.) In addition, member functions that the application software does not need to know about should also be marked protected. Don't expose any more of the class internals than necessary.

A related rule is that public member functions should trust application code as little as possible. Any argument passed to a public member function should be treated as though it might cause bugs until it has been proven safe. A function such as the following is an accident waiting to happen:

class Array
{
public:
explicit Array(int s)
{
size = 0;
// new throws exception if memory not available
pData = new int[s];
size = s;
}
~Array()
{
delete[] pData;
size = 0;
pData = nullptr;
}
//either return or set the array data
int data(int index)
{
return pData[index];
}
int data(int index, int newValue)
{
int oldValue = pData[index];
pData[index] = newValue;
return oldValue;
}
protected:
int size;
int *pData;
};

The function data(int) allows the application software to read data out of Array. This function is too trusting; it assumes that the index provided is within the data range. What if the index is not? The function data(int, int) is even worse because it overwrites an unknown location.

What’s needed is a check to make sure that the index is in range. In the following, only the data(int) function is shown for brevity:

int data(unsigned int index)
{
if (index >= size)
{
throw Exception("Array index out of range");
}
return pData[index];
}

Now an out-of-range index will be caught by the check. (Making index unsigned precludes the necessity of adding a check for negative index values.)

Comment Your Code While You Write It

You can avoid errors if you comment your code while you write it rather than wait until everything works and then go back and add comments. I can understand not taking the time to write voluminous headers and function descriptions until later, but you always have time to add short comments while writing the code.

Short comments should be enlightening. If they’re not, they aren’t worth much. You need all the enlightenment you can get while you’re trying to make your program work. When you look at a piece of code you wrote a few days ago, comments that are short, descriptive, and to the point can make a dramatic contribution to helping you figure out exactly what it was you were trying to do.

In addition, consistent code indentation and naming conventions make the code easier to understand. It’s all very nice when the code is easy to read after you’re finished with it, but it’s just as important that the code be easy to read while you’re writing it. That’s when you need the help.

Single-Step Every Path at Least Once

It may seem like an obvious statement, but I'll say it anyway: As a programmer, it’s important for you to understand what your program is doing. Nothing gives you a better feel for what’s going on under the hood than single-stepping the program with a good debugger. (Code::Blocks contains an integrated debugger.)

Beyond that, as you write a program, you sometimes need raw material to figure out some bizarre behavior. Nothing gives you that material better than single-stepping new functions as they come into service.

Finally, when a function is finished and ready to be added to the program, every logical path needs to be traveled at least once. Bugs are much easier to find when the function is examined by itself rather than after it has been thrown into the pot with the rest of the functions — and your attention has gone on to new programming challenges.

Avoid Overloading Operators

Other than using the assignment operator operator=(), you should hold off overloading operators until you feel comfortable with C++. Overloading operators other than assignment is almost never necessary and can significantly add to your debugging woes as a new programmer. You can get the same effect by defining and using the proper public member functions instead.

After you’ve been C-plus-plussing for a few months, feel free to return and start overloading operators to your heart’s content.

Manage the Heap Systematically

As a general rule, programmers should allocate and release heap memory at the same “level.” If a member function MyClass::create() allocates a block of heap memory and returns it to the caller, there should be a member function MyClass::release() that returns the memory to the heap. Specifically, MyClass::create() should not require the parent function to release the memory. This certainly doesn’t avoid all memory problems — the parent function may forget to call MyClass::release() — but it does reduce the possibility somewhat.

Use Exceptions to Handle Errors

The exception mechanism in C++ is designed to handle errors conveniently and efficiently. In general, you should throw an error indicator rather than return an error flag. The resulting code is easier to write, read, and maintain. Besides, other programmers have come to expect it — you wouldn’t want to disappoint them, would you?

It is not necessary to throw an exception from a function that returns a “didn't work” indicator if this is a part of everyday life for that function. Consider a function lcd() that returns the least common denominators of a number passed to it as an argument. That function will not return any values when presented a prime number (a prime number cannot be evenly divided by any other number). This is not an error — the lcd() function has nothing to say when given a prime.

Declare Destructors Virtual

Don't forget to create a destructor for your class if the constructor allocates resources (such as heap memory) that need to be returned when the object reaches its demise. Having created a destructor, don't forget to declare it virtual (almost) every time especially if you know that your class is likely to be inherited and extended by subclasses. The problem is demonstrated in the following code snippet:

#include <iostream>

using namespace std;
class Person
{
public:
Person(const char* pszName)
{
psName = new string(pszName);
}
~Person()
{
delete psName; psName = nullptr;
}

protected:
string* psName;
};
class Student : public Person
{
public:
Student(const char* pszName, unsigned ID,
const char* pszMajor)
: Person(pszName), mID(ID)
{
psMajor = new string(pszMajor);
}
~Student()
{
delete psMajor; psMajor = nullptr;
}
protected:
unsigned mID;
string* psMajor;
};

void fn()
{
Person* p = new Student("Stew Dent", 1234, "Physics");
delete p;
}

The function fn() creates a Student object. The Student class extends a class Person that allocates heap memory to hold the person's name in the constructor and returns it in the destructor. In addition, the Student constructor allocates memory that it returns in its own destructor.

The problem occurs when fn() stores the returned pointer to a Student in a variable declared Person*. This is allowed because a Student IS_A Person (see Chapter 11 if this doesn't make sense). The problem is that the delete p invokes the Person destructor but not the Student destructor, resulting in a memory leak.

Making the following change to the class solves the problem:

class Person
{
public:
Person(char* pszName) { psName = new string(pszName);}
virtual ~Person() { delete psName; / nullptr; }

protected:
string* psName;
};

This causes C++ to invoke the destructor based on the pointer's actual type (in this case Student*) and not its declared type (Person*).

Okay, so when should I not declare the destructor virtual? Declaring any member in a class virtual means that C++ must add an extra pointer or two to each object of that class to keep track of its virtual members. Thus, if you were to declare a lot of Person objects, an extra few pointers per object might be a big deal.

As a general rule, if you expect someone to inherit your class then declare its destructor virtual. And if you are about to inherit from an existing base class, make sure that its destructor is declared virtual as well.

Avoid Multiple Inheritance

Multiple inheritance, like operator overloading, adds another level of complexity that you don’t need to deal with when you’re just starting out. Fortunately, most real-world relationships can be described with single inheritance.

After you feel comfortable with your level of understanding of C++, experiment with setting up some multiple inheritance hierarchies. That way, you’ll be ready when the unusual situation that requires multiple inheritance to describe it accurately arises.