Skip to content
academia.sh

Lesson 01 / 15

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.

Published Updated

In this lesson

  • 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
Contents

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.

// 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;
}
// 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.

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

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

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close