Register

C for Beginners

0 of 24 notes read 0%

Reading progress is saved to your INFIRALS account. Sign in to keep this note marked as read.

Stack, heap, and the lifetime of data

Pointers and memory12 min readLesson 5 of 6

5.1 One stack, one frame per call

Local variables with automatic storage live in a stack frame created when the function is called and destroyed when it returns. Parameters often start in registers and may be copied into the frame.

The frame also holds the return address and saved registers. Its size is fixed when the function compiles, apart from platform extensions such as alloca, which are best left alone.

Lifetime inside the frame is short. An address that points into the frame becomes invalid the moment the function returns, even though the old bytes may still hold the old value.

c
#include <stdio.h>

int main(void)
{
    int outer = 1;
    {
        int inner = 2;
        printf("inner: %d\n", inner);
    }
    printf("outer: %d\n", outer);
    return 0;
}

Output

inner: 2
outer: 1

5.2 Returning an address of a local

int *bad(void) { int x = 5; return &x; } compiles with a warning on most compilers, and the pointer is invalid at the call site. The next call may reuse the frame.

The compiler cannot catch every form. The address can travel through another function or a struct before it is read. GCC and Clang report the direct case with -Wreturn-local-addr; MSVC reports it with /W4.

Return the value instead of the address, or allocate storage whose lifetime outlives the call.

c
#include <stdio.h>

struct Pair {
    int first;
    int second;
};

static struct Pair make_pair(int a, int b)
{
    struct Pair result = {a, b};
    return result;
}

int main(void)
{
    struct Pair p = make_pair(3, 4);
    printf("%d %d\n", p.first, p.second);
    return 0;
}

Output

3 4

5.3 Heap lifetime is manual

Heap memory lives from the moment an allocation returns until the matching free. Nothing in the language ends its life automatically, and leaving a function changes nothing.

That property makes the heap the right choice when a value must outlive the call that created it. It also means every allocated block needs a plan for release.

A pointer in a local variable can point at the heap and disappear when the variable goes out of scope. The block stays allocated, and if no other pointer holds its address, the program can never free it.

c
#include <stdio.h>
#include <stdlib.h>

static int *make_counter(int start)
{
    int *counter = malloc(sizeof *counter);
    if (counter == NULL) return NULL;
    *counter = start;
    return counter;
}

int main(void)
{
    int *counter = make_counter(40);
    if (counter == NULL) return 1;
    *counter += 2;
    printf("counter: %d\n", *counter);
    free(counter);
    return 0;
}

Output

counter: 42

5.4 Dangling pointers

A dangling pointer points at memory that has been freed or at a frame that has returned. The pointer value is still there, and comparing it with NULL succeeds, so a guard does not help.

The symptoms vary. The value looks correct in a small program, changes after an unrelated call, or crashes when the allocator reuses the block.

Setting a pointer to NULL after free prevents a second free and makes a later dereference crash close to the bug. It cannot help when another copy of the same address still exists.

c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *data = malloc(2 * sizeof *data);
    if (data == NULL) return 1;
    data[0] = 5;
    free(data);
    data = NULL;

    int *replacement = malloc(2 * sizeof *replacement);
    if (replacement == NULL) return 1;
    replacement[0] = 11;
    printf("replacement: %d\n", replacement[0]);
    printf("old pointer cleared: %d\n", data == NULL);
    free(replacement);
    return 0;
}

Output

replacement: 11
old pointer cleared: 1

5.5 What a leak looks like

A leak is an allocation with no remaining pointer to it. The program stays correct, and the memory stays reserved until the process ends. A long-running service loses more memory with every leak until an allocation fails.

Leaks usually appear on error paths. Two allocations succeed, the third fails, and the function returns without freeing the first two. They also appear when a pointer is overwritten with a new allocation before the old one is freed.

Valgrind reports leaks with the allocation stack, AddressSanitizer does the same on Windows, Linux, and macOS, and Visual Studio has a CRT leak report for debug builds.

c
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char *a = calloc(8, 1);
    char *b = calloc(8, 1);
    if (a == NULL || b == NULL) {
        free(a);
        free(b);
        return 1;
    }
    for (int i = 0; i < 7; i++) {
        a[i] = 'x';
        b[i] = 'y';
    }
    printf("%s %s\n", a, b);
    free(a);
    free(b);
    return 0;
}

Output

xxxxxxx yyyyyyy

5.6 Stack or heap

Use automatic storage first. It costs nothing to allocate, needs no cleanup, and stays local to the function. Arrays with a compile-time size belong there.

Use the heap when the size is decided at run time, when the value must outlive the function, or when the object is too large for the stack. A multi-megabyte buffer on an 8 MB Windows stack can overflow before the first byte is written.

Recursion reaches the same limit from another direction. Every call adds a frame, and deep recursion can exhaust the stack without allocating anything explicitly.

c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int local[4] = {1, 2, 3, 4};
    int count = 100000;
    long *large = malloc((size_t)count * sizeof *large);
    if (large == NULL) return 1;
    for (int i = 0; i < count; i++) large[i] = i;
    printf("local sum: %d\n", local[0] + local[3]);
    printf("large last: %ld\n", large[count - 1]);
    free(large);
    return 0;
}

Output

local sum: 5
large last: 99999

5.7 A lifetime checklist

Before returning a pointer, ask what it points at and how long that storage lasts. Before storing a pointer in a struct, ask the same question, because the struct may live longer than the data.

When a struct holds a pointer, the struct does not own the data by default. Either the struct owns it and its cleanup function frees it, or it borrows the data and the caller keeps the lifetime guarantee.

Nothing in C enforces the rule, so write it next to the declaration and keep allocation and release in the same layer when you can.

c
#include <stdio.h>

struct View {
    const char *text;
    int length;
};

static void print_view(const struct View *view)
{
    printf("%.*s (%d)\n", view->length, view->text, view->length);
}

int main(void)
{
    struct View view = {"borrowed text", 8};
    print_view(&view);
    return 0;
}

Output

borrowed (8)

Interview questions

3 questions from this note. Open one to see the answer; no account needed.

  • 01Why is returning `&local` from a function a bug?

    The local lives in the function's stack frame. After the return the frame is gone, so the address points at storage the program may reuse at any moment.

  • 02What is a dangling pointer?

    A pointer that still holds an address whose storage has been released, either by `free` or by a function returning. Reading through it is undefined behaviour.

  • 03When should a value live on the heap?

    When its size is known only at run time, when it must outlive the function that creates it, or when it is too large for the stack.