Skip to content
academia.sh

Lesson 08 / 15

Deallocation Discipline

When a function returns a piece of text, who owns that text is not written in the signature — what writes it is the comment in the header file. Two functions pointing at the same record, one lending it and one returning a copy, show how the same contract can bind to two different owners.

Published Updated

In this lesson

  • Shows how a comment in a header file carries an ownership contract
  • Distinguishes with the counter difference that record_name and record_name_copy return the same text with two different owners
  • Separates the obligation question in this lesson from the lifetime question in M10/K02
Contents

The previous lesson showed that all four allocation functions carry the same contract: the allocator frees. But this is not the contract’s only form. When a function returns a pointer, the signature does not decide who that pointer’s owner will be — two functions that return char *, with no visible difference between them, can carry two entirely different obligations. This lesson measures that difference with two functions that point at the same record: one keeps the memory inside the module and only lends it, the other returns the caller a new block.

The Contract Is Written in the Header File

The measurement is done with the owner.h/owner.c pair reused throughout the rest of the course; both are built on top of the counting wrapper (counter.h/counter.c), so they appear in the same unit. The inside of the record (the fields of struct Record) is not visible in owner.h — there is only a name (Record) and four function signatures. The ownership information is in the comment above the signature.

// 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;
}
// owner.h — the record's inside is hidden, the ownership contract is in the comments
#ifndef OWNER_H
#define OWNER_H

#include <stddef.h>

typedef struct Record Record;

/* The caller owns it and frees it with record_destroy. If the name does not
   fit the limit, returns NULL; the function frees what it allocated so far. */
Record *record_create(const char *name, size_t limit);

/* Frees the record together with the name buffer inside it. */
void record_destroy(Record *r);

/* The record owns it: the returned pointer is valid until the record is
   destroyed, and the caller does not free it. */
const char *record_name(const Record *r);

/* The caller owns it: the returned copy is freed with count_free. */
char *record_name_copy(const Record *r);

#endif
// owner.c — the unit where the contract is carried out
#include <string.h>
#include "owner.h"
#include "counter.h"

struct Record {
    char *name;
    size_t length;
};

Record *record_create(const char *name, size_t limit) {
    Record *r = count_alloc(sizeof *r);
    if (r == NULL) {
        return NULL;
    }
    r->length = strlen(name);
    if (r->length >= limit) {
        count_free(r);
        return NULL;
    }
    r->name = count_alloc(r->length + 1);
    if (r->name == NULL) {
        count_free(r);
        return NULL;
    }
    memcpy(r->name, name, r->length + 1);
    return r;
}

void record_destroy(Record *r) {
    if (r == NULL) {
        return;
    }
    count_free(r->name);
    count_free(r);
}

const char *record_name(const Record *r) {
    return r->name;
}

char *record_name_copy(const Record *r) {
    char *copy = count_alloc(r->length + 1);
    if (copy != NULL) {
        memcpy(copy, r->name, r->length + 1);
    }
    return copy;
}

DM4. The signatures of record_name and record_name_copy are nearly identical — both take a const Record *, both return a pointer to a char array. The difference between them sits nowhere the compiler can check; it sits only in the comment. This is a short summary of how C carries memory ownership: the type system does not enforce an obligation, it only enforces a sentence one person wrote and another person reads.

Lent Versus Owned

record_name returns a pointer directly into the name field inside the record — it does not allocate new memory. This pointer is lent: its validity depends on the record itself, and lasts until the record is destroyed with record_destroy. record_name_copy, on the other hand, allocates a new block with count_alloc and copies the same text into it; this block is independent of the record’s lifetime, and its owner is now the caller.

// contract.c — contrasting a lent name with a returned copy
#include <stdio.h>
#include <string.h>
#include "owner.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");

    Record *r = record_create("contract", 16);

    /* --- callee: the module owns it, the caller does not free it --- */
    before = allocated_count() - freed_count();
    const char *lent = record_name(r);
    after = allocated_count() - freed_count();
    snprintf(buffer, sizeof buffer, "name %s, counter diff %d", lent, after - before);
    observe("lent name", "callee", buffer);

    /* --- caller: the owner of the returned copy is the caller --- */
    before = allocated_count() - freed_count();
    char *copy = record_name_copy(r);
    int equal = (copy != NULL) && (strcmp(copy, lent) == 0);
    count_free(copy);
    after = allocated_count() - freed_count();
    snprintf(buffer, sizeof buffer, "equal text %d, counter diff %d", equal, after - before);
    observe("returned copy", "caller", buffer);

    /* --- lent pointer still valid: copy freed, record still standing --- */
    const char *lent_after = record_name(r);
    snprintf(buffer, sizeof buffer, "name %s, same as before %d", lent_after, lent_after == lent);
    observe("lent pointer still valid", "callee", buffer);

    /* --- once the record is destroyed, the lent pointer follows its lifetime --- */
    before = allocated_count() - freed_count();
    record_destroy(r);
    after = allocated_count() - freed_count();
    snprintf(buffer, sizeof buffer, "counter diff %d", after - before);
    observe("record destruction", "callee", buffer);

    return 0;
}
observation                   owner     evidence
lent name                     callee    name contract, counter diff 0
returned copy                 caller    equal text 1, counter diff 0
lent pointer still valid      callee    name contract, same as before 1
record destruction            callee    counter diff -2

DM5. In the lent name observation the counter difference is 0 — record_name allocated nothing, freed nothing; it only copied a pointer that already pointed at an existing block. The returned copy observation is also 0, but for a different reason: here an allocation did happen, and it was freed right away with count_free(copy). Two zeros hide two different stories — one is zero because it did no allocation at all, the other is zero because it freed what it allocated. The -2 in the record destruction observation is expected: record_destroy freed two blocks (the name and the record itself), reallocating neither; the counter difference turned negative because this time only the freeing side was measured.

The lent pointer still valid observation shows that the lent pointer depends on the record’s lifetime: the record_name_copy call allocated and freed a separate block, but never touched the record itself — which is why the second call to record_name returned the same address (lent_after == lent). The only thing that invalidates the lent pointer is record_destroy itself.

Two Independent Copies

record_name_copy allocates a new block every time it is called; this means that if two copies are requested from the same record, two separate owners are born. If one of the copies is freed early, the other is entirely unaffected by it — because the only thing the two share is nothing beyond an address.

// two_copies.c — two copies taken from the same record have independent lifetimes
#include <stdio.h>
#include <string.h>
#include "owner.h"
#include "counter.h"

int main(void) {
    char buffer[96];
    Record *r = record_create("independent", 16);

    int before = allocated_count() - freed_count();
    char *copy_a = record_name_copy(r);
    char *copy_b = record_name_copy(r);
    int mid_diff = (allocated_count() - freed_count()) - before;

    count_free(copy_a);
    int b_still_standing = (strcmp(copy_b, "independent") == 0);
    int inner_diff = (allocated_count() - freed_count()) - before;

    count_free(copy_b);
    int end_diff = (allocated_count() - freed_count()) - before;
    record_destroy(r);

    snprintf(buffer, sizeof buffer,
             "diff after two allocations %d, when a is freed %d, b still readable %d, when both are freed %d",
             mid_diff, inner_diff, b_still_standing, end_diff);
    printf("%-30s%s\n", "two independent copies", buffer);

    return 0;
}
two independent copies        diff after two allocations 2, when a is freed 1, b still readable 1, when both are freed 0

DM6. copy_a and copy_b were derived from the same source (r->name), but both were born through record_name_copy with a separate count_alloc call — which is why the counter difference after two allocations is 2. The count_free(copy_a) call only affects copy_a‘s block; copy_b is still valid and strcmp confirms this (b still readable 1). This is the exact opposite pattern of record_name returning a single lent pointer: there, two calls on the same record gave the same address (lent_after == lent); here, two record_name_copy calls on the same record give two different blocks. The ownership class is the caller in both cases, but each copy carries its own independent ownership — freeing one has no effect on the other’s contract whatsoever.

Cleanup at the Boundary

The comment in record_create’s header file also records a third possibility: if the name does not fit within the given limit, the function returns NULL, but it itself frees whatever memory it had allocated up to that point. This shows that the callee class carries an obligation not only on a successful return but also on a failed one — the module cleans up its own unfinished work, leaving nothing behind for the caller.

// limit.c — when record_create does not fit the limit, it frees what it allocated itself
#include <stdio.h>
#include "owner.h"
#include "counter.h"

int main(void) {
    char buffer[64];
    int before = allocated_count() - freed_count();

    Record *failed = record_create("a-very-long-name", 4);
    int is_null = (failed == NULL);
    int diff = (allocated_count() - freed_count()) - before;

    snprintf(buffer, sizeof buffer, "is NULL %d, counter diff %d", is_null, diff);
    printf("%-30s%s\n", "name over the limit", buffer);

    return 0;
}
name over the limit           is NULL 1, counter diff 0

DM7. "a-very-long-name" is sixteen characters long, the limit is 4 — record_create allocates a block for r, then finds the length check failed, and calls count_free(r) without ever allocating r->name, returning NULL. The counter difference is still 0: a single allocation, a single free, both happened inside the function itself. The caller sees that failed is NULL but never knows which block was allocated and which was freed — and does not need to know, because the contract stays on the module even in a failure case.

The Bounding Measurement

The comment in owner.h is binding in both directions. If the caller frees the pointer returned by record_name with count_free, the record’s own free call (the count_free(r->name) inside record_destroy) frees the same block a second time — this is undefined behavior, and it is not run in this lesson. The reverse direction can break the same way: if the caller never frees the block returned by record_name_copy, then because the block is independent of the record’s lifetime, destroying the record does not rescue it either — it leaks. Both directions of the contract are written in the same comment, but the compiler checks neither; the only thing that checks is a person reading the code.

What these two ways of breaking have in common is that neither is visible from the signature. char *record_name(const Record *r) and char *record_name_copy(const Record *r) are equivalent in the compiler’s eyes — both return a pointer to a char array, both take a const Record *. If the caller handles the wrong function the wrong way (tries to free record_name’s return, or never frees record_name_copy’s return), compilation still succeeds; the error only surfaces at runtime, as a double free or a leak. This is the same class of error as overwriting a failed realloc’s return in the previous lesson: both are syntactically valid, both are ownership violations the compiler cannot catch. The difference is the source — there the error came from a return value being ignored, here it comes from a comment not being read.

owner.c itself has no defense against these two ways of breaking; the one who takes on that defense is the module’s user. The record_create/record_destroy pair keeps a record’s entire lifetime under the module’s own control — the caller is safe as long as it uses this pair symmetrically. Storing the pointer record_name lent in a variable and reading that variable after destroying the record is also an error from the same family (a dangling pointer); this lesson does not run that separately, because it already returns to M10/K02’s lifetime question — the subject there was when it ends, and here it is who is obligated to free it, and both break here at the same time.

This lesson does not overlap with the lifetime question measured in M10/K02. There, what was measured was where a value’s lifetime ends, and the language decided that place. What is measured here is who is obligated to end that lifetime, and what decides that is not the language but the contract in owner.h. record_name and record_name_copy give the same text, and both of their lifetimes end eventually — but one leaves that ending to the record’s owner, the other hands it to the caller.

Summary

  • Who owns a function’s returned pointer is understood not from the signature but from the comment in the header file; the memory class this lesson measures is the callee.
  • record_name lends: the counter difference is 0, its validity depends on the record’s lifetime.
  • record_name_copy allocates a new block: the counter difference is also 0, but this time an allocation and a free actually happened — its owner is now the caller.
  • Breaking the contract (freeing something lent, or never freeing a copy) produces a double free or a leak; neither was run in this lesson, only described.
  • The obligation question in this lesson is separate from the lifetime question in M10/K02: one asks who will free it, the other asks when it ends.

Next Step

What happens if the copy record_name_copy returns is never freed was only described in this lesson — not run. The next lesson measures exactly that: a copy whose return is never held leaves behind a block no pointer can reach any longer, and this block does not appear as an error message or a crash — only as a counter difference.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close