---
title: 'Memory Leaks'
source: 'https://academia.sh/en/courses/c-memory/memory-leaks'
course: 'Memory and Pointers in C'
language: en
updated: '2026-09-26T03:02:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows with the counter difference how overwriting a pointer leaves the previous block unreachable'
    - 'Measures how ignoring a returned value produces the same result'
    - 'Explains that a memory leak is not an error message but a counter difference'
---

# Memory Leaks

A memory leak gives no error message and crashes no program — it only widens the gap between what was allocated and what was freed. This lesson produces a leak two different ways, catches both with the counter difference, and shows why the total comes out to exactly 2.

The previous lesson only **described** how a contract can be broken — neither a double free
nor an unfreed copy was actually run there. This lesson actually runs one form of a broken
contract: a block that no one is obligated to free. This belongs to none of the previous
three classes (language, caller, callee) — the course's fourth and final class is
**unowned**. The unowned class is not a design, it is a defect; and the only place it
appears is the **counter difference**.

## The Same Result, Two Ways

A block can end up unowned in two different ways: by overwriting the only pointer that
reaches it, or by never holding on to the return value of a call that returns it. Both
produce the same thing — a block no variable can reach any longer — but one is an assignment
mistake, the other is an oversight.

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

```c
// leak.c — comparing an overwritten pointer with an ignored return
#include <stdio.h>
#include "owner.h"
#include "counter.h"

static int diff(void) {
    return allocated_count() - freed_count();
}

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;
    int first_diff, second_diff;

    printf("%-30s%-10s%s\n", "observation", "owner", "evidence");

    /* --- overwritten pointer: the first block is unreachable --- */
    before = diff();
    int *lost = count_alloc(sizeof *lost);
    lost = count_alloc(sizeof *lost);
    count_free(lost);
    first_diff = diff() - before;
    snprintf(buffer, sizeof buffer, "counter diff %d", first_diff);
    observe("overwritten pointer", "unowned", buffer);

    /* --- ignored return: the copy disappears without ever being held --- */
    Record *r = record_create("draft", 16);
    before = diff();
    if (r != NULL) {
        record_name_copy(r);
    }
    second_diff = diff() - before;
    snprintf(buffer, sizeof buffer, "counter diff %d", second_diff);
    record_destroy(r);
    observe("ignored return", "unowned", buffer);

    printf("total unowned counter diff %d\n", first_diff + second_diff);

    return 0;
}
```

```
observation                   owner     evidence
overwritten pointer           unowned   counter diff 1
ignored return                unowned   counter diff 1
total unowned counter diff 2
```

**DM8.** In the `overwritten pointer` observation, `lost` is allocated twice but freed only
once: when the second `count_alloc` call overwrites `lost`, no variable remains holding the
address the first call returned. `count_free(lost)` can only free the **second** block — the
first can no longer be reached by any path. The counter difference is **1**: one block was
allocated, and its counterpart was never freed.

**DM9.** The `ignored return` observation reaches the same result by a different path.
`record_name_copy` allocates a new block every time it is called and **returns** it — the
function itself does not free the block, because its contract leaves that to the caller (the
"Deallocation Discipline" lesson). Here the return value was never assigned to any variable;
the function ran successfully, the block was allocated, but no pointer that could reach it
ever existed. The counter difference is again **1**. Together the two observations give a
**total unowned counter diff of 2** — this number is the same **diff 2** as the final line
in the measurement core's full run; here it is only arrived at by summing two separate
unowned observations.

## When a Leak Repeats

The two observations in the previous section measured the leak **once**: each raised the
counter difference by one. But the claim that "in a long-running process, the counter keeps
growing" has so far only been stated in words, not measured. Running the same leaking
mechanism a few times inside a loop turns that claim directly into a number.

```c
// repeat.c — the same leak repeated in a loop
#include <stdio.h>
#include "owner.h"
#include "counter.h"

static int diff(void) {
    return allocated_count() - freed_count();
}

static void leaking_call(const Record *r) {
    /* the return value is deliberately not held: every call leaks a block */
    record_name_copy(r);
}

int main(void) {
    char buffer[64];
    Record *r = record_create("loop", 16);
    int before = diff();

    for (int i = 1; i <= 3; i++) {
        leaking_call(r);
        int now = diff() - before;
        snprintf(buffer, sizeof buffer, "counter diff after call %d is %d", i, now);
        printf("%-30s%s\n", "leak repetition", buffer);
    }

    record_destroy(r);
    return 0;
}
```

```
leak repetition               counter diff after call 1 is 1
leak repetition               counter diff after call 2 is 2
leak repetition               counter diff after call 3 is 3
```

**DM10.** Each `leaking_call` call allocates a block through `record_name_copy` and never
holds the return — just like the `ignored return` observation, but here three calls in a
row instead of one. The counter difference rises by exactly **1** per call: 1, 2, 3. These
three numbers show that the sentence "in a short-lived program this difference may stay
negligible; in a long-running process the counter keeps growing" is now an observation, not
a claim. The loop stops after three iterations, so the difference stops at three; in a real
process, a call like this may run thousands of times per second, and the same linear growth
continues without stopping — the counter is visible here only because it is held by hand in
this course, it is not visible in a real program, but it is still **there**.

## A Partial Leak: A Half-Freed Array

The previous two observations either leaked completely or not at all — a call's return was
either held or never held. But real code also has a sneakier pattern: **most** of an array
of blocks is freed correctly, one is **skipped**. The total counter difference catches this
with the same sensitivity — independent of how many were done correctly, it only counts how
many are **missing**.

```c
// partial.c — three of four copies freed, one skipped
#include <stdio.h>
#include "owner.h"
#include "counter.h"

static int diff(void) {
    return allocated_count() - freed_count();
}

int main(void) {
    char buffer[80];
    enum { COUNT = 4 };
    char *copies[COUNT];
    Record *r = record_create("partial", 16);
    int before = diff();

    for (int i = 0; i < COUNT; i++) {
        copies[i] = record_name_copy(r);
    }

    for (int i = 0; i < COUNT; i++) {
        if (i == 2) {
            continue; /* the third copy is deliberately not freed */
        }
        count_free(copies[i]);
    }

    int remaining_diff = diff() - before;
    record_destroy(r);

    snprintf(buffer, sizeof buffer,
             "allocated %d, freed %d, remaining counter diff %d", COUNT, COUNT - 1, remaining_diff);
    printf("%-30s%s\n", "partial free", buffer);

    return 0;
}
```

```
partial free                  allocated 4, freed 3, remaining counter diff 1
```

**DM11.** The loop allocated four copies, the second loop freed three of them and skipped
only the copy where `i == 2`. `remaining_diff` comes out to **1** — exactly as many as the
skipped copy, no more, no less. This observation shows that a leak is not "all or nothing":
three of the four free calls in a loop working correctly does not hide the fourth's absence,
the counter difference still grows by exactly the missing amount. In a real program, this
pattern shows up in a branch a loop skips early with a `continue` or a `break` — while the
rest of the code applies a fully correct freeing discipline, a single control-flow branch
falls outside that discipline.

## The Bounding Measurement

In neither of these two observations did the program **crash**, print any warning, or have
any function return `NULL`. The allocation functions fulfilled their contract completely:
they gave a block of the requested size. The problem is not on their side, it is on the
caller's side — an assignment was forgotten, or a return value was ignored. This is why a
memory leak gives **no observable symptom** at runtime; only the gap between what was
allocated and what was freed grows. In a short-lived program this difference may stay
negligible; if the same mechanism is run over and over in a long-running process, the
counter keeps growing and eventually memory runs out.

**This does not overlap with the lifetime question measured in M10/K02.** There, the
question was where a value's lifetime ends, and that place was decided by the **language** —
a local variable going out of scope was reclaimed automatically. Here there is **no language
rule at all** that ends the block's lifetime; the block is reclaimed not by the language but
only by the **caller's own free call**. If that call is never made, the language ends
nothing on its own — which is why, unlike the other three, the unowned class is the name of
an **absence**, not of a rule.

Each of the other three classes gives an **answer** to who the owner is: in the language
class the owner is the scope itself, in the caller class the owner is the calling side's
code, in the callee class the owner is the contract inside the module. The unowned class
gives no answer — because the question was never asked. In the `overwritten pointer`
observation, the block belonged to the **caller** class the moment it was allocated; the
pointer `count_alloc` returned was not ambiguous about whose responsibility it was. The
ambiguity was born afterward, when that pointer was crushed by a second assignment. The
same is true for `ignored return`: `record_name_copy`'s contract is clear (the caller owns
it), but no variable on the caller's side ever took up that role. Unownedness is therefore
not a fourth, **independent** class; it is the erasure, in the code, of the trace of a
previously existing ownership relationship. A block's owner is never designed to be
"unowned" — it is only ever **left** that way.

## Summary

- A memory leak is born two ways: overwriting a pointer, or ignoring a returned value; the
  memory class this lesson measures is **unowned**.
- A leak produces no error message or crash; its only visible evidence is the **counter
  difference**.
- Each of the two observations raised the counter difference by **1**, for a total of
  **2** — the same number as the difference on the measurement core's binding final line.
- The unowned class is not a language rule, it is an **absence**: the language never makes
  any free call on its own.

## Next Step

This lesson measured the leak with a **counter difference**, because the core itself was a
counting wrapper. In a real program, such a counter is not held by hand — instead, **a
tool** that does this job is run. The next lesson shows how this lesson's counting wrapper
actually models what such a tool does: **caught** or **missed**, with no other name
appearing.
