Before you start
Open only what you need; each one is a short refresher.
Words used in this lesson
The lesson assumes you can read an assignment, a function call and return, an if and zero-based array indexing.
- Objects and addresses
- An object is a region of storage that holds a value, and it occupies consecutive bytes.
- An object's address is the number of its first byte; addresses are printed in hexadecimal (base 16), where each digit stands for four bits.
- Two pointer values that hold the same address are aliases of one object.
- Names and time
- A name's scope is the part of the program where the name can be used.
- An object's lifetime is the stretch of the run during which the object exists.
- Valid and invalid accesses
- A store or load is valid only when a live object of a suitable type sits at that address and the location is inside it.
- Undefined behavior means the C standard places no requirement on what happens; a crash is one possible result, and so is a program that appears to work.
Sizes on the machine used here
For this x86-64 Linux illustration (the LP64 model), a byte has 8 bits, an int occupies 4 bytes and each pointer used here occupies 8 bytes. C guarantees sizeof(char) == 1, but the int and pointer sizes are target-specific, so another target may differ.
1. Which object does a store change? (original example using book §2.1–2.4)
Here is the program the whole lesson follows. It asks for a block of seven int slots, writes two readings into it through a helper, counts them, prints them and gives the block back.
Three pieces of notation carry the whole lesson. In a declaration such as int *slot, the star declares slot as a pointer to an int. In an expression such as *slot = reading, the star follows the address already stored in slot and writes the int found there. &filled produces the address of filled. NULL is a pointer value that addresses no object and must never be followed.
The block that malloc returns has no name; this lesson calls it block, so block[0] is a label for its first element, not C code. sizeof *entries is the size of the object entries points to, an int, so the request is 7 × 4 = 28 bytes; sizeof does not evaluate *entries, so nothing is read through the not-yet-initialized pointer.
#include <stdio.h>
#include <stdlib.h>
#define LOG_CAPACITY 7
static int samples_logged = 0; /* static storage: exists for the whole run */
/* Writes one reading into the element `slot` addresses and bumps the caller's count.
Returns 1 if the reading was stored, 0 if the log is already full. */
static int record_reading(int *slot, int reading, int *count)
{
if (*count >= LOG_CAPACITY)
return 0;
*slot = reading; /* store through a copied address */
*count = *count + 1; /* store into the caller's object */
slot = NULL; /* changes this call's parameter only */
return 1;
}
int main(void)
{
int *entries = malloc(LOG_CAPACITY * sizeof *entries);
if (entries == NULL)
return 1; /* no block: nothing to log into */
int filled = 0; /* automatic storage: exists while main runs */
int *cursor = entries; /* a second handle to the same block */
record_reading(cursor, 23, &filled);
record_reading(entries + filled, 61, &filled);
samples_logged = samples_logged + filled;
printf("%d %d %d\n", entries[0], entries[1], samples_logged);
free(entries); /* the block's lifetime ends here */
return 0; /* entries and cursor now hold indeterminate values; neither is used again */
}The line *slot = reading; is a store through a pointer: slot holds an address, and the store writes the object found at that address, not slot itself. In the first call slot holds the address of block[0], so the store writes 23 into those 4 bytes. Its storage column says where each object's storage comes from: static (the whole run), automatic (while its function runs) or allocated (from malloc until free); section 3 explains each. The figure highlights the row each event changes. The ledger gives each named object a row, reuses the helper rows for each call, and groups the five untouched elements as block[2..6]. Its events are snapshots of the run, and each column shows the state after its event; E12 appears only in Check mode, as a store after free that the program never executes. Step to the first store (E4) and check which row lights up.
On a narrow screen, swipe the figure sideways to reveal the rest of it; the explanation below it stays in view. In the ledger, read the object and value columns first, then check the highlighted event on the right.
2. Calls copy values, including addresses (book §2.3 and §2.4.4)
entries, cursor and, during the first call, slot are three separate pointer objects that hold the same address. Each call creates its own parameter objects, so call 2's slot is a different object from call 1's. A call copies each argument's value into the parameter, so slot receives a copy of cursor's address. Assigning to slot, as in slot = NULL;, changes only slot; cursor still holds the address of block[0]. Switch the figure above to Incorrect: assigning slot also moves cursor to see the wrong model and where it breaks.
The same rule lets the helper change an object it cannot name. record_reading receives &filled in its parameter count, so the store *count = *count + 1; changes filled in main, even though filled's name is out of scope inside the helper. Each call copies three values, the slot address, the reading and the address of filled; neither the block nor filled itself is copied. The helper's capacity test does not check either pointer: the caller must pass, in count, the address of a live, initialized int and, in slot, the address of a writable element of the block. Both calls here meet that obligation.
The second call passes entries + filled. Pointer arithmetic counts in elements, so with filled equal to 1 this is the address of block[1], 4 bytes past block[0] on this target (this element-offset explanation is added for our example; the book discusses access through a block's base address in §2.4.3). Indexing uses the same calculation: entries[i] accesses the same int as *(entries + i), so entries[1] later reads the element the second call wrote. An element access needs an index from 0 to 6 here.
3. Where each object's storage comes from, and how long it lives (book §2.1, §2.4.1)
The program uses three kinds of storage.
samples_loggedhas static storage: it exists for the whole run.entries,filledandcursorhave automatic storage: they exist whilemainruns, and each call's parameters exist only while that call runs.- The block has allocated storage:
malloccreates it, it has no name of its own, and it exists untilfree.
An automatic object's lifetime starts when its function's block is entered, not when its declaration is reached; until its initializer runs it has no value. On x86-64 Linux these kinds map to regions of the address space: static objects sit in the data region and allocated blocks on the heap, both near the program's own image (the 0x55… addresses in the figure, with the heap higher), while automatic objects sit in the running call's stack frame (the 0x7ffd… addresses) or in registers; all these addresses are illustrative; instructions sit in the code region and are not objects. Scope is a different question from lifetime: the name slot can be used only inside record_reading, while the block it points to outlives every call. The figures use an illustrated, unoptimized model; a compiler may keep a parameter in a register instead of memory, and code is instructions, not an object with a lifetime. In the figure, the extent cells are the object's own bytes, numbered from its first byte.
On a narrow screen, swipe the figure sideways to reveal the rest of it; the explanation below it stays in view.
4. Asking for memory and giving it back (book §2.4.2, §2.4.3)
malloc(LOG_CAPACITY * sizeof *entries) asks for 7 × 4 = 28 bytes and returns their address, or NULL when it cannot provide them; the program checks for NULL before using the block (the figure's Alternative: malloc returns NULL mode). The new block's contents are not initialized, so the program reads only the two elements it wrote. This example fixes the capacity at seven to keep the trace small; in general, allocation lets a program choose a block's size at runtime and keep the block alive independently of the function that requested it. free(entries) ends the block's lifetime. It is not required to clear the bytes, and the allocator may reuse some of them for its own records, so the old values cannot be relied on. free assigns nothing to entries or cursor, but C makes their values indeterminate: any use of either, even copying or comparing it, is undefined behavior. The program never uses them again. Assigning entries = NULL; after the free would make that pointer safe to test, but it does not repair cursor: cursor remains indeterminate and must not be used. Assigning NULL to one alias never repairs the others.
Two further rules complete the picture; they come from the C standard rather than from the sections read. free must receive the address malloc returned, while the allocation is still live: passing an interior address such as entries + 1, or freeing the block a second time through another alias, is undefined behavior, while free(NULL) does nothing. Conversely, returning from a helper or overwriting a pointer never frees the block: if the last usable pointer to it is lost before free, the block stays allocated and unusable for the rest of the run, which is a memory leak.
What this answers
For each assignment shown here, find the object its left side designates: the named variable itself or, for *p, the object at the address p holds. Then ask where that object's storage came from, and check that its lifetime has not ended before the store. Switch the ledger figure to Check: name the object and answer its four questions, the last one after free.
Sources and credits
- Book — Suzanne J. Matthews, Tia Newhall and Kevin C. Webb. Dive into Systems. diveintosystems.org, online edition version 1.2, 2020. Chapter 2, sections 2.1–2.4 (online edition)
- Sections read — This lesson focuses on pointer copies, stores through pointers and allocation lifetime, using ideas from sections 2.1 to 2.4 of the free online edition linked above. It is not a complete guide to those sections.
- Program and figures — The sensor-log program, its names and values, the event ledger and the object inspector were written for this lesson; no listing, example or drawing from the book is reproduced. Addresses are illustrative. Sizes are those of x86-64 Linux.
- Licence of the source — The book is published under the Creative Commons BY-NC-ND 4.0 licence. This lesson is an independent explanation of the ideas in those sections, not an adaptation of the text or figures.
The explanations, code and interactive drawings are original interpretations of the cited source. Rights in the source remain with its rightsholders. This is an independent explanation: the referenced authors, publishers and institutions have not endorsed it unless expressly stated.
© 2026 Justin Calderon. All rights reserved. Applies to original lesson material, subject to rights provided by law. See Sources and rights or rights.txt. Request a correction or removal.