Ownership and Lifetimes in C¶
C has no unique_ptr, no borrow checker, no garbage collector. What it has is convention — a set of unwritten rules about who allocates and who frees. Every leak in production C code is really a convention that got violated or was never written down. In this file you’ll pick a convention and make it explicit; that alone will make you a stronger C programmer than 80% of the people writing it today.
What Ownership Means Here¶
A pointer’s owner is the entity responsible for eventually calling free on it (or, more generally, ending its lifetime cleanly — closing the fd, unmapping the region, releasing to the arena). At any moment, every heap allocation has exactly one owner. Multiple things can reference it — borrow it, look at it, read from it — but only one thing is responsible for its death.
Rust makes this a first-class type-system feature. In C you make it a first-class documentation and code-review feature. Same idea, much less enforcement, so you have to be disciplined.
The Three Roles a Function Plays With a Pointer¶
Every function signature that takes or returns a pointer falls into one of these categories. Write which one it is at the top of every non-trivial function you write for the rest of your career.
1. Owner-returner (“you now own this”)¶
// Caller must free() the returned buffer.
char *string_dup(const char *src);
// Caller must call user_destroy() on the result.
User *user_new(const char *name);
The function allocates. Ownership transfers to the caller on return. The caller — or someone the caller hands the pointer to — will eventually free it. If the function fails, it returns NULL and does not leak internally.
2. Borrower (“I’ll look, I won’t touch, I won’t remember”)¶
// Reads name; makes no copy, doesn't store the pointer past return.
void user_print(const User *u);
// Reads src; may modify *dst.
void buf_copy(char *dst, const char *src, size_t n);
Parameter is const T* (or T* if the function writes through it). Function must not free it, must not store it anywhere that outlives the call, must assume it becomes invalid the moment the function returns. const on a pointer parameter is a promise to the caller — it says “I’m borrowing, not taking.”
3. Sink (“I’m taking ownership from you”)¶
// Takes ownership of user. Caller must not free() it afterward.
void registry_add(Registry *r, User *user);
// Takes ownership of items; frees the container and everything in it.
void list_destroy(List *l);
Parameter is a plain T* (non-const), and the function’s contract says it will free or store the pointer. The caller’s T *p = ...; sink(p); p = NULL; pattern is the usual accompaniment — explicit acknowledgement that the caller no longer owns.
Making the Convention Visible¶
Since C’s type system won’t help you, help yourself with a comment convention. A minimal scheme:
/* [owned] caller must free the returned pointer with user_destroy() */
User *user_new(const char *name);
/* [borrow] pointer must remain valid for the call only */
void user_print(const User *u);
/* [sink] takes ownership of user; do not free after this call */
void registry_add(Registry *r, User *user);
/* [out] *out is written on success; unchanged on failure */
int user_lookup(Registry *r, const char *name, User **out);
Some codebases use attribute-style macros (OWNED, BORROWED, TRANSFER) so grep can find them. Some use Doxygen tags. The specific syntax matters less than having one and applying it everywhere.
GCC/Clang has __attribute__((malloc)) and, since GCC 11, __attribute__((malloc(deallocator))) that lets the compiler track which allocator/deallocator pairs go together — useful for warnings but still just a hint, not enforcement.
The Out-Parameter Pattern¶
Functions that both return a status and produce a value use out-parameters. The convention: on success, *out is set; on failure, *out is untouched.
// Returns 0 on success; -1 on error (errno set).
// On success, *out is a newly-allocated User owned by the caller.
int user_load(const char *path, User **out) {
User *u = user_new(NULL);
if (!u) return -1;
if (load_from_file(u, path) < 0) {
user_destroy(u);
return -1;
}
*out = u; // ownership transfers to caller here
return 0;
}
This pattern generalizes cleanly to multiple outputs and is easier to compose with error handling than tagged-return sentinels like NULL.
Container Ownership¶
When a container (list, vector, hashmap) stores pointers, decide up front:
Owning container: destroying the container frees the elements. Insertion transfers ownership from caller to container.
Borrowing container: the container just holds references. Insertion does not transfer. Destruction does not free elements.
Both are valid; mixing them in the same container is a nightmare. Encode the choice in the container’s name or in a single field:
typedef struct {
void **items;
size_t len, cap;
void (*elem_free)(void*); // NULL == borrowing; non-NULL == owning
} Vec;
With this pattern you can build both kinds of vectors from one implementation. vec_destroy calls elem_free on each item if set, then frees the array.
Multiple References, Single Owner¶
When more than one part of the system needs to look at the same object, the safest pattern is one owner, many borrowers, borrower lifetimes strictly shorter than the owner’s. Example: a Config loaded at startup, owned by main, borrowed as const Config* by every subsystem. Nobody but main frees it.
When borrowers’ lifetimes can outlive the owner’s, you’re in reference-counting territory. C doesn’t stop you; you just have to write it:
typedef struct { atomic_int refs; /* ... */ } RcThing;
RcThing *thing_retain(RcThing *t) { atomic_fetch_add(&t->refs, 1); return t; }
void thing_release(RcThing *t) {
if (atomic_fetch_sub(&t->refs, 1) == 1) { /* free fields */; free(t); }
}
Reference counting is a legitimate tool, not a smell. Use it when the alternative is more complex.
Arena Ownership: The Alternative¶
Ryan Fleury / Chris Wellons style: don’t do per-object ownership at all for most memory. Allocate everything for a scope (a request, a frame, a parse) out of an arena, then free the whole arena at scope end. Ownership becomes the arena owns everything, and I own the arena. Two rules replace hundreds. See 06_custom_allocators.md.
What Most People Get Wrong About This¶
They treat leaks as accidents — “oh, I forgot a free.” Leaks in real codebases are almost always architectural: the code has no clear ownership story, so different code paths make different assumptions about who frees. The fix is not more frees. The fix is deciding, in writing, who owns what, and then making the code obviously match. When your team can’t answer “who frees this?” for a given pointer without reading the callers, the leak is already in the design.
Exercise: Ownership-Annotate Something You’ve Written¶
Before moving on, take a 200-500 line C file you’ve written (or grab one from your M2 work) and annotate every function’s parameters and return value with [owned] / [borrow] / [sink] / [out]. If a function’s role is unclear, that’s the bug; fix the signature or the docs until it isn’t. You’ll be shocked how much this exercise sharpens the code.
Return to README.md · Next: 05_memory_bug_taxonomy.md