JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Object Lifetime and Who Owns Memory

Guide 1 taught you to borrow a block from the heap with malloc() and hand it back with free(). Now we ask the harder question that block raises: how long does it live, and whose job is it to free it? Lifetime and ownership are the two ideas that turn a heap of leaks into a program you can trust.

A block does not vanish when its name does

In guide 1 you learned the mechanical move: `void *p = malloc(n)` asks the heap for `n` raw bytes, and `free(p)` gives them back. But a pointer is small and a block is large, and the two have completely different fates. Up in the earlier rung you met scope and lifetime for ordinary variables — an `int x` inside a function is born when the function is entered and dies the instant it returns, its storage on the stack reclaimed automatically. Heap blocks break that comfortable rule. The block malloc() hands you has no name and no scope; it lives until you, by hand, call free() on it — not one instruction sooner, not one later.

This is exactly what storage duration is naming. An `auto` (ordinary local) variable has automatic duration: the compiler frees its stack slot for you. A heap block has allocated duration: the compiler frees nothing — the lifetime is yours to manage. The pointer `p` and the block it points to are two separate objects with two separate lifetimes. Lose track of that and you get the two classic disasters: the block outlives every pointer to it (a leak — guide 3), or a pointer outlives its block (a dangling pointer — guide 4). Both spring from confusing the name with the thing.

Ownership: the rule the compiler cannot enforce

If lifetime is when a block lives, ownership is who is responsible for ending it. Every malloc'd block needs exactly one free(), and the deep question is which piece of code is on the hook to make that call. This is allocation ownership, and C does not track it for you at all — it is a discipline you keep in your own head and, far better, in your documentation. A function that returns a malloc'd pointer is silently saying "I am handing you ownership; you must free this". A function that merely borrows a pointer to read it is saying "I do not own this; do not free it". Confuse those two contracts and you get a double-free or a leak.

The single most useful habit is to write the ownership contract down, right where the function is declared, because the type signature alone cannot tell you. A `char *make_greeting(void)` that owns and returns a block and a `void print_line(char *s)` that merely borrows one have indistinguishable pointer types, yet one transfers ownership out and the other does not. The standard library itself relies on convention this way: `strdup()` returns a block you must free; `getenv()` returns a pointer you must not free, because the C runtime still owns it. There is no syntactic difference — only the documented contract, so a one-line comment naming the owner ("caller frees", "borrows, never frees", "takes ownership") is worth more than any clever type.

calloc and realloc: the same block, fuller contracts

malloc() is not alone. calloc(count, size) allocates `count * size` bytes and — unlike malloc(), which hands you uninitialised garbage — guarantees every byte is set to zero. The two arguments are not just cosmetic: calloc() checks `count * size` for overflow internally, so `calloc(n, sizeof(int))` is safer than `malloc(n * sizeof(int))` when `n` comes from outside your program. The ownership story is identical, though: a calloc'd block is yours, freed by the same free(). Zeroing is a convenience, not a change of contract.

realloc(p, newsize) is the subtle one, and it is where lifetimes get slippery. It resizes an existing block, and its contract has a sting: it may move the block to a new address. If the heap can grow the block in place it returns the same pointer; if it cannot, it allocates a fresh block, copies your bytes across, frees the old one, and returns a different pointer. So the old `p` may now be dangling. The cardinal rule is to assign the result to a separate variable and check it before overwriting `p`, because realloc() can also fail and return `NULL` — and if you wrote `p = realloc(p, ...)` and it failed, you would have leaked the original block by clobbering its only pointer.

int *grow(int *p, size_t newcount) {
    int *tmp = realloc(p, newcount * sizeof(int));
    if (tmp == NULL) {       /* realloc failed; p is STILL valid */
        return NULL;         /* caller keeps the old block, no leak */
    }
    return tmp;              /* p may be dangling now; use the new one */
}

/* never do this:  p = realloc(p, ...);   <- leaks p if it returns NULL */
Catch realloc's result in a temporary. On failure the original block survives and is not leaked; on success the old pointer may be dead, so only the returned pointer is safe to use.

Designing where free() lives

Good C programs do not scatter free() calls at random; they decide ownership up front and put the matching free() in a predictable place. The most reliable pattern is to pair allocation and release in the same layer: whatever function (or struct) creates a block also owns the responsibility to destroy it, exposing a `make_x()` and a `free_x()` that mirror each other. The caller never has to guess. This mirrors a deeper idea you will meet later — Rust's ownership model takes exactly this human discipline and lets the compiler enforce it, freeing the block automatically when the owner goes out of scope, no garbage collector required.

Let me be honest about the cost. In C, all of this is on you. There is no garbage collector quietly reclaiming what you forget — that is a deliberate tradeoff for control and predictable timing, and it is why systems programmers reach for C. The price is that ownership bugs are easy to write and silent until they bite. The discipline below is not optional polish; it is the difference between a program that runs for years and one that bloats and crashes overnight.

  1. Decide the owner before you allocate: name the single function or struct responsible for the eventual free(), and write it in a comment next to the declaration.
  2. Pair the verbs: for every make_x()/create_x() that returns ownership, provide a matching free_x()/destroy_x() so callers release with the right call.
  3. After free(p), set p = NULL so a later accidental use is a clean null-deref (a caught crash) rather than a use-after-free reading reused memory.
  4. On every error path, free what you already allocated before you return — goto-cleanup or a single exit block keeps this honest as functions grow.

Where this leaves you

You now hold the mental model that the rest of this rung rests on. A heap block has a lifetime that begins at malloc() (or calloc/realloc) and ends, by your own hand, at free() — the compiler manages neither. And every block has an owner: exactly one place in the code responsible for that final free(). Lifetime answers "how long"; ownership answers "whose job". Get both clear and you can hand pointers across functions, store them in structs, and resize them with realloc() without losing the thread.

These two ideas also set up the failures we tackle next, head-on and honestly. When ownership is unclear, a block ends up with no owner and is never freed: that is the memory leak of guide 3, and we will learn to catch it with the sanitizers and valgrind. When a block has two would-be owners, or one owner frees while a stale pointer lingers, you get the dangling pointer, the use-after-free, and the double-free of guide 4 — undefined behaviour the optimizer is free to twist into corruption. Lifetime and ownership are not bureaucracy; they are the map that keeps those bugs off your program.