---
title: 'Memory Layout'
source: 'https://academia.sh/en/courses/c-memory/memory-layout'
course: 'Memory and Pointers in C'
language: en
updated: '2026-09-26T03:02:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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"
---

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

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.

```c
// 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
```

```c
// 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;
}
```

```c
// 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.

```c
// 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.

```c
// 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.
