For embedded programming it is important to know what code your compiler is actually generating. Create small sample programs and compile them. Look at the resulting code. You may be surprised!
For example, which of the following would you choose if you wanted the smallest, fastest executable?
index = ReadHardware();
if ( index != 0 ) DoStuff();
or
if ( (index = ReadHardware()) != 0 ) DoStuff();
With the compiler I am currently using, the first one actually generates
smaller code. The second creates extra instructions to save the temporary
value while it does the assignment.
When you come across something weird, get out your copy of the standard (K&R, ANSI, whatever), and read what it says about it. You may learn something new about the language, or what the designers really had in mind. Perhaps you will find an actual bug in your compiler. At any rate, you will probably wind up with a better understanding of how the language works, at least with your compiler. You will also learn to distinguish between coding for your compiler and coding for portability.
For example, look at the following code:
#if OPTION
printf("Hello");
#else
printf("Bonjour");
#else
printf("Gut morgen");
#endif
What will get printed for various values of OPTION?
I started examining this because of a typo in my code. After the compiler
confused me by never compiling the last piece, I checked the standard and
found that in a chain of #if, #else, and #elif, the
first block for which the condition value is non-zero will be compiled and
the following blocks will not. In other words, the standard allows the
(non-obvious) #else #else construct, with the (also non-obvious)
consequence that the second #else block will NEVER be compiled. The
compiler I am using does not even generate a warning for this, so I have
added it to the list of things that I check when I am getting a confusing
bug.
Step back and look at your code
When you are debugging, you will spend a lot of your time running the code and examining the problems that result. If you can't find the cause, even though you understand the problem, step back. Get away from the computer. Read your code carefully. Step through it in your mind. If you have analysis tools, use them.
I have a tool to list source after processing out the #if's. This has
often helped me to find a missing or extra conditional compile. I recently
created an AWK script to read comments from one of my programs and extract a
state table. By comparing the extracted state table with the design, I was
able to find some subtle editing errors, and an oversight in the design
(which becomes obvious once the code has to implement it).
Use tools to examine your code
Use tools to examine your code. After all, a computer is much better at looking at thousands of lines of code than a person. If the tools don't exist, write them. They don't need to be marketable tools; they just need to help you get your job done.
For example, whenever I want a printout of some source file, I use a listing
utility which removes the conditioned out code, generates descriptive
headers for each function, and marks the nesting levels within each
function. This allows me to concentrate on how the code works, rather than
what the compiler needs to know about it.
Change your habits slowly
Make small changes in the way you work. If you can make your way of coding a little bit better, and make that change into a habit, then after a while you'll be writing better code without even thinking about it.
Habits save you from making mistakes, while freeing your mind to think about
more important things.
Be able to explain your code
When you are having trouble getting your code to work, or when you are trying to finish your project, or whenever you think it is necessary, try to explain your code. If your design is complete, and your code correctly implements the design, then you should be able to explain the design and how you implemented it without any hand-waving.
The design objective is to have a design which provides a solution to the problem, and which is "obvious" enough that you can explain it to another programmer who has, at most, limited familiarity with your project. If you have to skip past details, or if you find yourself getting confused, then you have probably found a weak spot in your design.
You should be able to explain what each variable represents, and how each part of the code contributes to the implementation. Watch out for variables which change their meaning as you explain different parts of the implementation; these tend to be a favourite place for bugs to hide. See also "Name things right".
Mental trick: When you are reading your code, try replacing the variable
name with the description from the comment where it was defined. If the code
still makes sense, then it has a good chance of working the way you designed
it.
Record how you do things
When you find a new way of debugging or a new way of avoiding some problem, write it down. The act of writing it down will help you remember it, and if you forget, there will still be somewhere you can look for all the helpful tricks you have found in the past.
If you are working as part of a team, I strongly recommend creating a "How To" document for each project you work on. This is simply a place to record the details of how to do all those things you wind up doing over and over again. Any time somebody asks a question about how to do something, that question becomes a candidate for the list.
When debugging, especially somebody else's code, remember that a lot of
effort and a lot of thought went into creating the code. That means the code
is probably the way it is for a reason. Debugging is not the time to be
questioning implementation decisions, but to correct the implementation
flaws. If decisions have to be questioned, then you may want to consider
redesigning the module; but remember all that effort that went into the
original design.
Understand the real problem
When you find a problem in your code, try to understand how the symptom relates to the code. If you find a problem, but cannot explain how it causes the symptoms you are seeing, you may wind up fixing the wrong problem.
I have often found that this has caused me to keep looking after I fixed what I thought was the problem. Several times, this extra searching has led me to find a deeper problem which was the real cause. Even when I don't find a deeper problem, looking for the explanation has usually led me to a deeper understanding of the code.
Recently I had a problem which was losing data. In my investigation, I found there was a problem in the code which would cause the symptom, except that the same problem existed in the previous version, and the previous version was working fine.
I initially found it hard to explain, but ultimately was able to explain why
it happened in the new version and not the old version due to a timing
difference introduced by another bug fix. From this, I gained a better
understanding of that previous bug fix, and also was confident that this fix
would really solve the problem.
Fix the whole problem
After you find and fix a problem, search you code for other occurrences of the same problem. This will often save you from fixing the same problem again, and will sometimes lead you to a problem which may otherwise have been difficult to find.
Also, if you fix it now, while it's fresh in your mind, you will understand the fix, rather than needing to go through all the reasoning again later.
Recently I fixed a problem which was causing a program to lose data. After I
found the problem, and understood what the root cause of the problem was (in
this case, clearing a buffer without signalling another piece of the
problem), I searched the code to find out where else the same situation
occurred. This search uncovered a total of three places where the same
potential data loss existed.
Leave the code better than you found it
When you go in to fix some code, you should make an effort to improve the
code, not just to fix it. Try to write the modifications using the same
style and commenting techniques as the existing code. If you create a new
variable, give it the right name, not i.
Update the documentation for any code you modify. Don't leave undocumented code sitting there waiting to trap the next programmer. If you had trouble understanding some of the code, maybe the comments need to be clearer. Expand on what was written to explain it in a way that would have helped you.
If every time you fix a bug, the code gets a little better, rather than just a little bigger, then you can be confident that you will eventually run out of things to fix.
Many of the rules which follow are intended to help overcome this memory
limitation. All of them are intended to help create code which starts with
fewer bugs in it.
Never write the same code twice
If you find yourself writing the same code, it should either be pulled off
as a separate function or a macro, or put into a library.
Name things right
Make variable and function names self-explanatory. They will affect your ability to write the code, and other people's (or even your own) ability to maintain the code later. Names will be taken as self-explanatory, whether you write them that way or not. I remember a function called "Putone" which sounds like a yucky colour. If it had been called "PutOne" I would have understood it immediately.
One trick for managing variable names is to provide a comment with the definition of the variable. This comment should be a concise explanation of what the variable represents.
For example,
int i; /* counter */gives you no clue about what the counter is counting, or how you can determine if it is doing the right thing, but
int numRecords; /* Number of active records in the file */immediately tells you what the variable is measuring, and anywhere you see it used in the code you can ask yourself "does numRecords contain the number of active records in the file?" You can even implement debugging code which actually checks to ensure that numRecords does what the definition says.
Whenever you find yourself using numRecords as anything other than the
"Number of active records in the file", watch out! That's where the bugs are
likely to bite you.
Make functions do what they say
Recently, I was upgrading an automatic mail handler. I added a line to
handle the "From " field at the start of every message by checking:
Be careful when naming functions. Even if you are the one who will update the program, you may get confused by a name which tells you approximately what the function does. You will assume it does what it claims, and may introduce bugs by that assumption.
Make the function do what its name says it does. If it does less, the extra
might be overlooked, and if it does more, that might give some unexpected
side effects in a future version. Anybody reading the code is going to
assume the function does what its name says, so make that assumption work
for you, not against you.
Eliminate warnings
If your compiler is giving you a warning, do something to eliminate it.
Then, when you get a warning, you know to pay attention!
Don't use global variables
You never want to access global variables directly from another module. In general, you can use a function (or set of functions) to access the variable. If you are programming an application where speed is EXTREMELY important (note: I have only seen this in embedded programming, and even then only rarely) then you can use an inline function, or fake an inline function by using a macro defined in the other module's header file.
The main idea here is to keep all knowledge and control of a module's variables with that module.
Everyone has different ideas on how many comments are enough, and what comments should say. The best approach is to get in the habit of writing comments to describe the code. Once you are in the habit, the comments will flow more easily, and you will hardly notice the extra typing.
Commenting standards can be useful because they allow you to develop the
habit. If you work with a team of programmers, using the same commenting
style also makes it easier to read each other's code.
Use comments!
It sounds obvious, but it's a serious suggestion. Even if you're not
consistent about it, and not very good at it, use comments! Whenever you can
remember to do so, use comments to write notes about why you are writing
the code the way you are, or what this variable is supposed to represent.
The code will be easier to understand later if there is at least some clue
about what you were thinking at the time.
Comments describe, not transcribe, the code
The code already describes how the program works, and most of the people who
read the code probably know how to read the language. Use comments to
capture what the code can't. Use comments to explain the algorithm, explain
your assumptions, note any ideas for future changes, etc, not to say the
same thing as the code.
Use comments to describe your variables
When you create a variable, you know what it is supposed to mean. Document
that in a comment with the variable and you will be able to remember exactly
what you intended when you look at the code many months later.
Pseudo-comments make extractable documentation
Once you get comfortable putting in comments, you can consider using
pseudo-comments to make the algorithm extractable. If you mark your comments
with a letter (for example, "/*P" for pseudo-code, or "/*V" for variables),
you can write a simple script (for grep or awk, for example) which can
extract the pieces you want. Extracting the pseudo-code (/*P comments) would
give all the function names and the algorithms, whereas extracting the
variables (/*V comments) would simply list the variables.
Document macros as if they were functions
If you are using macros to emulate inline functions, document them to the same standards you use for functions. This will make it easier to understand them later, or to convert between macros and functions.
It seems that no two programmers can agree on style! The most important rule I've been able to come up with is "BE CONSISTENT". It is less important what style you use than how consistently you use that style. By sticking to one style you make your code easier to read because you eliminate the surprises that come with differing styles. You also give the reader some confidence that certain kinds of problems won't show up (for example, a style that insists all if's are followed by braces will probably not contain brace errors after if's).
Choose a style and use it consistently! If you want to change styles, do it
on your next project.
Use those braces
Never put the body of an "if" statement on the next line without bracketing it in curly braces. If you do, I guarantee that someday you will add a line (debug code, perhaps, or a necessary later addition) without realizing that the line will not be inside the "if". The resulting bug is often quite difficult to find.