C / DYNAMIC MEMORY
free and the ownership rule for every allocation
Decide who owns each heap block, pair every allocation with exactly one free on all paths, and write NULL-safe destructors for structs that own memory.
What you will learn
- Pair every malloc with exactly one free on every return path, errors included
- Mark every pointer parameter and return value as owning or borrowed
- Give each owning struct a NULL-safe _free that releases members before itself
- Set a variable to NULL once it hands its block to a new owner
Understanding free and the ownership rule for every allocation
free(p) tells the allocator that the block starting at p is available again. It does not change p, and it does not inform any other pointer in your program that the block is gone. The allocator tracks block sizes and free lists only; it has no record of which variable was supposed to release a block, so it cannot warn you when nobody does or when two pieces of code both do. Two hard requirements come from the allocator's side: p must be exactly the address a malloc-family call returned, and free(NULL) is defined to do nothing.
The ownership rule is that at every instant a live block has exactly one owner, a single variable, struct field or container slot, and releasing that owner is what frees the block. Every other pointer to the same block is a borrow: it may read and write while the owner is alive, but it never calls free and it must not outlive the owner. C has no syntax for this distinction, so the convention lives in names and comments. A function called thing_new returns ownership, thing_free consumes it, and a parameter of type const char * almost always only borrows.
Ownership turns freeing into something you can count. Pick one block, trace every path the program can take, and confirm exactly one free runs on each, including the early return taken when the second malloc inside a constructor fails, which is where most miscounts hide. When ownership moves, because you stored the pointer in a list or passed it to a function documented as taking it, your old variable stops being the owner and should be set to NULL so no later cleanup claims it. An owner that owns other owners must release its children first, since freeing the parent destroys the only record of where the children were.
<stdio.h>
<stdlib.h>
<string.h>
/* Returns a block the caller owns: it must reach text_free exactly once. */
static char *text_new(const char *src)
{
size_t n = strlen(src) + 1;
char *p = malloc(n);
if (p == NULL)
return NULL;
memcpy(p, src, n);
return p;
}
/* Takes ownership: after this call the argument names nothing. */
static void text_free(char *p)
{
free(p);
}
/* Borrows only: it may read the block but must never free it. */
static size_t count_spaces(const char *p)
{
size_t k = 0;
for (; *p != '\0'; p++)
if (*p == ' ')
k++;
return k;
}
int main(void)
{
char *owned = text_new("one owner per block");
if (owned == NULL) {
fprintf(stderr, "out of memory\n");
return 1;
}
printf("text: %s\n", owned);
printf("spaces: %zu\n", count_spaces(owned));
text_free(owned); /* the one free that pairs with the one malloc */
owned = NULL; /* this variable is no longer an owner */
text_free(owned); /* free(NULL) is defined and does nothing */
printf("owned is %s\n", owned == NULL ? "NULL" : "a live block");
return 0;
}
Each heap block has exactly one owner at any moment and calling free is that owner's job, a rule your code must enforce because the compiler cannot.
Worked examples
A struct that owns its members
One destructor releases every block an object owns, and the constructor's failure path reuses that same destructor.
<stdio.h>
<stdlib.h>
<string.h>
struct config {
char *name;
int *ports;
size_t nports;
};
/* Owns the struct and everything it points to. NULL-safe on purpose. */
static void config_free(struct config *c)
{
if (c == NULL)
return;
free(c->name);
free(c->ports);
free(c);
}
static struct config *config_new(const char *name, size_t nports)
{
struct config *c = calloc(1, sizeof *c); /* members start out NULL */
if (c == NULL)
return NULL;
c->name = malloc(strlen(name) + 1);
c->ports = malloc(nports * sizeof *c->ports);
if (c->name == NULL || c->ports == NULL) {
config_free(c); /* releases whatever did get allocated */
return NULL;
}
strcpy(c->name, name);
c->nports = nports;
for (size_t i = 0; i < nports; i++)
c->ports[i] = 8000 + (int)i;
return c;
}
int main(void)
{
struct config *c = config_new("web", 3);
if (c == NULL)
return 1;
printf("%s owns %zu ports:", c->name, c->nports);
for (size_t i = 0; i < c->nports; i++)
printf(" %d", c->ports[i]);
putchar('\n');
config_free(c); /* one call at the call site, three blocks released */
puts("all blocks released");
return 0;
}
Example explained
Line 1calloc zeroes the struct, so c->name and c->ports are already NULL before either malloc runs.
Line 2config_free frees the two members before free(c), because after the struct goes away their addresses are unreachable.
Line 3The failure path calls config_free on a half-built object, which works only because free(NULL) is harmless for the member that was never allocated.
Line 4main makes one call for three blocks: owning the struct means owning everything the struct points to.
Rows before the array
Freeing a jagged two-dimensional array in the order that keeps every address reachable, including the unwind on a mid-loop failure.
<stdio.h>
<stdlib.h>
int main(void)
{
size_t rows = 3, cols = 4;
int **grid = malloc(rows * sizeof *grid);
if (grid == NULL)
return 1;
for (size_t r = 0; r < rows; r++) {
grid[r] = malloc(cols * sizeof *grid[r]);
if (grid[r] == NULL) { /* give back only what we own */
while (r-- > 0)
free(grid[r]);
free(grid);
return 1;
}
for (size_t c = 0; c < cols; c++)
grid[r][c] = (int)(r * cols + c);
}
printf("grid[2][3] = %d\n", grid[2][3]);
for (size_t r = 0; r < rows; r++)
free(grid[r]); /* rows first: grid still holds their addresses */
free(grid); /* then the array of pointers itself */
printf("freed %zu blocks\n", rows + 1);
return 0;
}
Example explained
Line 1Each grid[r] = malloc(...) creates a block whose only owner is the slot grid[r], so the slot is the thing that must free it.
Line 2The while (r-- > 0) loop frees rows 0 through r-1, exactly the ones that succeeded, and skips the row whose malloc returned NULL.
Line 3Freeing free(grid) first would be a leak of three blocks, because grid is the only place their addresses were ever stored.
Line 4Four allocations mean four frees: three row blocks plus the pointer array.
Important notes
free returns a block to the allocator's pool for later reuse and usually does not shrink the process footprint, so flat memory usage after freeing is not by itself evidence of a leak.
Only addresses produced by malloc, calloc, realloc or aligned_alloc may be passed to free, never a stack array, a string literal or a static object, even when the pointer type is identical.
Common mistakes
Calling free on a struct pointer and stopping there: only the struct's own block returns to the allocator, and each member block becomes unreachable, so the leak grows with every object created.
Freeing a pointer that was only lent, such as one read out of a struct or returned by a getter: the real owner frees the same address later, so one block is released twice and the allocator's bookkeeping is corrupted.
Passing an adjusted address to free after walking a pointer with p++, or freeing an interior address like &buf[8]: free requires the exact address malloc returned, so this is undefined behaviour, not a partial release.
Try it yourself
Change, predict, then run
Write xmalloc and xfree wrappers that increment and decrement a global live-block counter, then build a struct owning a duplicated string and an int array and destroy it through one NULL-safe destructor. Print the counter at the end; any value other than 0 means a block had no owner or two.
Open the C workspaceCheck your understanding
A struct owns a heap-allocated label, and a getter returns it: char *label_of(struct item *it) { return it->label; }. A caller writes char *s = label_of(it); puts(s); free(s); and later calls item_free(it). Why is this broken?
- It is not broken, since s holds a genuine malloc address and freeing such an address is legal.
- The getter only lends the block, so it->label is still the owner and item_free releases the same address a second time.
- free cannot be applied to a pointer that arrived as a function return value.
- The block must be released by the same function that allocated it rather than by free.
Show answer
Living on the heap is not permission to free; ownership is, and the owner here is the field it->label, which item_free will release. Option 0 is tempting because s does satisfy free's only technical requirement, an unmodified malloc address, but two frees of one block is precisely the situation the one-owner rule exists to prevent. Making the getter's contract explicit, borrow only, or having it return a copy the caller owns, fixes it.