Three Lessons, One Habit

This chapter was about the distance between code that compiles and a program you would hand to someone. Structuring a Larger Program closed that distance from the design side: outline the steps first, make each step a function with one job, put the prototypes at the top as the program's table of contents, and pass data explicitly instead of reaching for globals. Exam C versus Modern C closed it from the other side, naming the five Turbo C idioms that old papers still carry — void main, conio.h, gets, the 2-byte int, and the predict-the-output puzzles — so you can answer a paper without absorbing its errors. The capstone then ran the whole method once: analyse the problem, write the algorithm, design the data, and only then write C, with every call that can fail checked.

What is left is the part of the craft that has no single lesson of its own, because it applies to every program you will ever write: recognising the kinds of mistake C lets you make, finding them, and knowing when to stop caring about speed.

Four Kinds of Error

Errors sort into four kinds, and the sort is useful because each kind is caught by a different thing — the compiler, a run, a reader, or bad luck.

A syntax error breaks a rule of the language, so the compiler refuses to produce a program and tells you where. Textbooks written in the Turbo C era warn that a missing semicolon produces a cascade of misleading messages; today's gcc is better than its reputation:

error: expected ';' before 'b'
   12 |     a = x + y
      |              ^
      |              ;

Still, the old advice survives in one form: when a reported line looks blameless, check the line above it, because that is where the missing punctuation actually was.

A run-time error is one the compiler cannot see and the run trips over: an index outside an array, a mismatched conversion in printf, dereferencing a pointer that was never given a value. The program builds and starts, then does something the standard does not define. On this platform many of these become an AddressSanitizer report, which is more help than C promises you.

A logical error produces a program that builds cleanly, runs happily, and computes the wrong answer. Here is the classic, and note that it compiles without a single warning:

sum = 0;
for (i = 1; i <= 10; i++);
sum = sum + i;
printf("sum %d i %d\n", sum, i);

It prints sum 11 i 11 rather than sum 55. The semicolon after the for header is a complete, legal, empty statement, so the loop body is nothing at all; the loop counts to 11 and then the addition runs exactly once. Nothing is wrong with this program as C, only as arithmetic. Indent that addition as though it belonged to the loop and gcc will notice the mismatch and warn under -Wmisleading-indentation; leave it flush left, as above, and the compiler has nothing to complain about. That is the whole character of a logical error: the only test it fails is the one you thought to run.

A latent error is a logical error that hides until a particular set of data arrives. ratio = (x + y) / (p - q); is correct on every input you are likely to try first and is undefined behaviour the moment p and q are equal. Nothing detects it in advance, which is why test data is designed rather than guessed.

Testing Is Not Debugging

These are two different activities and confusing them wastes time. Testing answers "is something wrong?" and debugging answers "where is it?" — you cannot start the second until the first has succeeded.

Testing has a stage most people skip, which is reading the code before running it: walking a function statement by statement against a checklist of known traps, alone or with someone else reading over your shoulder. It is unglamorous and it is the cheapest bug you will ever fix, because nothing has to be built or reproduced. Then the machine gets its turn, in two stages that are worth separating: the compiler's testing, which is what a warning-free build under -Wall -Wextra buys you, and then run-time testing, which is the only stage that can find a logical error. A clean compile means the program is valid C. It says nothing about whether it computes what you meant.

Run-time testing goes in the order the capstone used: each function alone, then the whole program. Testing a function alone means reaching every one of its exits, and counting its return statements tells you when your list of cases is complete. Only then assemble it, because whole-program testing finds a different class of bug — the ones in the joins between functions, where a delete moves the count that the next add relies on. Neither stage substitutes for the other. And the test data itself is designed for coverage of conditions and paths rather than for quantity: nothing at all, exactly full, one past full, a missing file, and input that is wrong in every way you can think of.

Three Ways to Locate a Bug

Once a test has failed, three techniques find the line. They are not rivals; experienced programmers switch between them within a single bug.

Instrument the program. Print the values you are unsure of and let the program tell you what it is doing. The trick that keeps this from becoming a mess is chapter 14's conditional compilation, so the diagnostics can be switched off without deleting them:

Standard output is sum 10, which is wrong — 1 to 5 is 15. Standard error carries the trace:

  i=1 sum=1
  i=2 sum=3
  i=3 sum=6
  i=4 sum=10

The loop stopped at 4, so the bug is in the condition, i < limit where it should be i <= limit. Three things make this instrumentation rather than clutter. The diagnostics go to standard error, so the program's real output stays clean and redirectable. #define DEBUG 0 removes them from the build entirely rather than merely silencing them, so they cost nothing when off and are still there next time. And because the trace is buffered separately from printf output, do not read the interleaving of the two streams as the order things happened.

Deduce. List every cause that could produce the symptom you are looking at, then eliminate them one at a time with a test that distinguishes between them. Wrong sum: the loop bound, the initial value of the accumulator, the values going in, the addition itself. Four candidates, and one look at the trace above kills three of them. The value of writing the list down is that it stops you from checking your favourite suspect four times.

Backtrack. Start where the symptom is visible and trace the program backwards through its logic until the values stop being wrong. The wrong number was printed in main, so it came from sumTo's return, which came from sum, which came from the loop — and somewhere on that path is the last point where everything was still correct. The bug is immediately after it.

A Checklist for Reading Your Own Code

C's most common bugs are a short and stable list, which is what makes reading against a checklist work. These are worth memorising in the form "where would this hide?":

  • A stray semicolon after if, for, or while. if (total > 100); and for (i = 0; i < n; i++); are legal statements with empty bodies. gcc warns suggest braces around empty body in an 'if' statement [-Wempty-body], and catches the loop case only when the indentation gives it away.
  • = where == was meant. if (code = 1) assigns and then tests the assigned value, so it is always true. Warned as suggest parentheses around assignment used as truth value [-Wparentheses].
  • Missing braces around a multi-statement body. Without them only the first statement belongs to the loop, and the second runs once after it. The fix is a rule rather than vigilance: braces always, even for one statement.
  • An unterminated or nested comment. C comments do not nest, so /* compute x /* then double it */ ends at the first */ and gcc warns '/*' within comment [-Wcomment]. Leave the closing */ off entirely and everything down to the next one silently disappears from the program, or you get error: unterminated comment. Commenting out a block that already contains a comment is where this bites.
  • A missing & in scanf. scanf("%d", code) passes a value where an address was required.
  • An index outside the array. C indices start at 0, so a valid index of x[10] runs 0 to 9 and a loop written i <= 10 reaches one too far.
  • No room for the null terminator. A string of n characters needs n + 1 bytes.
  • A pointer used before it points anywhere. Declaring int *p; and then writing *p = 5; writes through a pointer that was never given a value.
  • Macro arguments without parentheses. #define square(x) x * x turns square(a + b) into a + b * a + b.
  • A conversion specifier that does not match its argument. Every printf and scanf format is a promise about the types that follow it.

Style Rules, Stated as Rules

The course has followed these throughout by example; here they are as instructions, because most of them exist to make the traps above visible rather than to look tidy.

Write one statement per line — C permits several, and the reason not to is the stray semicolon and the missing brace, both of which are invisible in a crowded line. Indent every body and keep the indentation honest, since gcc's -Wmisleading-indentation is only useful to someone whose indentation means something. Keep nesting no deeper than about three levels; past that, extract the inner part into a function with a name, which is the same advice as one job per function arriving from a different direction. Break a complicated condition into simple ones, either with intermediate variables that name what each part means or with an early return. Parenthesise anything whose precedence you would have to look up, because the parentheses cost nothing and the reader is not always you. And put spaces around operators and after commas.

Efficiency, and When to Care

A program consumes two resources worth measuring: execution time and memory. Efficiency comes mostly from the algorithm rather than from the coding, which is why the choice made during design matters more than anything you can do afterwards — a better search beats every micro-optimisation applied to a worse one.

The coding-level improvement that genuinely pays is moving work out of loops that did not need to be inside them:

for (i = 0; i < (int)strlen(text); i++) {
    /* strlen walks the whole string on every single test */
}

length = (int)strlen(text);
for (i = 0; i < length; i++) {
    /* the walk happens once */
}

The first form turns a loop over n characters into roughly n squared work. A modern compiler may hoist the call itself when it can prove text does not change, but it cannot prove that in general, and the second form does not require it to.

Four cautions matter more than the techniques, and they are the reason this section is short:

  • Analyse before you optimise. Improve the part that is actually slow, which is rarely the part you assumed.
  • Make it work before you make it fast. A fast wrong answer is worth nothing.
  • Keep it right while making it faster. Every optimisation is a change, and every change needs the tests run again.
  • Never sacrifice clarity for speed unless a measurement told you to. Unreadable code cannot be maintained, and its bugs outlast the microseconds saved.

For memory the same spirit applies: keep the program simple, declare arrays and strings at the sizes actually needed, and prefer the simpler algorithm.

Key Takeaways

  • Errors come in four kinds: syntax errors the compiler rejects, run-time errors that build and then misbehave, logical errors that run cleanly and compute the wrong answer, and latent errors that stay hidden until particular data arrives.
  • A warning-free compile proves the program is valid C and says nothing about whether it does what you meant; only running it against designed data can tell you that.
  • Testing finds that something is wrong, debugging finds where; read the code against a checklist before running anything, then test each function alone by reaching all its exits, then test the assembled program for the bugs that live in the joins.
  • Three techniques locate a bug: instrument it with diagnostics on standard error wrapped in #if DEBUG so they can be compiled out, deduce by listing candidate causes and eliminating them, and backtrack from the symptom to the last point where the values were still right.
  • Keep the classic-trap checklist in reach: stray semicolon after if or for, = for ==, missing braces, unterminated or nested comments, a missing & in scanf, an index one past the end, no byte for the null terminator, an uninitialized pointer, unparenthesised macro arguments, and a format specifier that does not match its argument.
  • The style rules — one statement per line, honest indentation, nesting no deeper than three, simple conditions, parentheses, spaces — exist mainly to make those traps visible.
  • Efficiency is decided by the algorithm; hoist invariant work out of loops, analyse before optimising, make it work before making it fast, and never trade clarity for speed you have not measured.

The Course, Closed

Fifteen chapters ago a C program was a printf and a return 0. The arc since then has been one question asked at growing scale: what exactly is in memory, who put it there, and who is responsible for it. Tokens and types named the bytes, operators said what may be done to them, branching and loops gave the program its shape, arrays and strings showed what a sequence of bytes really is, functions gave the call stack its frames, structures gathered fields into one object, pointers made addresses ordinary, files carried data beyond the run, dynamic memory handed you the heap and the obligation that comes with it, the preprocessor showed what happens before compilation begins, and this chapter dealt with the trust you place in all of it.

What you have is C89 as the standard actually defines it, which is both the language the exam papers were always trying to test and a real, working tool. The habits are the transferable part: check every call that can fail, name undefined behaviour rather than guessing at it, keep functions small enough to finish testing, and design the data before writing the code. Files, function pointers, multi-file builds and the standard library's quieter corners are the natural next things to learn, and each of them assumes exactly what you now know.