Part II · C and the Machine

07 Structs, Headers, and Multi-File Programs

TutorWeek 5Lab: Split the station databaseMemoryOwnershipInterfaces

What still holds

The C type cannot say who owns a pointer, so the header must.

The agent may: explain linker and make errors after you state a hypothesis.

Learning objectives

After this chapter you should be able to:

Do unaided:

  1. Define structs, nested structs and arrays of structs, and use . and ->.
  2. Predict a struct's sizeof and member offsets, draw its padding, and check the prediction with offsetof.
  3. Split a program into headers and sources with include guards, with declarations in headers and each definition in exactly one source file.
  4. Write an interface's ownership rules as comments in its header.
  5. Write a makefile with complete dependencies, and predict which files rebuild after a change.

May delegate:

  1. 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.

  1. PredictOn paper: offsets and sizeof of struct 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?

  2. 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?

  3. SpecifyThe makefile requirements: one rule per object, every header dependency, the book's flags, clean and test, 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?

  4. 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?

  5. Auditmake -n after every touch, against gcc -MM; offsetof and ptype /o against 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?

  6. 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?

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

  1. 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
  2. 7.2 Predict and check
  3. 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
  4. 7.4 Working with the agent
  5. 7.5 Lab: Split the station database
  6. 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 reading has 21 bytes of members and takes 32 on x86-64 gcc.
  • offsetof, ptype /o and _Static_assert turn 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 fwrite of 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; extern plus one definition prevents "multiple definition" across them, and nm shows 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 -MP and a touch-and-make -n test 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/#endif wrapper 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.

See it on the map

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.