Learning objectives
After this chapter you should be able to:
Do unaided:
- Write C that reads and writes text and binary files, checks every
fopen,fgets,fread,fwriteandfclose, and tells a clean end of file from a short read and from an I/O error. - Compute
&,|,^,~,<<and>>on the station's flag byte by hand, and explain which shifts are undefined. - Predict, byte by byte, the hex dump of a little-endian binary log written field by field.
- Declare and call a function pointer, write a
qsortcomparator, and dispatch readings through a table of handlers. - Explain what an opaque type hides from its callers, and what hiding it costs.
May delegate:
- Explanations of file I/O error messages and of a hex dump, to the tutor, after you have written your own reading first.
Opening case
A composite case, not a real student or course.
At 06:13 on a cold morning the field station lost power while writing its log. Sam found a day.bin of 31,262 bytes instead of the usual 120,968. The logger had restarted on its own an hour later and begun a fresh file, so only this one was damaged. The week before, they had asked an agent for a program that reads the binary log and prints each reading, and it had worked on every file they tried. On this file it printed 1,488 readings that looked right, then this:
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.
Predictthe eight results of Listing 9.15 and the first 29 bytes of
one.bin, on paper, with confidence.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 8 · Predict in Chapter 10 →
Write
stn_pack_flags(Listing 9.4), then the lab reader and its handlers, by hand.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 8 · Write in Chapter 10 →
Specifythe opaque
sensor_db.hwith its ownership comments, thestn_logstatus enum, and the question you put to the tutor about the dump.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 8 · Specify in Chapter 10 →
Delegatethe tutor explains one dump or
errnomessage you have already annotated, and writes no code you submit.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 8 · Delegate in Chapter 10 →
Audittruncated-file tests, the dump checked with
od -j -N, ASan and UBSan on every build, Valgrind on your reader, and Listing 9.14's compile error.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 8 · Audit in Chapter 10 →
Defendwhy the struct is hidden, why the reader checks the count, what the file looks like when the power fails.
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 8 · Defend in Chapter 10 →
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.
Unchecked fopen, fread, fclose; while (!feof(f))
How to catch it Files truncated mid-record and at a record boundary; ASan; Valgrind --track-origins=yes
Section 9.3.1 · AddressSanitizer, Valgrind
Shifting a promoted byte past the width of int
How to catch it UBSan; one test value above 2³²
Section 9.3.2 · UndefinedBehaviorSanitizer, Tests
Exposing the representation through an accessor that returns the internal array
How to catch it A test that crosses a growth boundary under ASan; a lifetime comment on every pointer the header hands out
Section 9.3.3 · AddressSanitizer, Tests, Timing and log-log plots
What the chapter covers
- 9.1 The concept, by hand
- 9.1.1 Streams and how they fail
- 9.1.2 Text lines,
errnoandperror - 9.1.3 The short read
- 9.1.4 Bits, masks and the station's flags
- 9.1.5 Shifts and the width of a type
- 9.1.6 Endianness and a portable binary log
- 9.1.7 Function pointers and callbacks
- 9.1.8 Opaque types and abstract data types
- 9.2 Predict and check
- 9.3 What agents get wrong here
- 9.3.1 Unchecked I/O
- 9.3.2 Shifts past the width
- 9.3.3 Exposing the representation
- 9.4 Working with the agent
- 9.5 Lab: The station reads its log
- 9.6 Defend it
Cost thread
This chapter counts bytes and time. A station day is 4 sensors × 1,440 minutes = 5,760 readings. The binary log is 8 + 21 × 5,760 = 120,968 bytes, and record k sits at offset 8 + 21k, reachable without reading the records before it.
Summary
- Each file function fails its own way:
NULLfromfopenandfgets, a smaller count fromfreadandfwrite,EOFfromfclose. Ignore them and the program runs on bytes it never read. - Classify every short read:
feofandferrorseparate end from failure, a byte count separates a clean end from a partial record, and the header count catches a cut at a record boundary. - Set with
|, clear with& ~, toggle with^, test with&. Promotion makesf << 5352 and~f−12 forf = 0x0B; one undefined shift gave three answers on four builds. - A portable binary format is fixed on paper and written field by field with shifts: little-endian, 8 + 21n bytes, no padding.
- Function pointers let general code call specific code: a
qsortcomparator, a dispatch table, a visitor withvoid *ctx. - An opaque header keeps the representation secret, and the compiler enforces it; an accessor that hands out the internal array, with no stated lifetime, breaks the wall.
Key terms
- stream: a
FILEobject through which C reads or writes a file, with its own buffer and end-of-file and error indicators. - short read: a read that returns fewer items or bytes than requested, because of end of file or an error.
errno: an integer that library functions set on failure, meaningful only right after a call that reported failure.- mask: a value whose one bits mark the positions an operation tests, sets, clears or extracts.
- integer promotion: the conversion of a narrow integer such as
uint8_ttointbefore arithmetic or shifting. - endianness: the order of a multi-byte value's bytes; little-endian stores the least significant byte first, big-endian the most significant.
- function pointer: a variable that holds a function's address and can call it.
- callback: a function passed to other code so that the other code can call it.
- comparator: a callback that compares two elements and returns a negative, zero or positive result.
- dispatch table: an array of function pointers indexed by a kind or code, used to choose a handler.
- opaque type: a struct declared in a header without its fields, so callers can hold pointers to it but not see inside.
- abstract data type (ADT): a type defined by its operations and their promises, not by its representation.
Companion code
24 files for this chapter, compiled with the book's flags.
Read the code Download ch09.zip
Connections
Where this chapter and another point to each other most often.
- Chapter 2: The Working Loop
Chapter 2's trust ledger rates agents' claims about runtime errors well below their claims about compile errors (about 75% against about 90% accurate in one first-year C course; Taylor et al. 2024), and all three traps are runtime traps. - Chapter 5: Arrays, Strings, and Matrices: n, n², n³
Chapter 5 introducedfgetsas the bounded way to read a line. - Chapter 7: Structs, Headers, and Multi-File Programs
Chapter 7'sstation.hnames four with#define(Figure 9.1):STN_FLAG_VALID 0x01,STN_FLAG_CALIBRATED 0x02,STN_FLAG_LOW_BATTERY 0x04andSTN_FLAG_OUT_OF_RANGE 0x08. - Chapter 8: Growable Arrays and Linked Lists: The Cost of Growth
Any file could readdb->countor walkdb->rows, and the day you change the representation, to Chapter 8's linked list or Chapter 18's hash table, every such file breaks. - Chapter 10: Complexity and Sorting: Your First Delegation
Keeping equal elements in input order is a property Chapter 10 calls stability. ### 9.1.8 Opaque types and abstract data types Chapter 7 put onlystruct stn_db;insensor_db.hand kept the fields (name,rows,countandcapacity) insensor_db.c, so that the layout had one owner. - Chapter 11: Specify First: Abstract Data Types
Chapter 11 turns such comments into full specifications. - Chapter 12: Moving to C++: Is It Really C++?
From Chapter 12: This is the third meaning of&: in&tit takes an address (Chapter 4), ina & bit is bitwise AND (Chapter 9), and after a type in a declaration it makes a reference.
Further reading
- Parnas (1972): the argument for hiding each likely-to-change decision behind an interface; short, and the root of Section 9.1.8.
- Liskov and Zilles (1974): the original statement of abstract data types as types defined by their operations.
- Kernighan and Ritchie (1988), Chapter 7 and Appendix B: the standard I/O functions and their return values, from the language's authors.
- Wang et al. (2013): real cases of compilers using undefined behavior, shifts included, to remove code programmers thought was safe.