10 Ways to Avoid Bugs in C++ - dummies

By Stephen R. Davis

It’s an unfortunate fact that you will spend more time searching for and removing bugs than you will spend actually writing your C++ programs in the first place. The suggestions here may help you minimize the number of errors you introduce into your programs to make programming a more enjoyable experience.

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 just can’t decipher, it has no choice but to output a message. It tries to sync back up with the source code (sometimes less than successfully), but it will not generate an executable. This forces the programmer to fix all error messages.

However, when C++ comes across a structure that it can figure out but the structure smells fishy anyway, C++ generates a warning message. Because C++ is pretty sure that it understands what you want, it goes ahead and creates an executable file so you can ignore warnings if you like. In fact, if you really don’t want to be bothered, you can disable warnings.

Disabling or otherwise ignoring warnings is an extraordinarily bad idea. It’s a bit like unplugging the “check engine” light on your car’s dashboard because it bothers you. Ignoring the problem doesn’t make it go away.

Adopt a clear and consistent coding style

Writing your C++ code in a clear and consistent style not only enhances the readability of your program, but also it results in fewer coding mistakes. This somewhat surprising state of affairs results from the fact that our brains have only a limited amount of computing power.

When you read code that is clean and neat and that follows a style you’re familiar with, you spend very little brain power parsing the syntax of the C++ statements. This leaves more brain CPU power to decode what the program is trying to do and not how it’s doing it.

A good coding style lets you do the following with ease:

  • Differentiate between class names, object names, and function names

  • Understand what the class, function, or object is used for, based on its name

  • Differentiate preprocessor symbols from C++ symbols (that is, #define 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 format for your module headers that provides information about the functions or classes in each module, the author, the date, the version, and something about the modification history.

All programmers involved in a single project should use the same coding style. A program written in a patchwork of different coding styles is confusing and looks unprofessional.

Comment the 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.

Formulating comments forces you to take stock of what it is you’re trying to do. Short comments are enlightening, both when you read them later and as you’re writing them. Write comments as if you’re talking to another, knowledgeable programmer.

Single-step every path in the debugger at least once

As a programmer, you have to understand what your program is doing. It isn’t sufficient that the program outputs the expected value. You need to understand everything your program is doing. Nothing gives you a better feel for what’s going on under the hood than single-stepping the program, executing it step by step with a good debugger (like the one that comes with Code::Blocks).

Beyond that, as you debug a program, you need raw material to figure out some bizarre behavior that might crop up as the program runs. Nothing gives you that material better than single-stepping through each function as it comes 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 you examine the function by itself rather than after it has been thrown into the pot with the rest of the functions — by then, your attention has gone on to new programming challenges.

Limit the visibility

Limiting the visibility of class internals to the outside world is a cornerstone of object-oriented programming. The class should be responsible for its internal state — if something gets screwed up in the class, then it’s the class programmer’s fault. The application programmer should worry about solving 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. 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 to get the job done.

Keep track of heap memory

Losing track of heap memory is the most common source of fatal errors in programs that have been released into the field — and, at the same time, the hardest problem to track down and remove. (Because this class of error is so hard to find and remove, it’s prevalent in programs that you buy.) You may have to run a program for hours before problems start to arise (depending upon how big the memory leak is).

As a general rule, programmers should always 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, then there should be a member MyClass::release() that returns it to the heap. Specifically, MyClass::create() should not require the parent function to release the memory.

If at all possible, MyClass should keep track of such memory pointers on its own and delete them in the destructor.

Zero out pointers after deleting what they point to

Make sure that you zero out pointers after they are no longer valid; you do so by assigning them the value nullptr. The reasons for this action become clear with experience: You can continue to use a memory block that has been returned to the heap and not even know it. A program might run fine 99 percent of the time, making it very difficult to find the 1 percent of cases where the block gets reallocated and the program doesn’t work.

If you null out pointers that are no longer valid and you attempt to use them to store a value (you can’t store anything at or near the null location), your program will crash immediately. Crashing sounds bad, but it’s not if it exposes a problem. The problem is there; it’s merely a question of whether you find it or not before putting it into production.

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, and you wouldn’t want to disappoint them, would you?

Limit your use of exceptions to true errors. 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.

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 ultimate demise. Having created a destructor, don’t forget to declare it virtual.

“But,” you say, “my class doesn’t inherit from anything, and it’s not subclassed by another class.” Yes, but it could become a base class in the future. Unless you have some good reason for not declaring the destructor virtual, then do so when you first create the class.

Provide a copy constructor and overloaded assignment operator

If your class needs a destructor, it almost surely needs a copy constructor and an overloaded assignment operator. If your constructor allocates resources such as heap memory, the default copy constructor and assignment operator will do nothing but create havoc by generating multiple pointers to the same resources.

When the destructor for one of these objects is invoked, it will restore the assets. When the destructor for the other copy comes along, it will screw things up.