← roadmap
subject · The Machine & Its Tongue

C From the Metal

Types, pointers, arrays, structs, and functions — as memory you can see

6–9 hours

C has a reputation for being hard and scary. It isn't — it's honest. C is a thin, human-readable skin over the machine, and everything confusing about it becomes obvious the moment you stop treating it as syntax and start seeing the memory underneath. This subject teaches you to do exactly that. By the end you won't just 'know C syntax' — you'll look at a line of C and see the bytes it moves, and you'll prove it by building a growable array from nothing, the same data structure behind Python's lists and C++'s vector. Read it start to finish; each part is built from the one before.

After this subject you can
  • › Read a line of C and picture the exact memory it touches
  • › Use pointers, arrays, and structs correctly — and explain why the tricky cases behave as they do
  • › Reason about the heap, and manage memory without leaks
  • › Build a dynamic array in C from scratch, and understand every line of it

1 · A type is just a size

People talk about C types like they're a deep mystery. They're not. A C type is two facts: how many bytes the value takes, and how those bytes are read (as an unsigned number, a signed number, or a float). That's the whole thing. Everything else follows.

TypeSize (bytes)Read as
char1one byte (a small integer, or a character)
int4 (usually)signed whole number
long8 (Linux/mac)signed whole number
a pointer (T*)8 (on 64-bit)an address
double8a floating-point number

"How many bytes" is why a char holds 0–255 and an int holds billions — more bytes, more room. "How it's read" is why the same byte pattern can be 255 or −1 depending on whether the type is unsigned or signed. When you later hear "this variable is 4 bytes," that means it occupies 4 of the memory cells the machine addresses. A type is a *label on some bytes that says how to treat them* — nothing more.

Note
Don't trust "usually." The exact sizes can differ by platform, so a careful programmer asks the compiler with `sizeof`: sizeof(int) gives the real byte count here. And when the exact width matters (it will), use the precise names from <stdint.h>: uint8_t is unmistakably one unsigned byte, everywhere.

2 · A pointer is an address

Here's the thing everyone fears, made simple. Memory is one giant row of numbered boxes (bytes), each with an address. A pointer is just a variable that holds one of those addresses. It's 8 bytes holding "the location of something else." Its type (int*, char*) records *what kind of thing* lives at that address — how many bytes to read there.

Two operators, and both are ordinary once you see them as memory:

  • `&x` ("address of x") gives you the address where x lives — the number of its box.
  • *p** ("what p points at") means "go to the address in p and use the value there." Reading
  • *p reads those bytes; writing *p = 5 writes into them.
pointer.cc
1int x = 42;
2int *p = &x; // p holds the address of x
3int y = *p; // y = the value at that address = 42
4*p = 99; // write 99 into x's box, through the pointer
5// now x == 99, because p pointed at x
p points at x
   name  address     value
   x    0x7ffe1c  ->  42        <-- *p reads/writes HERE
   p    0x7ffe10  ->  0x7ffe1c  (p's value is x's address)

   &x  = 0x7ffe1c   (the address of x)
   *p  = the value at 0x7ffe1c = 42
   *p = 99  changes x to 99, because they are the same box
Key idea
Keep two things straight and pointers stop being scary: `p = ...` changes the *pointer* (which box it points to); ***p = ...** changes *what's in the box it points to*. That single distinction is most of what trips people up. This is also how a function changes its caller's variable: pass &x, and the function writes through the pointer into your x.

3 · Arrays are pointer arithmetic

An array is a block of equal-sized values sitting back-to-back in memory. The array's name, used in an expression, is really *the address of its first element*. And here is the definition that explains everything about arrays:

Key idea
a[i] means exactly *(a + i).** Take the array's starting address, add i, and read what's there. The subscript brackets are just sugar for pointer addition and a dereference. (This is why a[i] even equals i[a] — addition doesn't care about order.)

The one subtlety that makes it work: adding to a pointer counts in elements, not bytes. On a 4-byte int array, a + 1 moves 4 bytes to the next int; a + i moves i × 4 bytes. The compiler multiplies by the element size for you — that's why the pointer's type matters.

an int array; a+2 lands on a[2]
   a[0]    a[1]    a[2]    a[3]
  ┌──────┬──────┬──────┬──────┐
  │  10  │  20  │  30  │  40  │   each box = 4 bytes
  └──────┴──────┴──────┴──────┘
  0x1000 0x1004 0x1008 0x100c

  a + 2  = 0x1000 + 2*4 = 0x1008   (NOT 0x1002)
  *(a+2) = a[2] = 30
Watch out
C does no bounds checking. a[7] on a 4-element array cheerfully computes *(a+7) and reads memory past the end of your array — no error, just wrong (or dangerous). This is fast and unforgiving, and it's the root of a whole class of bugs. When you pass an array to a function it decays to a bare pointer with no length attached, which is why C functions always take a size argument.

4 · Structs: fields in a row, with gaps

A struct groups several values into one object. In memory it's just its fields laid out in order, each at some offset from the start. To read a field, the compiler adds that field's offset to the struct's address. Simple — with one twist: padding.

Hardware reads a value fastest when it sits at an address that's a multiple of its size (a 4-byte int at an offset divisible by 4). So the compiler quietly inserts unused padding bytes to line each field up — which is why a struct's size can be *bigger* than the sum of its fields.

struct.cc
1struct S {
2 char a; // 1 byte, at offset 0
3 int b; // 4 bytes, wants an offset divisible by 4
4 char c; // 1 byte
5}; // sizeof == 12, not 6
why struct S is 12 bytes
  offset: 0    1  2  3    4  5  6  7    8    9 10 11
          ┌────┬─────────┬────────────┬────┬────────┐
          │ a  │ PAD PAD │     b      │ c  │ PAD... │
          └────┴─────────┴────────────┴────┴────────┘
  a@0. b needs a multiple of 4, so 3 pad bytes -> b@4.
  c@8. Struct padded up to a multiple of 4 -> size 12.
Note
Because padding depends on order, reordering fields (biggest first) can shrink a struct with no change in meaning. You never guess the layout — you check it: sizeof(struct S) for the total, offsetof(struct S, b) for a field's offset. Derive, then confirm.
your turn — do it on paper
Hand-derive the layout of struct T { char a; double d; short s; }; — write each field's offset and the struct's total size, using the two rules (align each field; pad the whole struct to its largest alignment). Show the padding gaps.

5 · Function pointers: code is data too

One more idea, and it's a mind-opener: functions live in memory and have addresses, exactly like data. So you can store a function's address in a variable and call through it later. That variable is a *function pointer* — a pointer whose pointee is code.

fp.cc
1int add(int a, int b) { return a + b; }
2int sub(int a, int b) { return a - b; }
3
4int (*op)(int, int); // op: a pointer to a function taking (int,int) returning int
5op = add; // a function name decays to its address (like an array name)
6int r = op(3, 4); // call through the pointer -> add(3,4) -> 7
7op = sub;
8r = op(3, 4); // now -> sub(3,4) -> -1

Read int (*op)(int,int) from the inside out: op is a pointer, to a function taking two ints, returning an int. Assigning op = add stores add's *address* (a function name decays to its address, just like an array name decays to its first element). Then op(3,4) calls whatever address op holds. This is how dispatch tables, callbacks, and plug-in behavior work — you turn "which code runs" into data you choose at runtime.

Key idea
The whole subject in one line: data and code are both just addressable memory. A number, an array, a struct, and a function are all "bytes at an address," and C lets you hold any of their addresses in a pointer. Once you see that, C is no longer a maze of syntax — it's a clear window onto the machine.

The Project

Build a growable array of ints in C — a Vec — from nothing. It's the data structure behind Python's list and C++'s vector: it starts small and doubles its storage as you push more in. You'll use pointers, the heap, arrays, and structs together, and end with something real you understand completely.

Mandatory
  • 01

    A Vec struct holding: an int *data (a heap block), a size_t len (elements in use), and a size_t cap (elements the block can hold).

  • 02

    void vec_init(Vec *v) — start empty (data = NULL, len = 0, cap = 0).

  • 03

    void vec_push(Vec *v, int x) — append x; when len == cap, grow by DOUBLING capacity (0→4→8→16…) with realloc, then store.

  • 04

    int vec_get(Vec *v, size_t i) — return element i.

  • 05

    void vec_free(Vec *v) — free the block and reset the struct.

  • 06

    A main() that pushes 20+ ints and prints cap on each push, so the doubling (4, 8, 16, 32) is visible; then reads a few back and frees.

Constraints
  • •

    Only the C standard library (<stdlib.h> for malloc/realloc/free, <stdio.h> for printing). No external libraries.

  • •

    Every function that changes the vector takes Vec *v (a pointer) so it modifies the caller's struct — not a copy.

  • •

    You MUST reassign data from realloc's return value (it may move the block); never drop it.

  • •

    Compiles clean with -Wall -Wextra, and reports zero leaks and zero errors under valgrind.

Deliverable
vec.c
Build & check
gcc -O0 -g -Wall -Wextra vec.c -o vec && ./vec
valgrind --leak-check=full ./vec
You were taught everything for this

Everything this needs was taught above: a struct (section 4) holds the state; a pointer to the heap block (section 2) is the storage; indexing it is array arithmetic (section 3); and the sizes are just types (section 1). The one new fact — that `realloc` may move your block and returns its new address — is stated right in the constraints. Nothing here is a leap. If you get stuck, reread the section the stuck part comes from.

built the project?