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.