Skip to content
VLSI Mentor

C Part I · Module 13 · Writing Correct and Reliable C

Defensive Habits That Prevent C Bugs

Four defects have been outstanding since Chapter 13.1 measured them. Every warning flag this curriculum recommends is silent on them. The static analyser is silent. The sanitizers are silent. Even -Weverything, the complete set of diagnostics the compiler owns, says nothing. They are not hard bugs — each one is prevented completely by a habit that costs nothing and takes no runtime check. This chapter settles all four, adds the two habits that remove entire bug classes rather than single defects, and corrects the most widely taught defensive idiom in C by measuring it failing at the one job it is famous for.

Advanced22 min readCPart IFundamentalsCorrectness

Part I · Module 13 · Chapter 13.11 · Defensive Habits That Prevent C Bugs

1. Four Defects That Outlasted Every Tool

Chapter 13.1 compiled fifteen defective programs and counted how many the toolchain caught. The rest of Module 13 spent itself raising that count. Chapter 13.7 read the warnings properly and took it from one to four. Chapter 13.10 added the static analyser, which reached nine, and then the sanitizers, which reached eleven.

Eleven of fifteen. The remaining four were never caught by anything, and they are still outstanding:

  • a structure whose members were reordered under a positional initialiser
  • const defeated by a pointer typedef
  • a macro that expands to two statements, used after an if
  • a defect inside a branch nobody compiles

It is worth being precise about how thoroughly the tools miss them. The following is not a selective test. Each of the three that can be compiled as a single program was put through the recommended flag set from 13.7, then the static analyser, then AddressSanitizer and UndefinedBehaviorSanitizer together at runtime — and then through -Weverything, which is not a flag anyone should ship but which turns on every diagnostic clang possesses, including the ones that are off by default because they are too noisy to live with:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cc -std=c17 -Weverything -O2 -c reordered_struct.c -o /dev/null
warning: include location '/usr/local/include' is unsafe for cross-compilation
         [-Wpoison-system-directories]
1 warning generated.

That warning is about the machine, not the code. The same single warning, with the same text, is the entire output for all three files. The compiler has nothing to say about any of them.

That is not a failure of the tools. It is a statement about what kind of defect they are. A warning flag recognises a suspicious construction. An analyser traces a path to a fault. A sanitizer observes a forbidden operation at runtime. All four of these defects are perfectly well-formed programs performing entirely legal operations. There is nothing suspicious to recognise, no fault to reach, and no rule being broken. Each one simply means something other than what its author intended, and no tool can know what you intended.

What can know it is the way you write the code in the first place. Each of the four is prevented — not detected, prevented — by a habit that takes no runtime check, costs nothing at execution, and can be adopted in an afternoon. This chapter is those habits.

2. Habit One: Initialise by Named Member

Chapter 10.2 introduced designated initialisers as a readability feature. They are considerably more than that.

Here is a structure and two initialisations of it that are, today, identical in meaning:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#include <stdio.h>

struct sensor { int id; int scale; int offset; };

int main(void) {
    struct sensor pos = { 7, 100, -3 };
    struct sensor nam = { .id = 7, .scale = 100, .offset = -3 };

    printf("positional: id=%d scale=%d offset=%d\n", pos.id, pos.scale, pos.offset);
    printf("named     : id=%d scale=%d offset=%d\n", nam.id, nam.scale, nam.offset);
    return 0;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
positional: id=7 scale=100 offset=-3
named     : id=7 scale=100 offset=-3

Now someone — possibly you, six months from now, possibly a colleague tidying a header — swaps two members. Perhaps to group them more logically, perhaps to improve the padding the way Chapter 10.5 showed. It is a one-line change in a different file from the one above, and it looks completely harmless:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
struct sensor { int id; int offset; int scale; };   /* scale and offset swapped */

Nothing else changes. The same program, recompiled with -Wall -Wextra -Wpedantic -O2:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
positional: id=7 scale=-3 offset=100
named     : id=7 scale=100 offset=-3

The positional initialiser now puts the offset in the scale and the scale in the offset. It compiled without a single diagnostic, at any warning level, because it is a completely valid initialisation — it initialises the second member with 100 and the third with -3, exactly as it is written to do. The program's meaning changed when the structure was edited, and the braces had no way to object, because the braces never named anything.

The named initialiser is unaffected. It does not say "the second member", it says .scale, and .scale is still .scale wherever it has been moved to.

The habit: in an initialiser for any structure with more than one member, name every member. It is not a style preference. It converts a position-dependent construction, which silently follows a declaration you may not have seen, into a name-dependent one, which cannot.

There is a second, quieter benefit. Any member you omit from a designated initialiser is initialised as if it had static storage duration — zero for arithmetic members, null for pointers — which Chapter 9.12 established is the opposite of the indeterminate value an uninitialised automatic object carries. So { .id = 7 } is a fully defined object with a defined scale and offset, not a partly initialised one.

3. Habit Two: Never Hide a Pointer Inside a typedef

Chapter 10.9 established what typedef does: it introduces a name for a type. Not a macro, not a textual substitution — a genuine type name, subject to the ordinary rules of declaration syntax. That last part is what makes the following possible.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#include <stdio.h>

typedef char *string;

static void shout(const string s) {   /* reads as "a const string" */
    s[0] = 'X';
    printf("%s\n", s);
}

int main(void) {
    char buf[] = "hello";
    shout(buf);
    printf("%s\n", buf);
    return 0;
}

The parameter is declared const. The function writes through it. Compile with every flag in 13.7's set, including -Wcast-qual and -Wwrite-strings, which exist specifically to police qualifier discipline:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Xello
Xello

No diagnostic, and the caller's buffer has been modified. The function declared itself read-only and mutated its argument.

The reason is a rule from 10.9 that is easy to read past. When a qualifier is applied to a typedef name, it qualifies the type the name stands for, as a whole — it is not pasted into the middle of the declaration the way a macro would paste it. string is "pointer to char". Qualifying that whole type gives "const pointer to char" — char *const. The pointer is constant; the characters it points at are not. The const has protected the one thing nobody wanted to change.

You do not have to take that on trust. Force the compiler to name the type:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
typedef char *string;
static void f(const string s) { char **p = &s; (void)p; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: initializing 'char **' with an expression of type 'const string *'
         (aka 'char *const *') discards qualifiers
         [-Wincompatible-pointer-types-discards-qualifiers]

aka 'char *const *'. The compiler has known all along; it simply had no reason to mention it, because char *const is a perfectly ordinary type and the assignment to s[0] is a perfectly ordinary write through a non-const char *.

The habit: do not typedef a pointer type. Write char * and struct sensor * and const char * in full. Then const char *s means what it reads as, and the reader of any declaration can see the level of indirection without going to find the typedef.

4. Habit Three: Give a Multi-Statement Macro One Statement's Shape

Chapter 11.6 established that a function-like macro is a textual replacement performed before the compiler sees any syntax at all, and its §5 named this consequence the statement trap. What is added here is the measurement and the habit that closes it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#include <stdio.h>

#define LOG_AND_COUNT(msg)     printf("%s\n", msg); count++
#define LOG_AND_COUNT_OK(msg)  do { printf("%s\n", msg); count++; } while (0)

static int count = 0;

int main(void) {
    int failed = 0;                      /* nothing failed */

    if (failed)
        LOG_AND_COUNT("failure");
    printf("count after guarded-looking macro: %d\n", count);

    count = 0;
    if (failed)
        LOG_AND_COUNT_OK("failure");
    printf("count after do/while(0) macro   : %d\n", count);
    return 0;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
count after guarded-looking macro: 1
count after do/while(0) macro   : 0

Nothing failed, and the failure counter reads one.

After the preprocessor has run, the first if is this:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (failed)
    printf("%s\n", "failure");
count++;

The if governs one statement, because that is what an if without braces does. The printf is correctly suppressed. The count++ was never inside the if at all — it is the next statement in main, and it runs every time control reaches it. The indentation says otherwise and the indentation is a lie, because the preprocessor does not read indentation.

Compiled with -Wall -Wextra -Wpedantic the program is silent. There is no suspicious construction here: two statements in a row is the most ordinary thing in C, and the compiler has no record of the fact that the author believed they were one.

The habit: a function-like macro that expands to more than one statement is wrapped in do { ... } while (0).

The idiom looks like a loop and is not one: the body runs exactly once and while (0) is false, so there is no iteration. What it buys is syntax. do { ... } while (0) is a single statement, so it can be the entire body of an if with no braces — and because it is a do statement rather than a bare block, it requires the semicolon the call site already writes, so MACRO(x); is correct and MACRO(x) is a syntax error. A bare { ... } gets the first property and fails the second, which is why it breaks the else in if (c) MACRO(x); else ....

The measured difference is in the output above: one count versus zero, from the same conditional, with the same compiler and the same flags.

5. Habit Four: Build Every Configuration You Ship

Chapter 11.7 introduced conditional compilation and made the point that the preprocessor deletes code before the compiler sees it. Here is what that costs.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#include <stdio.h>

#ifndef ENABLE_RETRY
#define ENABLE_RETRY 0          /* default off; the build may define it as 1 */
#endif

static int attempt(void) { return 1; }

int main(void) {
    int ok = attempt();
#if ENABLE_RETRY
    for (int i = 0; i < 3 && !ok; i++)
        if (ok = attempt() == 1)      /* intended ==, wrote = */
            break;
#endif
    printf("ok = %d (retry %s)\n", ok, ENABLE_RETRY ? "on" : "off");
    return 0;
}

The default build, with the full recommended flag set:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cc -std=c17 -Wall -Wextra -Wpedantic -O2 retry.c -o retry
$ ./retry
ok = 1 (retry off)

Clean. Now the configuration that ships to the other half of your users — the same file, the same compiler, the same flags, one -D added:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cc -std=c17 -Wall -Wextra -Wpedantic -DENABLE_RETRY=1 -O2 retry.c -o retry
retry.c:13:16: warning: using the result of an assignment as a condition
               without parentheses [-Wparentheses]
   13 |         if (ok = attempt() == 1)      /* intended ==, wrote = */
      |             ~~~^~~~~~~~~~~~~~~~
retry.c:13:16: note: use '==' to turn this assignment into an equality comparison
1 warning generated.

The defect was always there. It was never compiled, so it was never diagnosed. Every warning flag in Module 13, every analyser pass, every sanitizer — none of them can instrument a token sequence the preprocessor discarded. Code inside a dead #if is not code as far as the toolchain is concerned; it is a comment that happens to be syntax-highlighted.

The habit: every configuration you ship is a configuration you build. If ENABLE_RETRY can be 0 or 1 in the field, both must be compiled, with warnings on, before release. If there are three such switches there are eight configurations, and that is an argument for having fewer switches, not for building fewer of them.

Where it can be arranged, a better habit still: prefer a runtime if to a compile-time #if. A branch the compiler can see is a branch it can check, and an optimiser will delete a branch on a compile-time constant just as thoroughly as the preprocessor would — the difference is that it type-checks and warns about the code first.

6. Two Habits That Remove Whole Classes

The four above each remove one specific defect. Two more remove entire categories, and they are the reason Modules 7 and 9 were as long as they were.

Carry a pointer and its length together. Chapter 7.7 established that an array decays to a pointer when passed to a function and that the length does not travel with it. A function given only a pointer cannot know how many elements it may read, so it must assume — and an assumption that is not checked is 13.6's entire bug class.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static int sum_bare(const int *a) {
    int t = 0;
    for (int i = 0; i < 8; i++) t += a[i];   /* 8 is an assumption */
    return t;
}

static int sum_span(const int *a, size_t n) {
    int t = 0;
    for (size_t i = 0; i < n; i++) t += a[i];
    return t;
}

Called with a five-element array, sum_span is correct by construction and sum_bare reads three elements past the end. Under AddressSanitizer at -O1, the level 13.10 recommends:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR: AddressSanitizer: stack-buffer-overflow
READ of size 4 at 0x00016f392674 thread T0
    #0 in main sum.c:9

Caught — at -O1. The identical program built at -O2 and run under the identical sanitizer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sum_span: 15
sum_bare: 15

Silence. This is not a reason to distrust sanitizers; it is a reason to use the build 13.10 specified rather than an arbitrary one. But it is a precise illustration of the difference the habit makes. sum_bare is a bug whose detection depends on which optimisation level you happened to choose. sum_span is not a bug at any optimisation level, under any tool, on any input, because the loop bound is the array's own length rather than a guess about it.

Give every allocation one documented owner. Chapter 9.8 stated the rule: for each allocation, exactly one piece of code is responsible for freeing it, and that responsibility is written down in the header next to the function that hands the pointer out. Leaks, double frees and use-after-free are three symptoms of the same missing sentence — nobody recorded who frees this. When the owner is named, the owner frees it once, and all three disappear together.

Six habits against the defect each one removes and the chapter that measured it. Initialise by named member removes every initialiser silently permuted, measured in chapter 10.2. Never hide a pointer in a typedef removes const qualifying the pointer rather than the pointee, measured in 10.9. Wrap a multi-statement macro in do-while-zero removes the second statement running unconditionally, measured in 11.6. Build every configuration you ship removes a dead branch rotting undetected, measured in 11.7. Those first four are the defects chapter 13.1 measured no tool catching. One documented owner per allocation removes leaks, double frees and use-after-free together, from module 9. Carry a pointer and its length together removes the entire out-of-bounds class, from chapters 7.7 and 13.6.The habitWhat it removesMeasured inInitialise by namedmemberevery initialisersilently permuted10.2no tool catches itNever hide a pointer ina typedefconst qualifying thepointer, not the pointee10.9no tool catches itdo { ... } while (0)the second statementrunning unconditionally11.6no tool catches itBuild everyconfigurationa dead branch rottingundetected11.7no tool catches itOne documented ownerleaks, double frees anduse-after-free togetherModule 9Pointer and lengthtogetherthe entire out-of-boundsclass7.7 · 13.612
Figure 1 — six habits, the defect each one removes, and where this curriculum measured that defect. The top four blue rows are the defects Chapter 13.1 measured and nothing caught: every warning flag, the static analyser, the sanitizers and even -Weverything are silent on all four, which is why the third column marks them 'no tool catches it'. They are not detected and then fixed; they are made unwritable. The bottom two green rows are different in kind — each removes an entire category rather than a single defect, which is why their third column names a whole module or a pair of chapters rather than one. Read the middle column as the answer to the only question that matters about a habit: what does adopting it actually take away?

7. The Misconception: Defensive Means Adding Checks

Ask a room of C programmers for a defensive habit and someone will offer this one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (p) free(p);

It is the best-known defensive idiom in the language. It is also worth taking apart, because it is wrong in two independent ways and measuring both is more useful than being told.

It guards against something that was never a problem. C17 §7.22.3.3 paragraph 2 says that if the argument to free is a null pointer, no action occurs. free(NULL) is a defined no-op — not undefined behaviour, not implementation-defined, simply nothing. The check cannot prevent a fault on the case it tests for, because that case has no fault.

It does not prevent the thing it is folklore for preventing. Here is the double free it is supposed to stop:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int *p = malloc(sizeof *p);
if (p == NULL) return 1;
*p = 1;

if (p) free(p);
printf("first free done, p is %s\n", p ? "non-NULL" : "NULL");
if (p) free(p);                      /* the same check passes again */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
first free done, p is non-NULL

free does not modify its argument — it cannot, since it receives the pointer by value. After the first call, p still holds the same address it always did. The guard tests whether p is null; p is not null; the second free runs. The idiom contributed nothing except the appearance of care.

The habit that works is the one from 9.8, and it is one line shorter than it looks:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
free(p);
p = NULL;

Now a second free(p) is free(NULL), which the standard defines as doing nothing. Verified under AddressSanitizer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
two frees, no fault: p is NULL

The general shape is what matters. The check-based version tests for a bad state that it cannot fix. The habit-based version makes the bad state unreachable: after the owner releases the object, the owner's pointer no longer denotes it. Defensive programming that works is almost always of the second kind. It removes the possibility; it does not inspect for it.

8. What Is Not on the List

A short list is only useful if things were left off it. These were considered and are not habits this chapter recommends.

Yoda conditions — writing if (0 == x) so that a mistyped = fails to compile. The defect it targets is real, but -Wparentheses is in -Wall and catches it, as §5's output shows, and it catches the cases Yoda ordering cannot — if (x = y) with two variables is unaffected by reordering. The habit costs readability at every comparison to duplicate a warning you already have switched on.

Hungarian notation — encoding a type in each identifier. It describes what the declaration already states, and when the declaration changes and the name does not, the name becomes a lie that reads like documentation.

A NULL check before every dereference. If a pointer can legitimately be null the check is not defensive, it is the logic. If it cannot, the check implies a possibility the reader must now rule out, and it hides the real failure — which is that the pointer arrived null, somewhere upstream — behind a silent early return. Chapter 13.8's assert is the tool for "this cannot be null and I want to be told if it is".

The pattern across all three: a habit earns its place by removing a defect that nothing else removes. Duplicating a warning flag, restating a declaration, or testing a condition that cannot occur all add text without removing anything. Every habit in §2 through §6 eliminates something this curriculum measured no tool catching, or an entire category at once.

9. What You Should Be Able to Say Now

  1. Why the four outstanding defects are outstanding. They are well-formed programs performing legal operations that mean something other than what the author intended. Warnings recognise suspicious constructions, analysers trace paths to faults, sanitizers observe forbidden operations — and none of those is what these are. -Weverything had one warning for each, about the include path.
  2. Name every member in an initialiser. Positional initialisers silently follow a declaration you may not have seen; { 7, 100, -3 } became scale=-3 offset=100 from a one-line edit to a structure in another file, with no diagnostic at any level. Omitted designated members are zero-initialised, not indeterminate.
  3. Never typedef a pointer. A qualifier applied to a typedef name qualifies the whole type, so const string is char *const — a constant pointer to mutable characters. The compiler will tell you this if you force it: aka 'char *const *'.
  4. Wrap a multi-statement macro in do { ... } while (0). It makes the expansion a single statement that still requires the caller's semicolon. Without it, if (failed) guarded the printf and not the count++, and the counter read one when nothing had failed. Better still, write a static function in a header unless you genuinely need the preprocessor.
  5. Build every configuration you ship. -DENABLE_RETRY=1 surfaced a -Wparentheses warning the default build could not, from the same file with the same flags. No tool can instrument tokens the preprocessor discarded. Guard a configurable default with #ifndef, or the build system cannot override it.
  6. Carry a pointer with its length; give every allocation one documented owner. The bare-pointer overread was caught at -O1 and missed at -O2; the pointer-and-length version is not a bug at any level. Naming the owner removes leaks, double frees and use-after-free together.
  7. if (p) free(p) is not defensive. free(NULL) is already a defined no-op (C17 §7.22.3.3), and free does not modify its argument, so the guard passes a second time and the double free proceeds. free(p); p = NULL; removes the possibility instead of testing for it.
  8. A habit earns its place by removing something. Yoda conditions duplicate -Wparentheses; Hungarian notation restates the declaration; a null check for a pointer that cannot be null hides the real failure. Prevention has no detection ceiling, because a bug that cannot be expressed does not need to be found.

10. Exercises

  1. Write a four-member structure and initialise it positionally. Swap two same-typed members in the declaration, recompile with -Wall -Wextra -Wpedantic, and confirm the silence. Convert the initialiser to designated form and repeat the swap.
  2. Take the typedef char *string; example and write the version that does what the author intended — a function that genuinely cannot modify its argument. Then explain, without compiling, what const string *p means.
  3. Write a macro that expands to two statements and call it as the body of an if whose condition is false. Show the defect, then fix it with do { } while (0). Then write the static function that makes the macro unnecessary and compare the disassembly at -O2.
  4. Put a deliberate -Wparentheses defect inside an #if FEATURE block that defaults off. Confirm the default build is clean. Then write a shell loop that builds every combination of two such switches and fails if any build warns.
  5. Reproduce §7's measurement. Build the double-free program at -O0 and -O1 under AddressSanitizer in a single file and record which reports it. Then split malloc/free into a second translation unit and build at -O1. Explain the three results.
  6. Take any function you have written in Module 12 that receives a pointer. Determine whether its length travels with it and whether its owner is documented. Fix whichever is missing.
  7. Pick one habit from this chapter that you did not already follow. Apply it to one file you have written in Part I. Record whether it found anything — a truthful "it found nothing" is a valid result and worth having.

11. Module 13 in One Page

Module 13 asked one question — how do you know a C program is correct? — and answered it by measurement rather than assertion. The same fifteen defective programs were carried through every chapter, and each chapter moved the number:

LayerCaught of 15
-Wall -Wextra -Wpedantic1
+ 13.7's recommended flags4
+ the static analyser9
+ AddressSanitizer and UndefinedBehaviorSanitizer11
design and review onlythe remaining 4

Around that spine the module established what the categories actually mean: undefined behaviour is a property of the program and not of the output you observed (13.2), and it is genuinely distinct from unspecified and implementation-defined behaviour (13.3); signed overflow is undefined while unsigned wraparound is defined (13.4); a signed value compared with an unsigned one is converted, not compared as written (13.5); an out-of-bounds write has no defined victim, and the neighbours it appeared to hit moved between -O0 and -O2 (13.6). Then the instruments: warnings are diagnoses to read rather than counts to total (13.7), assert records what you believe and is compiled out of the shipping build (13.8), a debugger answers what is true now where a printf answers only what did I think to ask (13.9), and sanitizers see what no static tool can but only on the inputs your tests actually reach (13.10).

And the four that nothing saw, which this chapter removed by making them unwritable.

The single most useful sentence in the module is 13.1's, and everything since has been evidence for it: the compiler accepting your program is a statement about its syntax, not about its meaning.

12. The End of Part I

This is the last chapter of Part I. One hundred and twenty-three chapters ago, Module 1 opened with what a program is and what a compiler does to one, and the question it set up has been the same one all the way through: what does this code actually do?

Part I answered it for the language. Values and their representation (M2), expressions and the conversions hiding inside them (M3), control flow (M4), functions and decomposition (M5), arrays (M6), pointers (M7), strings as the arrays they really are (M8), object lifetime and the heap (M9), the types you define yourself (M10), the multi-file program and what the linker does with it (M11), the standard library and the program's interface to the world (M12), and finally what correctness means and how it is established (M13).

What should have changed is the habit of mind. At the start, "it compiled and printed the right answer" was evidence. It is not, and you now have the measurements to say why: a program can be undefined and print the expected value, a sanitizer can be silent because the optimiser deleted the bug, a defect can sit in a configuration nobody built. Correct C is not code that happens to work on the run you tried. It is code you can give an account of.

You can also now read C you did not write — which, for most of a career, is the larger part of the job.

Part II changes the question. Part I asked what the language guarantees. Part II — C for SoC Verification, VLSI Engineers and DPI-C — asks what a value has to be when something outside your program has already decided its shape.

Module 14, C Through a Hardware Engineer's Eyes, makes that shift precisely. A value stops being an abstract quantity of a type you chose and becomes a bit pattern that a register, a bus or a reference model defined before your code existed. The width is no longer yours to pick, so Chapter 14.2 replaces "an int is fine" with the exact-width types; byte order stops being invisible the moment a value crosses a bus, a file or a language boundary (14.3); the alignment rules Chapter 10.5 taught as a layout curiosity acquire a cost and a hard edge, because converting a char * into a wider object pointer is implementation-defined rather than ordinary C (14.4); the structures of Module 10 are re-examined for when they may and may not be treated as a wire format (14.5); and the pointer model of Module 7 is laid over a real address map (14.6), which is the groundwork for the memory-mapped registers of Module 16 and the volatile of Module 17.

It closes on 14.7 — representing packet and transaction payloads as byte buffers with the length carried explicitly. That is §6 of this chapter, arriving as a requirement rather than a recommendation.

Nothing you have learned becomes wrong. The object model from Module 9, the representation rules from Module 2, the padding rules from Module 10, and Module 13's refusal to accept a working run as evidence are exactly the foundation Part II builds on, which is why Part II never repeats them. Reasoning about hardware without first being able to reason about the language is how people end up explaining real bugs with folklore — and §7 of this chapter is a small demonstration of what folklore costs.

Part I was the language. Part II is the language where the hardware has already decided.

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.