Learning objectives
After this chapter you should be able to:
Do unaided:
- Define structs, nested structs and arrays of structs, and use
.and->. - Predict a struct's
sizeofand member offsets, draw its padding, and check the prediction withoffsetof. - Split a program into headers and sources with include guards, with declarations in headers and each definition in exactly one source file.
- Write an interface's ownership rules as comments in its header.
- Write a makefile with complete dependencies, and predict which files rebuild after a change.
May delegate:
- A tutor explanation of a linker or make error, after you have written your own hypothesis.
Opening case
A composite case, not a real student or course.
Sam wants the station's logger, a small 32-bit x86 board in the box on the pole, to save readings to a memory card for the laptop to pull once a day. The text log works, but it is bulky to store and slow to parse, and Sam, who always asks what the hardware has to carry, wants each reading saved as raw bytes. They ask the agent how.
The chapter continues from here.
The loop in this chapter
Open a step to see what it asks of you and how it plays out in the Tutor phase.
PredictOn paper: offsets and
sizeofofstruct reading, reordered and not; which objects rebuild after each header changes.Step 1 of 6 · You, no AI
What you hand in: A written prediction: the output, the memory state, the operation count, and how sure you are.
Why this step: A prediction written before the run is the only way to learn whether your model of the machine is right. A miss is the most useful result there is, because it shows exactly where the model is wrong.
The first prediction is never delegated.
Ask yourself: What will this print, what will memory look like when it ends, and how many operations will it take?
Log the prediction in your work log · Predict in every chapter · ← Predict in Chapter 6 · Predict in Chapter 8 →
WriteBy hand: the three headers, with guards and ownership comments.
Step 2 of 6 · You, no AI
What you hand in: One small piece written by hand, to build the mental model.
Why this step: You can only check what you could have built. Writing a small piece yourself gives you the model you will need to judge the agent's version later.
Ask yourself: Could I write this piece again tomorrow without notes?
Browse the companion code · Write in every chapter · ← Write in Chapter 6 · Write in Chapter 8 →
SpecifyThe makefile requirements: one rule per object, every header dependency, the book's flags,
cleanandtest, and the rebuild table it must pass; plus the question for the tutor about one linker error.Step 3 of 6 · You
What you hand in: The header or class interface, unit tests, a complexity budget and the ownership rules.
Why this step: The specification is the contract the code will be held to. Tests written first, a cost budget and ownership rules turn "looks right" into something you can check.
Tutor In the Tutor phase you specify a question instead of code: what you want explained, quizzed or drilled.
The ownership and lifetime section of a specification is never delegated.
Ask yourself: Who allocates, who frees, what does it cost, and what input would break it?
Prompt and specification templates · Specify in every chapter · ← Specify in Chapter 6 · Specify in Chapter 8 →
DelegateThe tutor explains one linker error after you state a hypothesis.
Step 4 of 6 · The agent
What you hand in: The implementation from your specification, or an explanation in the Tutor phase, with the prompts and transcript saved.
Why this step: The agent does the typing; you keep the judgment. Saving the prompts and the transcript makes the delegation something you can audit afterward.
Tutor Tutor phase: the agent explains, quizzes and makes practice problems. It writes no code you submit.
Ask yourself: Did I hand over my specification and tests, and did I save what the agent said?
Prompt templates · Delegate in every chapter · ← Delegate in Chapter 6 · Delegate in Chapter 8 →
Audit
make -nafter everytouch, againstgcc -MM;offsetofandptype /oagainst your layout; ASan and Valgrind.Step 5 of 6 · You and the tools
What you hand in: The Audit Report: tests, sanitizer and Valgrind output, your own cost derivation against a timing plot, the defects found, and a keep-or-rewrite decision.
Why this step: What the agent says is testimony until an instrument confirms it. Tests, sanitizers, Valgrind and a timing plot are evidence.
Tutor In the Tutor phase the audit checks the tutor's claims as well as your own code.
The final judgment that the code is correct is never delegated.
Ask yourself: What did the sanitizer report, does the timing plot match my derivation, and would I keep this code or rewrite it?
The Audit Report template · Audit in every chapter · ← Audit in Chapter 6 · Audit in Chapter 8 →
DefendThe ownership rules line by line, why each item sits in the header or the source, and who frees every string.
Step 6 of 6 · You, live
What you hand in: A five-minute oral defense.
Why this step: If you can explain it live, you own it. The oral checks that the understanding is yours and not the agent's.
Ask yourself: Why this bound, what input breaks it, and where is this memory freed?
How the loop works · Defend in every chapter · ← Defend in Chapter 6 · Defend in Chapter 8 →
What agents get wrong here
Patterns observed in agent-written code, to test for, not guarantees. If the agent no longer makes the mistake, your job is to prove it got it right, with the same instrument.
Makefile pattern rule %.o: %.c with no header dependencies
How to catch it touch each header, then make -n; compare with gcc -MM; use -MMD -MP
Section 7.3.1 · Build tools
Definitions in headers, or the static "fix" that gives each file a private copy
How to catch it Link two objects; nm shows one name defined twice
Section 7.3.2 · Build tools, Your own trace
Saving a struct with fwrite(&rec, sizeof rec, 1, f)
How to catch it od dump of two equal records; Valgrind --track-origins=yes; _Static_assert per target
Section 7.3.3 · Valgrind, Build tools
What the chapter covers
- 7.1 The concept, by hand
- 7.1.1 A struct groups named members
- 7.1.2
typedef, nested structs and arrays of structs - 7.1.3 Alignment and padding
- 7.1.4 Declarations and definitions
- 7.1.5 Headers and sources
- 7.1.6 Separate compilation and the linker
- 7.1.7 make: rebuild what changed
- 7.2 Predict and check
- 7.3 What agents get wrong here
- 7.3.1 Missing header dependencies in makefiles
- 7.3.2 Definitions in headers
- 7.3.3 Writing a struct to a file with
fwrite
- 7.4 Working with the agent
- 7.5 Lab: Split the station database
- 7.6 Defend it
Cost thread
This chapter counts bytes: struct size times record count. Four sensors at one reading a minute make 4 × 1,440 = 5,760 readings a day.
Summary
- Members sit in order, each at a multiple of its alignment, and the size rounds up to the struct's alignment:
struct readinghas 21 bytes of members and takes 32 on x86-64 gcc. offsetof,ptype /oand_Static_assertturn a layout prediction into evidence. Reordering gives 24 bytes, about 17 MB a year less for the station.- Padding holds unspecified bytes and layout belongs to the target's ABI, so
fwriteof a struct writes leftover memory in a layout another machine may not share. - Headers hold types, declarations and ownership rules; sources hold definitions. Include guards prevent double inclusion within one translation unit;
externplus one definition prevents "multiple definition" across them, andnmshows which side of a linker error failed. - A makefile that omits header dependencies builds correctly from clean and later links objects that disagree about a struct.
-MMD -MPand atouch-and-make -ntest catch it.
Key terms
- struct: a type grouping named members, possibly of different types, into one object.
- member: a named part of a struct, accessed with
.or, through a pointer,->. - designated initializer: an initializer that names the members it sets, such as
.value = 21.75; the rest become zero. - typedef: a declaration giving an existing type another name.
- alignment: the number a type's address must be a multiple of on a platform.
- padding: unnamed bytes inserted in a struct to keep members aligned.
- offsetof: the
<stddef.h>macro giving a member's byte offset in its struct. - ABI (application binary interface): a platform's rules for type sizes, alignment and layout.
- declaration: a statement that a name exists, with its type.
- definition: a declaration that also reserves storage or supplies a function body.
- one-definition rule: each function and global variable has exactly one definition across a program.
- incomplete type: a struct declared without members, usable only through pointers.
- header: a file of types, declarations and interface rules, included by other files.
- include guard: the
#ifndef/#define/#endifwrapper that makes a header appear at most once per translation unit. - internal linkage: a file-scope name visible only in its own translation unit, given by
static. - external linkage: a file-scope name visible to the linker program-wide, the default.
- separate compilation: compiling each source file to its own object file, linking later.
- symbol: a name in an object file's table, marked as defined there or needed from elsewhere.
- undefined reference: a linker error for a symbol used but defined nowhere on the link line.
- multiple definition: a linker error for a symbol defined in more than one object file.
- make: a tool that rebuilds targets whose prerequisites have changed.
- target, prerequisite, recipe: in a rule, the file to build, the files it comes from, and the commands.
- phony target: a make target that names an action, such as
clean, rather than a file.
Companion code
8 files for this chapter, compiled with the book's flags.
Read the code Download ch07.zip
Connections
Where this chapter and another point to each other most often.
- Chapter 1: From Python to C: What Python Was Hiding
Treat a linker explanation as testimony: accept it provisionally, then confirm with a rebuild,nmormake -n. > Use the tutor prompt from Chapter 1. - Chapter 6: The Stack and the Heap: Dynamic Memory and Ownership
Chapter 6 named the patterns (caller owns, callee owns, borrowed); C cannot express which applies. - Chapter 8: Growable Arrays and Linked Lists: The Cost of Growth
Capacity is fixed; Chapter 8 teaches growth, and Chapter 9 makes this database grow. - Chapter 9: Files, Bits, and Function Pointers: ADTs Behind a Header
Chapter 9 builds abstract data types on this idea; here it gives the database's layout exactly one owner. ### 7.1.5 Headers and sources A header (.h) holds what other files need to use a module; a source file (.c) holds the definitions. - Chapter 12: Moving to C++: Is It Really C++?
From Chapter 12: The machine has not moved: Chapter 6's stack and heap, Chapter 7's struct layout and Chapter 5's row-major arrays all hold.
Further reading
- Kernighan and Ritchie (1988), Chapter 6 and Section 4.5: the original, compact treatment of structures and header files.
- ISO/IEC (2018), clauses 6.7.2.1, 6.2.6.1 and 6.9: the exact rules for layout, padding values and external definitions, for when an agent and a compiler disagree.
- Parnas (1972): the classic argument for dividing a program by the decisions each module hides, which is what a good header does.
- Drepper (2007), Sections 3 and 6: the hardware story behind alignment and struct size, for readers who want to know why padding exists.