Lesson 11 / 15
Memory Layout
A program's memory is not one single region; it is split into separate areas — automatic, static, and dynamic — and which region a block sits in decides from the start who its owner will be. This lesson lines up the four language observations side by side with the allocate-and-free observation and measures this distinction without printing an address.
Published Updated
In this lesson
- Shows how the automatic, static, and dynamic regions line up with the ownership classes
- Compares the four language observations with the allocate-and-free observation and measures that the counter difference only moves in the dynamic region
- Explains the region's relationship to ownership without printing region boundaries or alignment values
Contents
The Dynamic Memory topic’s previous four lessons stayed in the dynamic region of memory:
malloc, calloc, realloc, the ownership contract, the leak, the checkpoint — all of it
concerned blocks allocated at runtime and freed by hand. But since this course’s very first
lesson, every measurement has also carried the language class: local variables, static
areas, string literals. As it closes the topic, this lesson puts the dynamic region next
to the remaining three regions and shows how which region a block sits in decides from the
start who its owner will be.
Four Regions, One Question
A C program’s memory is split into several regions: the code itself, constant data, static variables, the runtime stack (for automatic variables), and the heap (for dynamic allocation). How these regions are laid out in hardware and how the boundaries between them are aligned was covered in the How Computers Work course (M01/K01); this lesson does not retell that layout, it only relates it to the ownership question. The question is always the same: who will free this block? The answer depends on which region the block sits in.
// counter.h — a wrapper that counts every allocation and every free #ifndef COUNTER_H #define COUNTER_H #include <stddef.h> void *count_alloc(size_t size); void *count_grow(void *old, size_t size); void count_free(void *block); int allocated_count(void); int freed_count(void); #endif
// counter.c — the counters live only in this unit
#include <stdlib.h>
#include "counter.h"
static int allocated = 0;
static int freed = 0;
void *count_alloc(size_t size) {
void *block = malloc(size);
if (block != NULL) {
allocated++;
}
return block;
}
void *count_grow(void *old, size_t size) {
int new_block = (old == NULL);
void *grown = realloc(old, size);
if (grown != NULL && new_block) {
allocated++;
}
return grown;
}
void count_free(void *block) {
if (block != NULL) {
freed++;
}
free(block);
}
int allocated_count(void) {
return allocated;
}
int freed_count(void) {
return freed;
}
// layout.c — four language observations against an allocate-and-free contrast
#include <stdio.h>
#include <string.h>
#include "counter.h"
static int fixed_static = 3;
static int diff(void) {
return allocated_count() - freed_count();
}
static void observe(const char *name, const char *class, const char *evidence) {
printf("%-30s%-10s%s\n", name, class, evidence);
}
int main(void) {
char buffer[64];
int before;
printf("%-30s%-10s%s\n", "observation", "owner", "evidence");
/* --- language: automatic region, reclaimed on its own when scope ends --- */
before = diff();
int local = 5;
int *p = &local;
snprintf(buffer, sizeof buffer, "*p %d, counter diff %d", *p, diff() - before);
observe("local variable", "language", buffer);
/* --- language: static region, sits in one place for the program's whole life --- */
int *s = &fixed_static;
snprintf(buffer, sizeof buffer, "*s %d, no free call", *s);
observe("static field", "language", buffer);
/* --- language: constant data region, immutable text --- */
const char *literal = "fixed";
snprintf(buffer, sizeof buffer, "length %d, no free call", (int)strlen(literal));
observe("string literal", "language", buffer);
/* --- language: an array living in the automatic region --- */
int array[4] = {1, 2, 3, 4};
int *first = array;
int *last = &array[3];
snprintf(buffer, sizeof buffer, "last - first %d, *last %d", (int)(last - first), *last);
observe("local array", "language", buffer);
/* --- caller: heap region, allocated by hand and freed by hand --- */
before = diff();
int *block = count_alloc(4 * sizeof *block);
int total = 0;
if (block != NULL) {
for (int i = 0; i < 4; i++) {
block[i] = i + 1;
total += block[i];
}
}
count_free(block);
snprintf(buffer, sizeof buffer, "total %d, counter diff %d", total, diff() - before);
observe("allocate and free", "caller", buffer);
return 0;
}
observation owner evidence local variable language *p 5, counter diff 0 static field language *s 3, no free call string literal language length 5, no free call local array language last - first 3, *last 4 allocate and free caller total 10, counter diff 0
DM16. In all four language observations, the counter never moved at all — local variable and local array sit in the automatic region, static field sits in the static
region, string literal sits in the constant data region; all three are reclaimed by the
program itself, by scope or by the program’s lifetime. The allocate and free
observation sits in the heap region and the counter difference again comes out 0 — but
this time the zero comes not from something the language does on its own, but from
count_free written to match count_alloc. The same result (diff 0) comes from two
different reasons: one is zero because it was never measured, the other is zero because it
was measured and balanced out.
The Region Determines the Owner, the Owner Does Not Determine the Region
Which region a block sits in is, most of the time, clear from the declaration itself:
writing int local = 5; inside a function places it in the automatic region, declaring it
at file scope or with the static keyword places it in the static region, writing a string
literal places it in the constant data region, and calling count_alloc (or malloc
directly) places it in the heap region. The ownership class derives from this
declaration: no block in the automatic or static region ever receives a free call, its owner
is always the language; every block in the heap region expects a free call, its owner is
the caller (or, as seen in the Deallocation Discipline lesson, the callee, per the
contract). The region alone is not enough — whether a block in the heap region belongs to
the caller or the callee is not told by the region itself, that is decided by a contract like
owner.h. But for the automatic and static region, the region alone is enough: no block
that lands there is ever freed by hand.
A Nested Scope Is Also in the Language Class
The four language observations above were all in main‘s outermost scope. But the
automatic region’s rule does not look at how deep the scope is — every { } block
inside a function body carries its own automatic lifetime, and a nested block is in the
language class just as much as the outer one. At the same time, it is also possible to show
that a block’s size growing on the dynamic side does not change its ownership class:
whether it is four elements or forty, every block returned by count_alloc still belongs to
the caller.
// nested.c — nested automatic scope against a growing heap block
#include <stdio.h>
#include "counter.h"
static int diff(void) {
return allocated_count() - freed_count();
}
static void observe(const char *name, const char *class, const char *evidence) {
printf("%-30s%-10s%s\n", name, class, evidence);
}
int main(void) {
char buffer[64];
int before;
/* --- language: an automatic variable inside a nested block --- */
int outer = 1;
{
int inner = 2;
{
int innermost = 3;
snprintf(buffer, sizeof buffer,
"outer %d, inner %d, innermost %d, counter diff %d",
outer, inner, innermost, diff());
observe("three nested scopes", "language", buffer);
}
}
/* --- caller: a heap block growing tenfold still belongs to the caller --- */
before = diff();
enum { SMALL = 4, LARGE = 40 };
int *small_block = count_alloc(SMALL * sizeof *small_block);
int *large_block = count_alloc(LARGE * sizeof *large_block);
int total = 0;
if (small_block != NULL && large_block != NULL) {
for (int i = 0; i < SMALL; i++) {
total += small_block[i] = i;
}
for (int i = 0; i < LARGE; i++) {
total += large_block[i] = i;
}
}
count_free(small_block);
count_free(large_block);
snprintf(buffer, sizeof buffer, "total %d, counter diff %d", total, diff() - before);
observe("small and large block", "caller", buffer);
return 0;
}
three nested scopes language outer 1, inner 2, innermost 3, counter diff 0 small and large block caller total 786, counter diff 0
DM17. In the three nested scopes observation, three variables were declared at three
separate { } depths, but all three are in the automatic region and none of them touched
the counter at all — the depth of the scope does not change whether the language class
applies. The small and large block observation allocates and frees two heap blocks with
sizes ten times apart (4 and 40 elements) side by side; the counter difference again comes
out to 0, but this time from two separate count_alloc/count_free pairs balancing
out. This tests the claim in the The Region Determines the Owner section against depth and
size: what decides whether a block is automatic or heap is neither how many scopes deep it
is nor how many bytes it holds — what decides it is only which declaration it came into
being through. int innermost is in the language class even at three scope depths;
count_alloc(40 * sizeof(int)) is in the caller class even in a single call.
Same Class, Different Persistence
No block in the language class is ever freed by hand — but this does not mean that every
region inside the language class behaves the same way. The automatic region starts
fresh on every call; the static region is persistent across calls. Both give the same
answer to the ownership question (owner is the language, no free call), but their lifetimes
are entirely different from each other.
// persistent.c — a static variable inside a function persisting across calls
#include <stdio.h>
#include "counter.h"
static int diff(void) {
return allocated_count() - freed_count();
}
static int keep_count(void) {
static int call = 0;
call++;
return call;
}
static int reset_locally(void) {
int value = 0;
value++;
return value;
}
int main(void) {
char buffer[96];
int before = diff();
int first = keep_count();
int second = keep_count();
int third = keep_count();
int automatic_first = reset_locally();
int automatic_second = reset_locally();
snprintf(buffer, sizeof buffer,
"static calls %d %d %d, automatic calls %d %d, counter diff %d",
first, second, third, automatic_first, automatic_second, diff() - before);
printf("%-30s%s\n", "persistent versus reset", buffer);
return 0;
}
persistent versus reset static calls 1 2 3, automatic calls 1 1, counter diff 0
DM18. The variable call inside keep_count is declared static, so it sits in the
static region and carries the same value across three calls: 1, 2, 3. The variable
value inside reset_locally sits in the automatic region, is redeclared on every call, and
starts from 0; both calls return 1, because the second call does not continue from
where the first left off. The counter difference is still 0, because no heap allocation
happened at all — this measurement shows not the ownership class but the lifetime
difference within the same class. The rule from the The Region Determines the Owner, the Owner Does Not Determine the Region section still holds here: the static keyword moves a
variable from the automatic region to the static region, and that move stays inside the
language class, which still requires no free call — but it changes persistence from the
ground up.
The Bounding Measurement
Where the boundaries between regions sit and which alignment value a block is placed
according to depend on the runtime environment; this lesson prints neither an address nor an
alignment value. The last - first difference came out to 3 — this is not the addresses
themselves but the element count between them, and it is a measure independent of the
region boundary, a portable one.
The automatic region’s size is not unlimited either — every call frame takes up a bounded
place in that region, and nested scopes (like the one in the three nested scopes
observation) or a deep recursion can exhaust that place. Crossing this bound is undefined
behavior (commonly known as stack overflow), and, following this course’s rule on
undefined behavior for the rest of the topic, this lesson does not run it — it only
states that it exists. The heap region’s bound shows up in a different form: when
count_alloc cannot meet the requested size, it does not fail silently, it returns NULL
in keeping with malloc’s contract, and every allocation throughout this lesson was checked
against that return (small_block != NULL && large_block != NULL). The automatic region
overflowing shows itself with a crash, the heap region filling up shows itself with a NULL
return — both are proof that these regions are not unlimited, but one produces a
checkable return value, the other does not.
Summary
- Memory is split into four regions: automatic, static, constant data, and heap; the memory class this lesson measures is language.
- Blocks in the automatic and static region are never freed by hand — the owner is the language, the counter never moves.
- Blocks in the heap region are balanced with
count_alloc/count_free; the counter difference can again come out to 0, but this time because it was measured and balanced. - The region alone decides the ownership class for automatic and static memory; in the heap region, what decides the class is not the region but the contract.
- Region boundaries and alignment values depend on the environment; this lesson does not print them, that is left to M01/K01.
Next Step
The Dynamic Memory topic closes here: each of the four ownership classes (language, caller, callee, unowned) has been seen with at least one measurement. The next topic, Robustness, takes up the moment the contract breaks — what happens when a rule is written down and then knowingly violated, which situations are only described in prose and never run, and which defined counterpart runs alongside them.
To keep your progress and take notes, Log in
My notes
Log in to take notes.