---
title: 'Pointer Basics'
source: 'https://academia.sh/en/courses/c-memory/pointer-basics'
course: 'Memory and Pointers in C'
language: en
updated: '2026-09-26T03:02:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures the address-of and dereference operators with equality and the pointed-to value, without printing an address'
    - "Shows with an example that a pointer's type does not say who owns the block it points to"
---

# Pointer Basics

The question of who owns a block of memory; the full run of the five-file measurement core and the 4/4/3/2 distribution across four classes (language, caller, callee, unowned), followed by measuring the address-of and dereference operators without printing an address.

The C Fundamentals course measured which stage (preprocessor, compiler, linker, runtime)
a decision was made at. This course opens with a different question: nothing in the source
says who will **free** a block of memory. The compiler knows a declaration's type, the
linker resolves which definition a name binds to, the runtime only sees the result — but
the answer to "who frees this block" lives in none of them. That answer travels through
a **contract** written in the source itself: a comment in a header file, a function name,
a convention.

The course measures this with a single question: **who owns this memory?** The answer
falls into one of four classes, and these four names hold for the whole course.

## Four Classes: Language, Caller, Callee, Unowned

| Class | Definition |
|---|---|
| **language** | The language decides the lifetime (automatic or static); nobody calls a free |
| **caller** | Whichever side allocates the memory, or receives it from a function, is obliged to free it |
| **callee** | A module holds the memory; it only lends it to the caller, and cleans up itself on the error path too |
| **unowned** | No side is obliged to free it; the block shows up only as a counter difference |

A five-file measurement core sets up these four classes: `counter.h`/`counter.c` is a thin
wrapper that counts every allocation and every free, `owner.h`/`owner.c` is a unit that
carries an opaque record's ownership contract in its comments, and `ownership.c` is the
measurement function that gathers thirteen observations into a single run. The remaining
fourteen lessons of the course take **slices** of this core; this lesson is the one
exception that runs it in full.

```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
// ownership.c — the core that measures who owns a piece of memory
#include <stdio.h>
#include <string.h>
#include "owner.h"
#include "counter.h"

static int static_counter = 3;

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 language = 0, caller = 0, callee = 0, unowned = 0;
    int before, allocated_before, freed_before;

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

    /* --- language: the language sets the lifetime, nobody frees --- */

    before = diff();
    int local = 5;
    int *p = &local;
    snprintf(buffer, sizeof buffer, "*p %d, counter diff %d", *p, diff() - before);
    observe("local variable", "language", buffer);
    language++;

    int *s = &static_counter;
    snprintf(buffer, sizeof buffer, "*s %d, no free call", *s);
    observe("static storage", "language", buffer);
    language++;

    const char *literal = "fixed";
    snprintf(buffer, sizeof buffer, "length %d, no free call", (int)strlen(literal));
    observe("string literal", "language", buffer);
    language++;

    int array[4] = {1, 2, 3, 4};
    int *first = array;
    int *last = &array[3];
    snprintf(buffer, sizeof buffer, "last - first %d, *last %d", (int)(last - first), *last);
    observe("local array", "language", buffer);
    language++;

    /* --- caller: whoever allocates frees --- */

    before = diff();
    int *block = count_alloc(4 * sizeof *block);
    int total = 0;
    if (block != NULL) {
        for (int i = 0; i < 4; i++) {
            block[i] = i + 1;
            total += block[i];
        }
    }
    count_free(block);
    snprintf(buffer, sizeof buffer, "total %d, counter diff %d", total, diff() - before);
    observe("allocate and free", "caller", buffer);
    caller++;

    before = diff();
    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);
    snprintf(buffer, sizeof buffer, "first element %d, counter diff %d", kept, diff() - before);
    observe("resize", "caller", buffer);
    caller++;

    allocated_before = allocated_count();
    Record *r = record_create("ledger", 16);
    snprintf(buffer, sizeof buffer, "not null %d, allocated blocks %d", r != NULL, allocated_count() - allocated_before);
    observe("opaque record", "caller", buffer);
    caller++;

    before = diff();
    char *copy = record_name_copy(r);
    snprintf(buffer, sizeof buffer, "copy %s", copy != NULL ? copy : "none");
    count_free(copy);
    snprintf(buffer + strlen(buffer), sizeof buffer - strlen(buffer), ", counter diff %d", diff() - before);
    observe("returned copy", "caller", buffer);
    caller++;

    /* --- callee: the module owns it, the caller does not free --- */

    before = diff();
    const char *lent = record_name(r);
    snprintf(buffer, sizeof buffer, "name %s, counter diff %d", lent, diff() - before);
    observe("lent name", "callee", buffer);
    callee++;

    before = diff();
    Record *too_long = record_create("a very long record name", 8);
    snprintf(buffer, sizeof buffer, "null %d, counter diff %d", too_long == NULL, diff() - before);
    observe("cleanup on error path", "callee", buffer);
    callee++;

    freed_before = freed_count();
    record_destroy(r);
    snprintf(buffer, sizeof buffer, "freed blocks %d", freed_count() - freed_before);
    observe("record's inner buffer", "callee", buffer);
    callee++;

    /* --- unowned: nobody is obliged to free it --- */

    before = diff();
    int *lost = count_alloc(sizeof *lost);
    lost = count_alloc(sizeof *lost);
    count_free(lost);
    snprintf(buffer, sizeof buffer, "counter diff %d", diff() - before);
    observe("overwritten pointer", "unowned", buffer);
    unowned++;

    Record *r2 = record_create("draft", 16);
    before = diff();
    if (r2 != NULL) {
        record_name_copy(r2);
    }
    snprintf(buffer, sizeof buffer, "counter diff %d", diff() - before);
    record_destroy(r2);
    observe("ignored return", "unowned", buffer);
    unowned++;

    printf("\n%-10s%d\n", "language", language);
    printf("%-10s%d\n", "caller", caller);
    printf("%-10s%d\n", "callee", callee);
    printf("%-10s%d\n", "unowned", unowned);
    printf("total observations: %d\n", language + caller + callee + unowned);
    printf("allocated %d freed %d diff %d\n", allocated_count(), freed_count(), diff());
    return 0;
}
```

```
observation                   owner     evidence
local variable                language  *p 5, counter diff 0
static storage                language  *s 3, no free call
string literal                language  length 5, no free call
local array                   language  last - first 3, *last 4
allocate and free             caller    total 10, counter diff 0
resize                        caller    first element 7, counter diff 0
opaque record                 caller    not null 1, allocated blocks 2
returned copy                 caller    copy ledger, counter diff 0
lent name                     callee    name ledger, counter diff 0
cleanup on error path         callee    null 1, counter diff 0
record's inner buffer         callee    freed blocks 2
overwritten pointer           unowned   counter diff 1
ignored return                unowned   counter diff 1

language  4
caller    4
callee    3
unowned   2
total observations: 13
allocated 11 freed 9 diff 2
```

**PO1.** Of the thirteen observations, **4** landed in the `language` class, **4** in
`caller`, **3** in `callee`, and **2** in `unowned`. None of the four `language` rows call
a free — the local variable, the static storage, the string literal, and the local array
each end their own lifetime. The `allocated 11 freed 9 diff 2` line says that at the end
of the run **two** blocks are still left unfreed; this matches exactly the gap left by the
`unowned` rows — the overwritten pointer and the ignored return each lost a whole block.
This run compiled **without warnings** and produced the **exact same** output across three
separate runs; every remaining lesson in the course takes this table and these four classes
as its source.

## Address-Of and Dereference: Deepening the `local variable` Row

The core's first row — `local variable` — contains the most common pair of operators:
address-of (`&`) and dereference (`*`). This lesson measures them separately, outside the
core, with a small run that focuses on nothing else.

```c
// address_of.c — address-of and dereference, measured with equality and value, no address printed
#include <stdio.h>

int main(void) {
    int number = 42;
    int *p = &number;

    printf("%s %d\n", "pointed-to value", *p);
    printf("%s %d\n", "does p point at number", (int)(p == &number));

    *p = 100;
    printf("%s %d\n", "new value written through dereference", number);

    int other = 7;
    p = &other;
    printf("%s %d\n", "pointed-to value after reassignment", *p);
    printf("%s %d\n", "does p point at number now", (int)(p == &number));
    return 0;
}
```

```
pointed-to value 42
does p point at number 1
new value written through dereference 100
pointed-to value after reassignment 7
does p point at number now 0
```

**PO2.** The expression `&number` produces the address of the `number` variable, and that
address is assigned to `p`; the comparison `p == &number` gives **1**, meaning `p` really
does point at `number` — but this comparison never prints the address itself, it only asks
whether two addresses are equal. The line `*p = 100` uses the **dereference operator** on
the left side of an assignment: it writes to where `p` points, not to `p` itself. The proof
is the next line — `number` is now **100**; the write made through `p` really did change
the variable it points to. With `p = &other`, `p` can be **reassigned** to point at a
different variable; after this assignment `*p` gives **7**, and `p == &number` is now
**0** — `p` is itself a variable, free to hold a different address whenever it is
reassigned.

**PO3.** The declaration `int *p` says that `p` holds **the address of an `int`** — but
that type never says how the memory `p` points to was obtained. In the run above, `p`
first pointed at one local variable (`number`), then at another (`other`); both were in
the `language` class, and nobody freed either. But the `int *` type can just as well point
at a dynamically allocated block — the core's `allocate and free` row did exactly that:
`int *block = count_alloc(...)`. Same declaration, same operators (`*`, `&`), two different
ownership classes. This is the lesson's bounding measurement.

## Pointer to Pointer: A Second Layer of Dereference

The `p` variable above pointed at an `int`; but since a pointer is itself a variable, another
pointer can be written that holds its address. This takes two `*` instead of one, and
deepens the dereference by one more layer.

```c
// double_pointer.c — a pointer to a pointer, applying dereference one layer deeper
#include <stdio.h>

int main(void) {
    int number = 9;
    int *p = &number;
    int **pp = &p;

    printf("%s %d\n", "value read through double dereference", **pp);
    printf("%s %d\n", "does pp point at p's address", (int)(*pp == p));

    **pp = 30;
    printf("%s %d\n", "new value written through double dereference", number);

    int other = 4;
    *pp = &other;
    printf("%s %d\n", "value p points to after changing p through pp", *p);
    printf("%s %d\n", "does p point at number now", (int)(p == &number));
    return 0;
}
```

```
value read through double dereference 9
does pp point at p's address 1
new value written through double dereference 30
value p points to after changing p through pp 4
does p point at number now 0
```

**PO4.** The declaration `int **pp` says that `pp` holds the address of an `int *` —
`pp` points at `p`, and `p` points at `number`. `**pp` applies two dereference operators in
sequence: first `*pp` reaches the address carried by `p`, then the second `*` reaches the
value at that address. The line `**pp = 30` uses this two-layer path on the left side of an
assignment, and `number` really does become **30** — just as `*p = 100` changed `number`
earlier, but now with `p` standing in between. The last two lines show that `*pp`, meaning
`p` itself, can be reassigned: the assignment `*pp = &other` turns `p` into a pointer at
`other` — through `pp`, without touching `p` directly. This distinction matters whenever
a function needs to change the caller's own pointer: a function taking `int *` can only
change the pointed-to value, while a function taking `int **` can turn the caller's pointer
toward a different address. The class is still `language`: all three are local variables,
none of them allocated.

## The Bounding Measurement

A pointer's type does not say **who will free** the block it points to. An `int *` can
point at a local variable just as well as at a block allocated with `count_alloc` — in the
source the two look identical, and the compiler draws no distinction between them. In the
two runs above, the `p` variable pointed at three different things at three different
moments: one local variable, another local variable, and (in the core) an allocated block.
The declaration was the same all three times (`int *`). Someone reading a pointer **cannot
infer** its ownership class from its type alone — that information can only be learned by
looking at where the variable came from (a `&` expression or an allocation call), or by
reading the contract that documents it. The rest of the course measures how that contract
is written, and how it is broken.

## Summary

- The owner of a memory block is one of four classes: **language**, **caller**, **callee**,
  **unowned**; this lesson measures the **language** class with a full run of the core.
- The five-file measurement core (`counter.h`, `counter.c`, `owner.h`, `owner.c`,
  `ownership.c`) distributes thirteen observations as **4 language · 4 caller · 3 callee ·
  2 unowned**; the total diff is **2**.
- Address-of (`&`) produces a variable's address; dereference (`*`) reads or writes the
  value at that address — both can be measured with equality and value, without ever
  printing the address.
- A pointer can be reassigned and can point at different variables at different times;
  that is the difference between the pointer itself and the value it points to.
- A pointer's type does not say **who will free** the block it points to — the same
  `int *` declaration can point at either a local variable or an allocated block.

## Next Step

The `local array` row measured in this lesson gave a `last - first` difference of **3**,
but how that difference is computed was left unexamined. The next lesson, Pointer
Arithmetic, measures why that difference is counted in **elements** rather than bytes, and
how incrementing a pointer advances according to its type's size — still within the
`language` class, but now looking at the relationship between an array's elements rather
than a single value.
