27 Rookie Mistakes of C++ Programmers
and How to Avoid Them
A. Program Design
Consequence: The program is developed very slowly, if at all.
Solution: Do not imagine that you can just sit down and write a program off the top of your head. You will not succeed. Do not stare at a computer monitor for inspiration. The monitor will offer none. Instead, take the time to develop the program correctly. This will save you time.
Summary: Think about your programs before writing them.
Consequence: The programmer spends too much time debugging the program, finding errors that would easily have been avoided by having contracts. Untold hours have been wasted fixing errors that would have vanished immediately had contracts been used.
Solution: Give a contract for each function before writing the function. The contract is a clear description of what the function must do. It is a contract between the function and the caller of the function. The function says, if inputs are provided satisfying certain preconditions, then the function will produce a certain kind of return value, and will possibly cause certain postconditions to become true.
Be sure to include in the contract information about the purpose of each and every parameter, and the meaning of the returned value, if there is one. Check the contract to see that each parameter is discussed.
Imagine trying to do business by writing and signing contracts only after the work has been done. The contractor does not know what he or she has signed on to do, and the person hiring the contractor does not know what he or she will get. Therein lies insanity. Yet, rookie programmers invariably try to work this way. If you find that you cannot think of an appropriate contract for a function, then you do not understand it well enough to write it. Think some more about what the function is supposed to do for its caller. Make the contract clear and precise.
A contract should not tell how a function works. It should tell what the function accomplishes. When writing a contract, you should not describe things that only make sense if you read the body of the function (the part after the heading). A person reading the contract should not have to read the body of the function to make sense of what the contract is saying.
Summary: Write clear and precise contracts.
Consequence: The program is exceptionally difficult to understand and modify. It probably does not work.
Solution: Think about functions at a conceptual level before working out details. Be sure to start by asking what the function does. Do not think about how it should work too soon. Ask if the conceptual view of what the function is supposed to do makes sense. If, for example, you are writing an instance method for a class Rabbit, ask whether it makes sense to ask a rabbit to do the things that this function does. If the rabbit is apparently being asked to take on the resposibilities of a tractor, then think again. Similarly, make sure each function has a focus, and that it does not try to do several unrelated tasks.
Look at the parameters of the function. Ask whether each parameter is required, and whether it is reasonable for the function to ask for them. You must have a contract for each function. What you are doing here is making sure the contract is simple and reasonable. Choose your functions so that the contracts can be made short and simple, without making them imprecise or vague. If your contract is exceptionally complicated, then you have probably broken the problem into functions in a bad way.
Summary: Make your contracts short and simple, without making them imprecise.
Consequence: Your program is very difficult to get working. You spend long hours trying to put bad parts together, hoping to get a working whole.
Solution: If a function does not fulfill its contract, fix it. Occasionally, you need to change the contract, but that is rare. Fix the function.
Summary: Make your functions fulfill their contracts.
Consequence: The program is difficult to modify, because the functions are not sufficiently general that they can be used at several places in the program. Any modification of the program usually involves modifying many functions.
Solution: Write functions in as general a way as is reasonable. Use parameters. Wherever possible, write the functions so that they might very well be used in another program. That way, it will be easy to use them in more than one way, and at more than one place, in this program.
Summary: Choose general functions where possible.
Consequence: The program is difficult to understand, and the misleading names lead the programmer into mistakes.
Solution: Choose names that reflect what a function really does, or what a variable holds or is used for. If you have a variable that tells how many widgets you have, call it something like numberOfWidgets. Do not call it x, or even count. Make the name as specific as possible. If you have a function that computes the discriminant of an equation, call it something like computeDiscriminant. If it returns the discriminant as its value, call it discriminant.
You can go too far in this. Avoid extremely long names. A rough rule of thumb is that a name of 30 or more characters is probably too long to be practical.
Summary: Use descriptive names.
Consequence: The program is difficult to read, and difficult to get working.
Solution: Break the program into small functions. A rule of thumb is that a function should be no more than about 40 lines, excluding comments. In some cases this rule must be broken, but try to follow it wherever possible. Much shorter functions are even better.
Be sure to have a clear contract for each function, and to choose your functions so that they have a focus, and have short descriptions.
Summary: Break your program into short functions.
B. Being Prepared for Errors
Consequence: The program does not work, and the programmer spends a long time on debugging.
Solution: It is tempting to be optimistic, and after writing some code, just try it on the computer to see if it works. Unfortunately, that is a waste of time unless you have a good reason for believing that the code works. Instead, try a few small inputs by hand. See if the code seems to be doing the right thing. This will save you time.
Summary: Check your function definitions by hand on small inputs.
Consequence: The programmer does not leave enough time for debugging, and cannot complete the program on time.
Solution: Remember that, after you get the program to compile, the problem of getting the program to work has only begun. Leave adequate time for testing and debugging.
Summary: Getting a program to compile is the beginning of getting it to work, not the end.
Consequence: The program does not work. The programmer has no idea where the mistake is. In reality, there are almost always several mistakes, and each acts to hide the others.
Solution: Develop your program in pieces. Write a contract for a function, write the function, then write a small program that tests the function, to see whether it seems to be meeting its contract. This way, if an error occurs, the error is almost always in the small part that was just written. Sometimes you need to write and test two or three functions together, but keep the number to a minimum.
It is very tempting to jump ahead too quickly. After you write part of a program, you will want to move ahead to the next part, so that you can get finished quickly. But if you move ahead to the next part before this part is working, you are only making much more work for yourself. You will spend time trying to fix a part that is correct, not realizing that another part is broken. Do not let yourself get caught in this trap.
Summary: Check each piece as you write it, even pieces that you are sure must work.
Consequence: The program does not work, and the fact that it does not is readily apparent to those who use the program.
Solution: This mistake might be due to an assumption that a program that compiles and works on simple inputs must work on more difficult inputs too. But that is not the case at all. Programs that work on some inputs, but not others, are very common. Do thorough testing. Try several cases, including relatively complicated or extreme cases. Be sure to do enough tests that every part of your program is used.
This mistake might also be due might be due to a belief that what you don't know cannot hurt you. (``I want it to work, and don't want to do a test that might show me that it does not work.'') If you find yourself reluctant to do tests for fear of finding further errors, take a break. Come back to the program when you are prepared to deal with errors. Look at debugging as a challenge, like working a crossword puzzle.
Summary: Test each part of your program thoroughly.
Consequence: All does not go well. The programmer is unprepared to catch the mistakes in his or her program.
Solution: Program defensively. What that means is, wherever it is easy to do so, include extra tests in your program to see that things are the way that you think they are.
Suppose that you have a function that takes an integer parameter, and that integer is required to be positive. (So the contract says that the integer must be positive.) The rookie programmer assumes that he or she will not make a mistake, and pass a nonpositive number to this function. The expert programmer is wary, and includes a test to see that the integer is indeed positive.
If you have a value that you know must be either 1 or 2 or 3, don't presume that you will not make a mistake and use a different value. Put a check in the program. If things are not right, the program can just print a message and stop.
Summary: Program defensively. Put extra tests in your program to check that all is well.
Consequence: The programmer makes changes that turn out to be the wrong thing to do. There is no way to get back to the previous version of the program. The programmer must try to recreate that version. This needlessly consumes time.
Solution: Keep backup files. Before you embark on a major change of a program, back it up so that you can return to the former version.
Summary: Keep backup files.
C. Fixing Errors
Consequence: The programmer gets nothing close to a working program.
Solution: If you see a compile error that you do not understand, examine the program carefully to see what is wrong. Use the following hints. If none of them help, and you cannot see what is wrong, then ask for help from somebody else.
Summary: Do not let compile errors stop you.
Consequence: The programmer spends a lot of debugging time trying to fix a mistake that the compiler warned was present.
Solution: Pay attention to warnings. They often indicate genuine errors. Fix your program so that it compiles without warnings.
Summary: Make your program compile without warnings.
Consequence: The programmer spends long hours trying random changes, only to find in the end that he or she has done nothing but ruin the program.
Solution: If you are a doctor, you do not cure a patient by trying random drugs until one works. You will almost certainly kill the patient before hitting upon the correct medication. You must find out what is wrong before you try to fix it.
Often, rookie programmers resort to random trial when they are tired, and they just want the program to work so they can go do something else. If you are tired, and cannot concentrate on your program, take a break. Think about something that you find relaxing. Resist the temptation to try random changes. Remember that they will almost certainly only make matters worse, not better.
Summary: To fix a program, find out what is wrong with it.
Consequence: The programmer ruins the program while trying to fix it.
Solution: If you get a compile error, understand why the compiler is complaining. See how to fix it, while keeping the program doing what you want it to do. Do not abandon your problem solving capabilities just because the compiler complains.
In some ways, the compiler is quite stupid. The error message that it gives might suggest a way to fix the error. But that way might not be the right way for your program. Do not let the compiler push you into a fix that is inappropriate for this program. For example, do not throw in a semicolon just because the compiler complains of a missing semicolon, unless that is clearly the right fix. Do not define a type just because the compiler complains that the type is undefined, unless that is the right thing to do. You are the boss, not the compiler.
Here is an example that actually happened. A student had a character variable called op that contained '+' or '-' or '*'. He wanted to compute a+b if op = '+', a-b if op = '-' and a*b if op = '*'. He wrote
result = a op b;hoping that it would do the right thing. This is not correct C++ syntax, and the compiler gave an error message. The programmer changed this part of the program to
result = a; op; b;to try to get the compiler to accept it. The compiler did indeed accept it. The new program has the following meaning.
Summary: Do not let the compiler push you around.
Consequence: The program is never made to work.
Solution: Develop and employ debugging skills. There are two common ways of localizing and diagnosing errors.
When deciding what to print, think about what might be wrong. Remember that, since the program does not work, at least one of your assumptions is incorrect. So print things that you think you know, to see if your are right. Show the input. Show intermediate results. Concentrate on those parts of the program that are directly concerned with what is not working. You will find this much easier if you test individual components as you create them. The error is usually in the new components.
Print to a file, since the output is usually too long to fit on a screen.
Summary: Learn and employ debugging skills.
D. Using the Language
Consequence: The program does not compile, or does not have the desired effect.
Solution: Keep a core subset of the programming language that you use for writing programs. Stick to this subset. As you learn more about the language, add more to your core subset.
As an example of making up the language, see the above item, where a programmer wrote a op b.
Summary: Know the language subset that you use, and stick with it.
Consequence: The programmer makes silly mistakes that would be apparent if the program were well indented during development, and spends hours trying to find them. Syntax errors due to mismatched braces are unnecessarily difficult to find.
Solution: Indent the program well during development. It does not take much time, and can save a lot of time.
Do not over-indent, and do not indent for no reason. Indent the body of an if-statement, while-statement, etc. about two spaces. Keep everything that belongs at the same level at the same indentation. If you have several if's in a row, do not indent deeper and deeper. Think of the if's as checking for several conditions. So you write
if(...) { ... (indented) } else if(...) { ... (indented) } else if(...) { ... (indented) } else { ... (indented) }
You will find tabs problematic. Different text editors might use a different amount of space for a tab. A good idea is only to use spaces. You can turn off the use of tabs in auto-indenting editors.
Summary: Keep your program well-indented during development.
Consequence: The program is mistake-prone, and is difficult to modify.
Solution: Declare variables in the local scope of functions. Use parameters and return values to communicate between functions.
There are exceptions to this rule. Sometimes, it makes sense to put variables in the global scope. Rookie programmers tend, however, to overuse global variables, so try to avoid them. You will learn later of occasions to use global variables.
Summary: Avoid global variables.
Consequence: The program either does not compile or does not do what was intended. (The former case is more desirable.)
Solution: Keep in mind that, to run a function, you must give a parameter list, even if there are no parameters. If you write
if(cin.fail) { ... }you are asking whether function cin.fail is 0. It is not, since it is a function. If you want to ask whether the last operation of cin failed, you write
if(cin.fail()) { ... }
Summary: Always give a parameter list when running a function.
Consequence: The program does not compile, and there is usually a mysterious compile error message.
Solution: Learn the reserved words. The reserved words of C++ are as follows. (Some of these are reserved only in certain versions of C++.)
Summary: At a compile error, check for reserved words.
Consequence: The program does not work, and has very mysterious behavior. The error is difficult to isolate.
Solution: This error only occurs in more advanced programs that do dynamic memory allocation. To avoid it, keep in mind that just because you have created a pointer variable does not mean that it is pointing to something. Think about the memory.
Summary: Pay attention to the values of pointer variables.
Consequence: There are no bad consequences for short runs. For long runs, your program runs out of memory.
Solution: Be sure to deallocate dynamically allocated memory that was allocated using new. Use delete to deallocate. But also be careful not to deallocate something that you need. A good idea is to make it possible to suppress deallocation, so that you can check whether that is causing a problem.
Summary: Pay careful attention to deallocating memory.
Consequence: The program does not work because the programmer misunderstood the library functions.
Solution: Make sure you understand the contract for a library function before you use it. Do not presume that you understand the function just because the name suggests something to you.
Summary: Find out about library functions before using them.
Consequence: The program does not work.
Solution: This is not really entirely a rookie mistake. Seasoned C++ programmers make these mistakes occasionally. Your best way to guard against them is to remember them, and to check for them carefully when reading your program. If you write
if(x = y) ...you do not get a test whether x and y have the same value, as you would with ==. Instead, your program does an assignment, forcing x to be equal to y. The value that is tested is the value of y. You can avoid the problem of using = for == when you are asking whether a variable is equal to a constant by putting the constant first. So you can write if(0 == x) rather than if(x == 0). If you accidentally write if(0 = x), you will get a compile error. If you accidentally write if(x = 0), you will not get a compile error. You would prefer to get the compile error here, since you have made a mistake.
If you write
if(s == t) ...where s and t are of type char*, then you are not asking whether the two strings are the same; you are asking instead whether they are stored at the same memory address. To compare strings, use library function strcmp. strcmp(s,t) returns 0 if strings s and t are the same, -1 if s is alphabetically smaller than t, and 1 if s is alphabetically larger than t.
Summary: Be careful in using =, ==, <, etc.