Skip to content
academia.sh

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, and realloc is always the caller; the memory class this lesson measures is the caller.
  • malloc does not zero the content, calloc does — this difference was shown in the measurement by reading the values directly, not by assuming it.
  • realloc does not create a new owner; content is preserved, the counter only increments on the first allocation.
  • A failed realloc does 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) and calloc is 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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close