C Part I · Module 13 · Writing Correct and Reliable C
It Compiled Is Not the Same as It Is Correct
Twelve modules have each, somewhere, reported a measurement in the same shape: a program with a real defect in it, compiled at this curriculum's full warning level, and not one diagnostic. A leak. A use-after-free. A function declared one way in one file and defined another way in the next, which compiled twice and linked. A reordered structure that silently permuted every initialiser in the project. Those were scattered across chapters and easy to read as individual curiosities. This chapter collects fifteen of them into one directory, compiles them all, and counts — which turns a series of anecdotes into a number, and the number is the reason this module exists.
Intermediate18 min readCPart IFundamentalsCorrectness
Part I · Module 13 · Chapter 13.1 · It Compiled Is Not the Same as It Is Correct
1. What a Clean Build Actually Claims
When a C compiler produces an object file without complaint, it has established a specific and surprisingly short list of things:
- the source is syntactically valid C;
- every name used is declared, with a type;
- every expression's types are compatible with the operators applied to them, after the conversions the language performs;
- the constraints the standard requires it to diagnose are satisfied.
That is the claim. Read it against what people hear, which is "the program is probably fine".
A clean build says the program is well-formed. It says nothing at all about whether it does what you meant, and remarkably little about whether it has meaning at all.
The second half of that sentence is the part that distinguishes C from most languages a reader will have met. In a language with bounds-checked arrays and managed memory, "well-formed" rules out whole categories of disaster. In C it does not, and the rest of this chapter measures by how much.
2. The Census
Fifteen files. Each contains exactly one defect, and every one of them is a bug this curriculum has already measured somewhere in Modules 9 to 12:
| Defect | First measured in | |
|---|---|---|
| d01 | a memory leak on an early-return path | 9.9 |
| d02 | a use-after-free | 9.10 |
| d03 | a double free | 9.10 |
| d04 | summing an uninitialised array | 9.12 |
| d05 | a struct whose members were reordered under a positional initialiser | 10.2 |
| d06 | assigning an out-of-range value to an enum | 10.8 |
| d07 | const defeated by a pointer typedef | 10.9 |
| d08 | a multi-statement macro used after if | 11.6 |
| d09 | a misspelled macro name in #if | 11.7 |
| d10 | a non-literal printf format | 12.2 |
| d11 | an overlapping memcpy | 12.9 |
| d12 | a comparator that subtracts and overflows | 12.9 |
| d13 | invalid code inside #if 0 | 11.7 |
| d14 | shifting an int by 40 | — |
| d15 | dereferencing a null pointer | 7.4 |
Compiled at the level every example in this curriculum has used:
$ cc -std=c17 -Wall -Wextra -Wpedantic -c d01..d15 -o /dev/null
defect classes: 15
produced a warning: 1
produced NOTHING: 14One. The shift by 40 was diagnosed. The other fourteen — including the use-after-free, the double free, the null dereference and the leak — compiled in silence.
3. What Does Catch Them
That number improves, and the shape of the improvement is the useful part.
Extra warning flags: 1 → 4. -Wundef caught the misspelled #if (Chapter 11.7 §3), -Wformat-nonliteral caught the unchecked format (Chapter 12.2 §4), -Wassign-enum caught the out-of-range enum. None of these is in -Wall -Wextra, and each has to be asked for by name. Chapter 13.7 is about which to turn on.
A static analyser: 6 of 15. It traces execution paths rather than checking one statement at a time, which is why it finds the leak, the use-after-free, the double free, the uninitialised read and the null dereference — the ones that depend on what happens between statements.
Both together: 9 of 15. The two overlap only partly, which is the argument for running both rather than choosing.
Plus sanitizers at run time: 11 of 15. The overlapping memcpy and the overflowing comparator are reported by instrumented builds — with a condition that matters and §4 is about.
4. The Four Nothing Catches
d05 a struct whose members were reordered under a positional initialiser
d07 const defeated by a pointer typedef
d08 a multi-statement macro used after an if
d13 invalid code inside #if 0None of these is exotic, and that is the point.
d05 is Chapter 10.2 §6: swapping two members of the same type silently permutes every positional initialiser in the project, and the compiler has nothing to object to because the count and the types still match. The cure is named initialisers — a habit, chosen before the bug exists.
d07 is Chapter 10.9 §5: const tp p qualifies the pointer rather than what it points at, so a function that promises not to modify can modify. The code is well-formed by construction. The cure is never hiding a pointer in a typedef — again a habit.
d08 is Chapter 11.6 §5: a macro expanding to two statements attaches only its first to an if. The preprocessor produced valid C, so there is nothing to diagnose. The cure is do { } while (0).
d13 is Chapter 11.7 §2: code inside a deleted branch is not compiled, not type-checked and not warned about, so it rots quietly. No tool examines text that no longer exists. The cure is building every configuration you ship.
Four of fifteen are not tooling failures. They are failures of design and review, and no amount of instrumentation reaches them — because in each case the compiler was handed a perfectly well-formed program that means something other than what the author intended.
That is why this module does not end at Chapter 13.10. Tooling has a ceiling, the ceiling is visible in the figure, and Chapter 13.11 is about the habits that work above it.
5. Why C Is Like This
Not an apology — an explanation, because it tells you where to expect gaps.
The compiler sees one translation unit. Chapter 11.2 §5 established the boundary and Chapter 11.1 §6 measured the consequence: a function declared with two doubles and defined with two ints compiled cleanly twice and linked. No compiler invocation ever saw both files.
Many properties are undecidable in general. Whether a pointer is still valid, whether two regions overlap, whether a free will eventually happen — these depend on values that only exist at run time. A compiler that tried to answer them would have to refuse correct programs.
C's design prefers a fast compile and no hidden cost. There is no bounds check because a bounds check costs an instruction you did not ask for. That trade is the reason the language is used where it is used, and the missing checks are the bill.
And the checks that do exist are mostly about types, which catches a real and useful class of mistake — d14's shift was caught, and Chapter 11.4 §7 measured a static mismatch being reported with both lines quoted. The type system is not nothing; it is simply answering a different question from "is this correct".
6. What This Module Is For
The gap in Figure 1 has a shape, and Module 13 is organised around it.
| The gap | The chapter |
|---|---|
| what "no meaning at all" actually means | 13.2 |
| which non-portable behaviours you may rely on | 13.3 |
| the arithmetic that silently leaves range | 13.4, 13.5 |
| the access that corrupts something else | 13.6 |
| getting more out of the compiler you have | 13.7 |
| stating an assumption so a violation stops the program | 13.8 |
| finding a fault you cannot see by reading | 13.9 |
| the tools that watch a running program | 13.10 |
| the habits that work above the ceiling | 13.11 |
And one disposition to adopt before any of them.
Treat a clean build as the beginning of the question, not the end of it. The compiler has told you the program is well-formed. Whether it is correct is a separate matter, and in C it is almost entirely yours.
That is not pessimism, and it is not an argument for anxiety. It is the reason the habits in this module exist: in a language that checks this little, the checking has to come from somewhere, and the somewhere is how you write the code, what you turn on, what you run it under, and who reads it.
7. What You Should Be Able to Say Now
- A clean build claims: valid syntax, declared names, compatible types, and the constraints the standard requires diagnosed. Nothing more.
- Measured: 15 real defects, 1 warning at
-Wall -Wextra -Wpedantic— the level this curriculum has used throughout. - The fourteen silent ones include a leak, a use-after-free, a double free, a null dereference and an uninitialised read.
- Extra warning flags: 4 of 15 —
-Wundef,-Wformat-nonliteral,-Wassign-enum, each asked for by name. - A static analyser: 6 of 15, because it traces paths rather than statements.
- All static tooling together: 9 of 15. They overlap only partly — run both.
- Plus run-time sanitizers: 11 of 15, and only on inputs that reach the defect.
- Four are caught by nothing: a reordered initialiser,
constthrough a typedef, a statement macro, and code in a deleted branch. - Those four are design and review failures, which is why the module continues past the tooling chapter.
- The reasons C is like this: one translation unit at a time, undecidable properties, and no hidden cost.
- A measurement that flatters the tooling is the one to re-check — mine did, and was wrong by a factor of nearly four.
- A clean build is the beginning of the question.
8. Exercises
Exercise 1 — predict the diagnostics. For each, say whether -Wall -Wextra -Wpedantic warns, and name the chapter that measured it.
int *p = malloc(4); free(p); return *p;
struct s { int b; int a; }; struct s x = { 1, 2 };
const tp q = r; q->n = 1; /* typedef struct t *tp; */
#if ENABLE_FEATRUE
printf(fmt, value); /* fmt is a const char * */
memcpy(b + 2, b, 6);Exercise 2 — build the census. Reproduce §2 with five defects of your own choosing. Report the count, and say which of the five you expected to be caught and were not.
Exercise 3 — raise the floor. Given the figure, write the compile command you would use on a new project, and justify each flag beyond -Wall -Wextra by naming a defect it catches.
Exercise 4 — classify the four. For each of d05, d07, d08 and d13, say why no tool can catch it, and give the habit that prevents it.
Exercise 5 — the attractive number. A colleague reports that a new linter finds 100% of the bugs in their test corpus. Give three questions you would ask before believing it, using the §4 callout.
Exercise 6 — the boundary. Explain why a compiler cannot catch the Chapter 11.1 §6 type mismatch, and what single change to the source would move it inside the boundary where it can.
Exercise 7 — cost the checks. For each of bounds checking, null checking and lifetime tracking, say what C would have to give up to provide it, and name one language that makes the opposite trade.
Exercise 8 — write the disposition. In three sentences, state what you now believe a clean build tells you, as advice to someone starting their first C project.
9. Summary
- A clean build means well-formed: syntax, declarations, types, required diagnostics. Not correct.
- Measured: 15 defects, 1 warning — including a leak, a use-after-free, a double free and a null dereference going silent.
- Extra flags → 4, analyser → 6, both → 9, plus sanitizers → 11.
- Four are caught by nothing automated — reordered initialiser,
constthrough a typedef, statement macro, dead branch — and all four are design and review failures. - C is like this because of one translation unit at a time, undecidable properties, and no hidden cost.
- A flattering measurement is the one to re-check. Mine said 15 of 15; the truth was 4.
- Treat a clean build as the beginning of the question.
The census used a word repeatedly that this curriculum has so far relied on you half-knowing. Eight of the fifteen defects were not merely wrong — they were undefined behaviour, and that phrase has been appearing since Module 2 with a working definition of roughly "anything could happen". That definition is good enough to make you cautious and not good enough to reason with, because it suggests a program with undefined behaviour is a program whose output is unpredictable. It is worse than that, in a way Chapter 9.12 §3 already measured once without naming: a compiler is entitled to assume undefined behaviour never occurs, and to work backwards from that assumption through code that runs before the offending line. The next chapter defines the term properly, and explains why a program containing it has no predictable meaning at all — not even the behaviour you observed on the last run.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
