Lesson 07 / 15
Memory Allocation Functions
malloc, calloc, realloc, and free all work from the same heap and all four leave the same question: who frees the block you allocated? The answer is always the same — the allocator is the caller; this lesson measures that with the counter difference, reads the zeroing directly, and shows that resizing leaves the old ownership unchanged.
Published Updated
In this lesson
- Measures with the counter difference that the owner of the block allocated by malloc, calloc, and realloc is the caller in every case
- Shows that calloc zeroes the block it allocates by reading the values directly
- Explains that a resize does not change ownership of the old block and that on failure the old block remains valid
Contents
The previous six lessons always took for granted where the memory a pointer points to came
from: the address of a local variable, the first element of an array, a field inside a
record. Even the Function Pointers lesson carried a callback’s context but never asked how
that context itself came to exist. This lesson closes that gap: a block, before anyone can
want it, must first be allocated — and whichever of the four functions the allocating
side calls, it takes on the same burden.
C’s standard library provides four allocation and release functions: malloc returns an
unzeroed block of the requested size, calloc takes the element count and element size as
separate parameters and returns the block zeroed, realloc grows or shrinks an existing
block, free releases a block. All four carry the same contract: the returned pointer’s
owner is the caller — whoever requests the block is also obligated to free it. On this
course’s measurement axis, this is the most direct of the four classes; there is no need to
look for a contract here, the contract is in the function’s own signature.
The Allocator Frees
The measurement is done with the same counting wrapper reused throughout the rest of the
course. counter.c counts every count_alloc and count_grow call, and every
count_free call; if the difference between the start and the end of an observation is
0, then everything allocated in that observation has been freed.
// counter.h — a wrapper that counts every allocation and every free #ifndef COUNTER_H #define COUNTER_H #include <stddef.h> void *count_alloc(size_t size); void *count_grow(void *old, size_t size); void count_free(void *block); int allocated_count(void); int freed_count(void); #endif
// counter.c — the counters live only in this unit
#include <stdlib.h>
#include "counter.h"
static int allocated = 0;
static int freed = 0;
void *count_alloc(size_t size) {
void *block = malloc(size);
if (block != NULL) {
allocated++;
}
return block;
}
void *count_grow(void *old, size_t size) {
int new_block = (old == NULL);
void *grown = realloc(old, size);
if (grown != NULL && new_block) {
allocated++;
}
return grown;
}
void count_free(void *block) {
if (block != NULL) {
freed++;
}
free(block);
}
int allocated_count(void) {
return allocated;
}
int freed_count(void) {
return freed;
}
These two files return under the same name and the same content in the remaining thirteen
lessons of the course; since this is where they first appear, they are written in full here.
count_alloc wraps malloc, count_grow wraps realloc — both only increment the counter
and call the real function, they do not change behavior.
// alloc.c — measuring malloc, calloc, and realloc against the caller class
#include <stdio.h>
#include <stdlib.h>
#include "counter.h"
static void observe(const char *name, const char *class, const char *evidence) {
printf("%-30s%-10s%s\n", name, class, evidence);
}
int main(void) {
char buffer[64];
int before, after;
printf("%-30s%-10s%s\n", "observation", "owner", "evidence");
/* --- malloc: raw block, the allocator frees --- */
before = allocated_count() - freed_count();
int *raw = count_alloc(3 * sizeof *raw);
int total = 0;
if (raw != NULL) {
for (int i = 0; i < 3; i++) {
raw[i] = i + 1;
total += raw[i];
}
}
count_free(raw);
after = allocated_count() - freed_count();
snprintf(buffer, sizeof buffer, "total %d, counter diff %d", total, after - before);
observe("raw block", "caller", buffer);
/* --- calloc: zeroed block, values read directly --- */
int *zeroed = calloc(4, sizeof *zeroed);
int all_zero = 1;
if (zeroed != NULL) {
for (int i = 0; i < 4; i++) {
if (zeroed[i] != 0) {
all_zero = 0;
}
}
}
snprintf(buffer, sizeof buffer, "all zero %d", all_zero);
observe("zeroed block", "caller", buffer);
free(zeroed);
/* --- realloc: resizing does not create a new owner --- */
before = allocated_count() - freed_count();
int *growing = count_grow(NULL, 2 * sizeof *growing);
int kept = 0;
if (growing != NULL) {
growing[0] = 7;
int *grown = count_grow(growing, 8 * sizeof *grown);
if (grown != NULL) {
growing = grown;
}
kept = growing[0];
}
count_free(growing);
after = allocated_count() - freed_count();
snprintf(buffer, sizeof buffer, "first element %d, counter diff %d", kept, after - before);
observe("resize", "caller", buffer);
printf("allocated %d freed %d diff %d\n", allocated_count(), freed_count(),
allocated_count() - freed_count());
return 0;
}
observation owner evidence raw block caller total 6, counter diff 0 zeroed block caller all zero 1 resize caller first element 7, counter diff 0 allocated 2 freed 2 diff 0
DM1. All three observations brought the counter difference back to 0, and there is
one reason for this: right after each allocation call, or at the end of the function, there
is a matching free call. This is not a shared behavior of the four functions, it is a
promise the caller makes: malloc, calloc, and realloc do not impose a contract, they
only return a block; forgetting to free the block triggers neither a compiler warning nor a
linker error. raw block was used like a three-element array and yielded a total of 6 —
unlike the decayed arrays from the previous lesson, this was a block whose type was known
and whose length was fixed at the source; the difference was that this time the length came
not from a fixed definition but from a runtime request.
calloc’s Zeroing
malloc allocates the requested size but does not touch its contents: the memory it returns
carries whatever was already there, and that value is undefined — reading it is not
safe, so the lesson never prints that value anywhere. calloc takes the same request with
two parameters — element count and element size — and zeroes the block it returns. The
zeroed block observation proves this directly by reading it: all four ints were found to
be zero, without any assignment.
This difference is not just a convenience. A block allocated with malloc, if not zeroed by
hand before first use, may carry the leftover of a previous allocation in every value read
from it — calloc moves this risk into the function’s own contract. It matters for the same
reason that calloc checks the multiplication of the two elements for overflow itself
(if element_count * element_size exceeds an integer limit): writing
malloc(element_count * element_size) leaves that check to the caller, writing
calloc(element_count, element_size) brings the check inside the function. Ownership does
not change here either — the zeroed block also belongs to the caller and must be
released with free, which the lesson does on the last line.
Resizing Does Not Create a New Owner
realloc (here through the count_grow wrapper) is the trickiest of the four functions,
because a single call can do three different things: the block grows in place, the block is
moved to another address and the old address is freed, or the request cannot be satisfied
and NULL is returned. The resize observation shows the first two cases in a single flow:
growing was allocated with two elements, its first element was written as 7, then it
was grown to eight elements. The first element 7 value in the output proves that the
content was carried over, regardless of whether the address changed during the
growth — even if realloc moved to a new location, it copied the old content into the new
block itself.
What matters for ownership is this: before the growth, growing‘s owner was the caller,
after the growth it is still the caller — no new owner was born, only the address in the
same owner’s hand may have changed. The counter confirms this: the count_grow(NULL, ...)
call incremented the allocation counter because it was counted as a new block, while the
second count_grow(growing, ...) call did not count a new allocation — the new_block
check in counter.c exists exactly for this. At the end of the observation, the
count_free(growing) call frees a single block and the difference returns to 0.
The structure of the code draws attention to the grown variable: the return of
count_grow is not written directly over growing, it is first taken into a separate
variable and checked for NULL. The reason is that on a failed resize, the old block is
still valid and still belongs to the caller: if realloc cannot satisfy the request, it
does not free the original block, it leaves it as it is and returns NULL. Writing the
return directly as growing = count_grow(growing, ...); would turn growing itself into
NULL on failure — from that point on no pointer would remain that could reach the old
block, and the block would become unowned, appearing as a leak. This lesson’s code does
not exercise this mistake, because the request here is always small enough to succeed; but
the flow — taking it into a separate variable, checking for NULL, overwriting only on
success — is built in a way that also blocks exactly this second pattern. The difference
between the right pattern and the wrong pattern is a single line of assignment, but the
result is the difference between an owner still being able to find its block and losing it
entirely.
Chained Growth
The previous observation grew growing in a single step. But growing an array repeatedly
as it fills is also a common pattern — instead of growing the block by one element each time
an element is added, doubling it whenever it fills up. What is interesting about this
pattern from an ownership standpoint is that no matter how many times it is grown, the
block is still counted as a single allocation.
// chain.c — repeated growth only increments the counter once
#include <stdio.h>
#include "counter.h"
int main(void) {
char buffer[128];
int before = allocated_count() - freed_count();
int capacity = 1;
int *array = count_grow(NULL, capacity * sizeof *array);
int filled = 0;
for (int i = 0; i < 5; i++) {
if (filled == capacity) {
capacity *= 2;
int *grown = count_grow(array, capacity * sizeof *array);
if (grown == NULL) {
break;
}
array = grown;
}
array[filled] = i + 1;
filled++;
}
int total = 0;
for (int i = 0; i < filled; i++) {
total += array[i];
}
int mid_diff = (allocated_count() - freed_count()) - before;
count_free(array);
int end_diff = (allocated_count() - freed_count()) - before;
snprintf(buffer, sizeof buffer,
"filled %d, capacity %d, total %d, diff after growth %d, diff after free %d",
filled, capacity, total, mid_diff, end_diff);
printf("%-30s%s\n", "chained growth", buffer);
return 0;
}
chained growth filled 5, capacity 8, total 15, diff after growth 1, diff after free 0
DM2. The loop grew capacity three times, going 1 → 2 → 4 → 8, so count_grow was
called four times in total (including the first allocation). Yet once the growths are done,
the counter difference is still 1: the new_block check in counter.c only increments
allocated on the first call, where old == NULL; the following three calls are counted as
a continuation of the same block. This proves that the rule shown in a single step in
“Resizing Does Not Create a New Owner” does not change no matter how many steps it is spread
across: ownership does not look at how many times a hand changed along the chain, it looks
at how many times it started with a new allocation. After the count_free(array) call,
the difference returns to 0 — at the end of the chain, just as at its start, there was a
single owner.
A Shrinking Block
Chained growth always expanded the block; but realloc can also shrink a block under
the same contract. Shrinking is not the mirror image of growing: the implementation may
shrink the block in place, or it may move it to a smaller address, but in either case the
answer to the ownership question does not change — a shrunk block still belongs to the
caller.
// shrink.c — shrinking with realloc keeps the same caller ownership
#include <stdio.h>
#include "counter.h"
int main(void) {
char buffer[96];
int before = allocated_count() - freed_count();
int *wide = count_grow(NULL, 10 * sizeof *wide);
for (int i = 0; i < 10; i++) {
wide[i] = i;
}
int first_diff = (allocated_count() - freed_count()) - before;
int *narrow = count_grow(wide, 2 * sizeof *narrow);
int kept_first = -1, kept_second = -1;
if (narrow != NULL) {
kept_first = narrow[0];
kept_second = narrow[1];
}
int shrink_diff = (allocated_count() - freed_count()) - before;
count_free(narrow);
int end_diff = (allocated_count() - freed_count()) - before;
snprintf(buffer, sizeof buffer,
"first element %d, second element %d, growth diff %d, shrink diff %d, diff after free %d",
kept_first, kept_second, first_diff, shrink_diff, end_diff);
printf("%-30s%s\n", "shrinking block", buffer);
return 0;
}
shrinking block first element 0, second element 1, growth diff 1, shrink diff 1, diff after free 0
DM3. narrow was allocated small enough to hold only two of wide’s first ten
elements; yet the kept_first and kept_second values carry over exactly the 0 and
1 written before the shrink — realloc does not corrupt the remaining content when it
shrinks a block, it only frees the now-excess space. The counter difference did not change
at all from first_diff to shrink_diff: the allocation stayed single, both the growth and
the shrink were a continuation of the same block. This shows that the rule from “Resizing
Does Not Create a New Owner” is direction-independent — the rule is not specific to growing,
it rests on realloc’s ability to change the size in either direction.
The Bounding Measurement
A malloc or calloc call with a size of zero is left open by the standard in both
directions: an implementation may either return NULL or return a valid pointer of zero
size that can be freed with free. This is an environment-dependent behavior, and which one
is chosen is not printed here — the only thing read at the source is that the caller
must write with the knowledge that either of these two possibilities can happen. The
ownership guarantee the four allocation functions give does not change — if the returned
pointer is not NULL, its owner is still the caller — but this guarantee says nothing about
what the return will be for a zero-size request.
The same uncertainty applies to a realloc(block, 0) call: on some implementations this
call has the same effect as free(block) and returns NULL, on others it returns a valid
block of zero size. The difference between the two affects whether the caller still holds
the block — in the first case the block has already been freed, in the second it still
belongs to the caller and a separate free call is required. Because of this uncertainty,
this course’s code never calls realloc with a zero size anywhere; when a size needs to be
brought down to zero, the block is freed directly with count_free and the pointer is
separately assigned NULL. Where the ownership question can have two different
environment-dependent answers, the lesson chooses not to ask that question at all.
Summary
- The owner of the block returned by
malloc,calloc, andreallocis always the caller; the memory class this lesson measures is the caller. mallocdoes not zero the content,callocdoes — this difference was shown in the measurement by reading the values directly, not by assuming it.reallocdoes not create a new owner; content is preserved, the counter only increments on the first allocation.- A failed
reallocdoes not free the old block; writing the return directly over the old variable leaves the old block unowned, which is why the return is first checked in a separate variable. - A zero-size request with
malloc(0)andcallocis implementation-dependent and is not printed in this lesson’s measurement.
Next Step
This lesson measured how ownership begins: as soon as a block is allocated, the
obligation to free it passes to the caller. But where that obligation is written down
has not yet been asked — count_alloc and count_free themselves say “you will free this”
nowhere; this information is only inferred from the function’s name and from habit. The
next lesson puts this obligation in writing, in a header file, and shows the first mechanism
by which ownership passes from the caller to the callee: of two functions that both
return the same text, one’s owner is the side that frees it, the other’s owner is the
caller.
To keep your progress and take notes, Log in
My notes
Log in to take notes.