Skip to content
academia.sh

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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close