---
title: 'Deallocation Discipline'
source: 'https://academia.sh/en/courses/c-memory/deallocation-discipline'
course: 'Memory and Pointers in C'
language: en
updated: '2026-09-26T03:02:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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'
---

# 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.

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.

```c
// 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
```

```c
// 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;
}
```

```c
// 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
```

```c
// 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.

```c
// 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.

```c
// 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.

```c
// 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.
