C Part I · Module 9 · Memory and Object Lifetime
Ownership: Who Frees What
You now know every function C gives you for managing memory, and together they still cannot answer the question that actually causes memory bugs. Look at a declaration like void consume(char *data); and ask whether consume frees what it is given. Read the signature as carefully as you like; the answer is not there. C has no owning pointer type, no transfer, no borrow, no destructor, and no checker. The question is answered entirely by a convention that you choose, state, and keep — and when two programmers answer it differently about the same allocation, the result is a leak, a double free, or a pointer to released storage. This chapter is the design discipline that the next four chapters' bugs are failures of.
Advanced22 min readCPart IFundamentalsMemory
Part I · Module 9 · Chapter 9.8 · Ownership: Who Frees What
1. The Question C Cannot Answer
void consume(char *data);
char *produce(void);
int process(char *buffer, size_t size);For each of those three declarations, say who is responsible for the free.
You cannot. Nothing in a C declaration carries that information. char * means "the address of one or more char objects" and nothing more — Chapter 7.3 established that a pointer type describes what is at the address, not where it came from, how long it lives, or whose job it is to end it.
This is not an oversight you can work around with a cleverer type. Other languages give you machinery for it — a type that owns, a type that borrows, a compiler that tracks which is which, a destructor that runs on its own. C gives you none of that. What C gives you is three things:
- the name of the function,
- the documentation next to it,
- and a reviewer who has read both.
That is the entire enforcement mechanism, which is why the conventions in this chapter have to be few in number, obvious from the name, and never mixed within one interface.
Define ownership precisely: at any moment, an allocated block has exactly one owner — the one piece of code responsible for calling
freeon it exactly once. Every other pointer to that block is a borrow: it may read, it may write if permitted, and it must never free.
Chapter 9.6 §5 said the pairing of malloc and free is "a property of the program, not of any single function." Ownership is the name of that property, and designing it on purpose is what the rest of this chapter is about.
2. Three Conventions That Work
There are only three answers worth giving, and the discipline is to pick one per interface and say which.
3. Convention A — The Function Allocates, the Caller Frees
The function performs the allocation and returns the only pointer to it. Ownership crosses the boundary outward, and the caller inherits the obligation.
/* Returns a newly allocated array of n squares, or NULL on failure.
The CALLER owns the result and must free() it. */
int *make_squares(size_t n);The comment is not decoration. It is the only place the information exists.
Because the signature cannot carry it, the name has to. C's own library establishes the vocabulary, and following it buys you readers who already know the rule:
| Name shape | Examples | What it tells the reader |
|---|---|---|
make_, new_, create_ | make_squares | a new block comes back; you own it |
_dup, _copy | strdup | a copy comes back; you own it |
open, _open | fopen | a resource comes back; close it |
The rule to never break: a function whose name promises a new object must return one on every success path, or NULL. Nothing else, ever — §6 shows what happens when that rule is bent.
#include <stdio.h>
#include <stdlib.h>
/* The CALLER owns the result and must free() it. NULL on failure. */
static int *make_squares(size_t n)
{
int *out = malloc(n * sizeof *out);
if (out == NULL) {
return NULL;
}
for (size_t i = 0; i < n; i++) {
out[i] = (int)(i * i);
}
return out;
}
int main(void)
{
int *owned = make_squares(5); /* I own this */
if (owned == NULL) {
return 1;
}
printf("A function allocated, caller frees:");
for (size_t i = 0; i < 5; i++) {
printf(" %d", owned[i]);
}
printf("\n");
free(owned); /* so I release it, exactly once */
return 0;
}A function allocated, caller frees: 0 1 4 9 16Notice that make_squares returns NULL on failure rather than allocating something smaller or partially filled. A function that transfers ownership must transfer all of it or none of it — a half-built block whose contents are part real and part indeterminate puts the caller in a position it cannot reason about.
4. Convention B — The Caller Allocates, the Function Fills
The caller provides the storage. The function writes into it, reads from it, and does nothing else — it never frees it and never keeps the pointer after returning.
/* Fills out[0..n-1]. The CALLER owns out; this function never frees it. */
void fill_squares(int *out, size_t n);This is the convention to reach for first, and the reason is best seen by measurement rather than argument.
#include <stdio.h>
#include <stdlib.h>
/* Fills out[0..n-1]. Never allocates, never frees, never keeps the pointer. */
static void fill_squares(int *out, size_t n)
{
for (size_t i = 0; i < n; i++) {
out[i] = (int)(i * i);
}
}
static void print_ints(const char *tag, const int *a, size_t n)
{
printf("%s:", tag);
for (size_t i = 0; i < n; i++) {
printf(" %d", a[i]);
}
printf("\n");
}
int main(void)
{
int on_stack[5]; /* automatic storage */
fill_squares(on_stack, 5);
print_ints("B caller allocated on the stack", on_stack, 5);
int *heap = malloc(5 * sizeof *heap); /* allocated storage */
if (heap == NULL) {
return 1;
}
fill_squares(heap, 5);
print_ints("B caller allocated on the heap ", heap, 5);
free(heap);
return 0;
}B caller allocated on the stack: 0 1 4 9 16
B caller allocated on the heap : 0 1 4 9 16fill_squares works identically on an automatic array and on an allocated block, and it would work on a static one too. It contains no allocation at all, so it cannot leak, cannot double-free, and cannot hand back a dangling pointer. There is no ownership question to get wrong, because no ownership moved.
That is a far stronger guarantee than any amount of careful free-matching, and it is the same argument Chapter 7.7 made for passing a pointer and a length: the function should ask for the least it needs.
Prefer convention B whenever the caller can reasonably know the size. Convention A exists for the cases where it cannot — when the required size depends on work only the function can do.
The cost of convention B is honest and should be stated: the function must be told the size, and it must not write past it. Chapter 8.5's output-buffer rule applies in full — take the capacity as a parameter, respect it, and report truncation rather than overrunning.
5. Convention C — Create and Destroy as a Pair
When a block's lifetime spans many operations, give it one function that allocates and one that releases, named as a pair, and put both in the same place so a reviewer sees them together.
#include <stdio.h>
#include <stdlib.h>
static int *table_create(size_t n)
{
return calloc(n, sizeof(int)); /* NULL propagates; caller checks */
}
static void table_destroy(int *t)
{
free(t); /* free(NULL) is defined: no guard */
}
int main(void)
{
int *t = table_create(4);
if (t == NULL) {
return 1;
}
t[2] = 99;
int *alias = t; /* a SECOND name for the SAME block */
printf("t[2] = %d, alias[2] = %d, same block ? %d\n",
t[2], alias[2], (void *)t == (void *)alias);
table_destroy(t); /* one owner, one release */
t = NULL;
alias = NULL; /* the alias was a borrow, never an owner */
table_destroy(NULL); /* defined: does nothing */
printf("destroy(NULL) returned normally\n");
return 0;
}t[2] = 99, alias[2] = 99, same block ? 1
destroy(NULL) returned normallyThree things in that program are the convention, not the example.
The destroy function tolerates NULL, because free(NULL) is defined (Chapter 9.6 §5). That means callers never need a guard, and a cleanup path can run unconditionally.
alias is a borrow, not a second owner. Having two pointers to one block is completely normal and completely safe — Chapter 7.1 showed many pointers to one object. What is unsafe is two pieces of code each believing they must free it. The alias here reads the block and is then set to NULL without ever being passed to table_destroy.
The pair is adjacent in the source. A create at the top of a file and a destroy four hundred lines away in another file is the same code with none of the reviewability — and reviewability is the only enforcement C offers.
One owner, any number of borrows. The number of pointers to a block is not the problem. The number of pieces of code that think they must release it is.
6. The Anti-Patterns
Each of these is an interface whose readers will guess, and half of them will guess wrong.
6.1 The return value that is sometimes allocated and sometimes not
This is the worst of them, because it cannot be used correctly at all.
/* BROKEN DESIGN: the caller cannot tell whether to free the result. */
static const char *label_for_bad(int code)
{
if (code == 0) {
return "ok"; /* a literal — must NOT be freed */
}
char *made = malloc(16);
if (made == NULL) { return NULL; }
snprintf(made, 16, "code %d", code); /* allocated — MUST be freed */
return made;
}broken, code 0 : okIt prints the right thing, which is exactly the trouble. The caller holds a const char * and has two options, both wrong: free it always, and the day code is 0 the program frees a string literal, which is undefined behaviour and commonly a crash; or free it never, and every non-zero code leaks a block. The caller cannot choose correctly, because the ownership of the result depends on a run-time value the caller does not see.
There are two fixes, and which to pick is a design decision rather than a coin toss.
/* FIX 1 — always allocate, so one rule covers every return. */
static char *label_for(int code)
{
char *made = malloc(16);
if (made == NULL) { return NULL; }
if (code == 0) { snprintf(made, 16, "ok"); }
else { snprintf(made, 16, "code %d", code); }
return made; /* caller owns it, always */
}
/* FIX 2 — never allocate; the caller supplies the buffer. */
static int label_into(char *out, size_t cap, int code)
{
int n = (code == 0) ? snprintf(out, cap, "ok")
: snprintf(out, cap, "code %d", code);
return (n >= 0 && (size_t)n < cap);
}always-allocate : ok / code 7
caller-supplied : code 7Fix 1 is convention A, applied uniformly. Fix 2 is convention B, and it is the better choice here — it removes the allocation entirely, so there is nothing to own and nothing to leak. Note that it also reports truncation rather than hiding it, which is Chapter 8.5's snprintf discipline.
6.2 Ownership that depends on a flag
/* BROKEN: "free the result only if copy was nonzero" */
char *maybe_copy(char *src, int copy);Same defect in a different costume. The caller must now carry a boolean alongside the pointer and get the pairing right at every call site, including the error paths. Any interface that requires the caller to remember a second fact about a pointer has already failed.
6.3 Freeing something you were only lent
/* BROKEN: frees a block the caller still owns */
void process(char *buffer)
{
/* ... use buffer ... */
free(buffer); /* who said this was yours? */
}The caller passed a borrow. Perhaps it was an automatic array, in which case free is undefined behaviour on the spot. Perhaps it was a block the caller frees later, which is Chapter 9.10's double free. Perhaps the caller keeps using it, which is Chapter 9.11's dangling pointer. One design error, three different bugs, which is the point this chapter opened with.
6.4 Two owners
int *a = make_squares(5);
int *b = a; /* a borrow, if you say so */
free(a);
free(b); /* ... but this says otherwise */Nothing about b = a; is wrong. The second free is wrong, and it is wrong because the code never decided which of the two names was the owner. Chapter 9.10 covers what the second free actually does.
6.5 Freeing on some error paths and not others
A function that frees the caller's block when it fails but not when it succeeds — or the reverse — makes the caller's correct behaviour depend on a return value it may not even check. Pick one: either the function never frees what it was given, or it always does. The first is almost always right.
7. const as Ownership Documentation
Chapter 7.11 gave you const char * as a promise not to modify. It doubles as a very strong hint about ownership, and the hint has mechanical teeth.
size_t count_bytes(const char *s); /* reads only — plainly a borrow */
void consume(char *s); /* may write — ownership unclear */free takes a void *, not a const void *. So freeing through a const-qualified pointer requires a cast, and that cast is the smell:
void wrong(const char *s)
{
free((void *)s); /* the cast is the warning sign */
}A
const T *parameter should be read as "this function borrows and does not own." It is not enforced — the cast above compiles — but it is a convention with real diagnostic value, and the cast makes a violation visible in review. Useconston every pointer parameter you only read from; Chapter 7.11 §6's reasons and this one are cumulative.
8. Ownership Inside One Function: Error Paths
Ownership is not only an inter-function question. A function that performs two allocations owns the first one the moment it succeeds — so if the second fails, the first must be released before returning.
#include <stdio.h>
#include <stdlib.h>
/* On success returns 1 and transfers BOTH blocks to the caller.
On failure returns 0, sets both outputs to NULL, and leaks nothing. */
static int build_pair(size_t n, int **out_a, int **out_b)
{
int *a = malloc(n * sizeof *a);
if (a == NULL) {
goto fail_a;
}
int *b = malloc(n * sizeof *b);
if (b == NULL) {
goto fail_b;
}
for (size_t i = 0; i < n; i++) {
a[i] = (int)i;
b[i] = (int)i * 10;
}
*out_a = a; /* ownership transfers to the caller */
*out_b = b; /* only now, and only on success */
return 1;
fail_b:
free(a); /* release what this function still owns */
fail_a:
*out_a = NULL;
*out_b = NULL;
return 0;
}
int main(void)
{
int *a = NULL, *b = NULL;
if (!build_pair(4, &a, &b)) {
printf("build failed; nothing to release\n");
return 1;
}
printf("a:");
for (size_t i = 0; i < 4; i++) { printf(" %d", a[i]); }
printf(" b:");
for (size_t i = 0; i < 4; i++) { printf(" %d", b[i]); }
printf("\n");
free(a);
free(b);
int *x = NULL, *y = NULL; /* the failure path, forced */
if (!build_pair((size_t)-1 / sizeof(int), &x, &y)) {
printf("forced failure: x == NULL ? %d y == NULL ? %d\n",
x == NULL, y == NULL);
}
free(x); /* free(NULL) is defined; nothing leaked */
free(y);
return 0;
}Output:
a: 0 1 2 3 b: 0 10 20 30
forced failure: x == NULL ? 1 y == NULL ? 1Four design decisions are doing the work there.
The outputs are written only on the success path. Until return 1, the caller's variables are untouched, so a failure cannot hand back a half-filled result. This is exactly Chapter 9.7 §7's "check, then commit", applied to two blocks instead of one.
Both outputs are set to NULL on failure, so a caller that calls free on them regardless is correct rather than undefined.
The cleanup labels fall through in reverse order of acquisition. fail_b releases a and then falls into fail_a; a third allocation would add a third label at the top of the chain. The structure scales without a single nested conditional.
This is Chapter 4.8's one defensible use of goto, and it is the first time in this curriculum that the motivation has been real. Chapter 4.8 §7 described the pattern — a single cleanup label, jumped to only forward, never backward — and could not show why it mattered, because nothing had been allocated yet. This is why. Write the same function with nested if statements and the release of a appears in two places, which is how one of them eventually gets forgotten.
9. A Checklist for an Interface
Before you publish a function that takes or returns a pointer, answer all five. If any answer is "it depends", the interface is not finished.
- Does this function allocate? If so, who owns the result — and does the name say so?
- Does it free anything it was given? If so, is that true on every path, success and failure alike?
- Does it keep the pointer after returning — storing it somewhere that outlives the call? If so, the caller must be told, because the caller can no longer free it freely.
- Can it fail halfway? If so, does it release everything it had acquired, and leave the caller's variables untouched or
NULL? - Is every pointer parameter it only reads marked
const?
And one for the caller's side: for every pointer you hold, you should be able to say in one sentence whether you own it or borrowed it. If you cannot, that is the bug — not whatever crash eventually follows.
10. What You Should Be Able to Say Now
- C has no syntax for ownership. The name, the documentation and the reviewer are the entire mechanism.
- Ownership: exactly one piece of code is responsible for exactly one
free. Everything else is a borrow. - Convention A — the function allocates and the caller frees; the name must announce it (
make_,_dup,create_). - Convention B — the caller allocates and the function only fills; verified to work unchanged on automatic and allocated storage, and it cannot leak because it never allocates.
- Convention C —
createanddestroyas an adjacent, reviewable pair; destroy should tolerateNULL. - Prefer B when the caller can know the size. A is for when only the function can.
- A result that is sometimes allocated and sometimes not cannot be used correctly — verified; fix it by always allocating or never allocating.
- One owner, any number of borrows. Two pointers are fine; two owners are a double free.
const T *reads as "borrowed", and freeing through it requires a visible cast.- Within a function, release on every failure path — the reverse-order cleanup labels of Chapter 4.8, finally motivated.
11. Exercises
Exercise 1 — name the convention. For each, say which of A, B or C it is, and whether the name is adequate.
char *make_message(int code);
void render(char *out, size_t cap, int code);
int *buffer_create(size_t n);
void buffer_destroy(int *b);
char *fix_up(char *s);Exercise 2 — write the contract comment. Write the one- or two-line comment that must sit above each of these, stating ownership and the failure value.
int *make_squares(size_t n);
void fill_squares(int *out, size_t n);Exercise 3 — find the design error. Each has exactly one. Name it and name the convention that fixes it.
void log_line(char *text) { puts(text); free(text); }
char *get_name(int id, int *must_free);
int *resize_all(int *a, size_t n) { a = realloc(a, n * sizeof *a); return a; }
void store(const char *s) { free((void *)s); }Exercise 4 — the unusable return. Explain, in terms a caller can act on, why label_for_bad in §6.1 cannot be called correctly. Then say which of its two fixes you would choose for a function called a million times a second, and why.
Exercise 5 — count the owners. For each line, say how many owners the block has after it runs, and whether the state is correct.
int *p = make_squares(4);
int *q = p;
int *r = make_squares(4);
free(p);
p = NULL;Exercise 6 — add a third allocation. Extend build_pair from §8 to build_triple, allocating a third block c. Write the full cleanup-label chain. How many free calls appear in the function, and why is that not the same as the number of allocations?
Exercise 7 — the function that keeps the pointer. A function register_handler(char *name) stores name in a static pointer for later use. Say what the caller must and must not do with name afterwards, why checklist item 3 exists, and how you would redesign the function so the question does not arise.
Exercise 8 — justify the preference. In three or four sentences, argue why convention B is safer than convention A using the §4 measurement as evidence, and then name the specific circumstance in which convention B is impossible.
12. Summary
- C has no ownership syntax. The only tools are the name, the documentation, and the reviewer.
- One owner, one
free; every other pointer is a borrow. - A — function allocates, caller frees, and the name says so. B — caller allocates, function fills. C —
create/destroyas a reviewable pair. - Prefer B: verified to work on automatic and allocated storage alike, and it cannot leak or double-free because it never allocates.
- An interface whose ownership depends on a run-time value is unusable — fix by always or never allocating.
const T *documents a borrow, and violating it needs a visible cast.- Inside a function, release on every failure path and commit outputs only on success — Chapter 4.8's cleanup labels, now motivated.
- "Whoever allocates must free" is false;
strdupandfopendisprove it. One documented owner is the rule.
Everything so far has been about getting the design right. The next four chapters are about what the design failures actually do to a running program, and they come in a deliberate order of increasing nastiness. The mildest is the one where the free simply never happens: nothing is corrupted, nothing crashes, every test passes, and the program is still wrong — and on a program that is supposed to run for months rather than seconds, it is wrong in a way that eventually stops the machine. The next chapter is about recognising that failure and reasoning about why its cost depends entirely on how long your program is meant to live.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
