← C for People Who Already Know How to Code

C for People Who Already Know How to Code

Pointers and Arrays

Arrays decay, pointers don't grow back

An array expression, almost anywhere it's used, silently converts to a pointer to its first element. This is called array decay, and the classic place it bites is sizeof:

c
void f(int arr[10]) {
	printf("%zu\n", sizeof(arr)); // prints 8 (or 4): sizeof(int*), NOT 40
}

int main(void) {
	int arr[10];
	printf("%zu\n", sizeof(arr)); // prints 40: the real array size, here in its defining scope
}

int arr[10] as a function parameter is quietly rewritten by the compiler to int *arr. The [10] is documentation, not enforcement. Any bound checking has to be passed in separately (an explicit length parameter) because the function has no way to recover it from the pointer.

Try it yourself: watch sizeof change the moment an array crosses a function boundary

Write a function taking int arr[10] and print sizeof(arr) inside it, then print sizeof of the same array in main. Confirm the numbers don't match, and that -Wall doesn't warn you about it. This one is by design, not a bug the compiler can catch.

Pointer arithmetic only has meaning inside (and one past) an array

ptr + 1 is only defined if ptr points within an array (or one element type) and ptr + 1 lands within the array or exactly one element past its end. Computing anything further, even without dereferencing it, is undefined behavior:

c
int arr[5];
int *p = arr + 5;   // legal: one-past-the-end, may be compared but not dereferenced
int *q = arr + 6;   // UB the moment this pointer is *computed*, even if you never touch *q

Multi-dimensional arrays vs arrays of pointers

int grid[3][4] is one contiguous 48-byte block. int *grid[3] is three separately-allocated rows reachable through an array of pointers. They're both "2D" in casual conversation and completely different in memory, which matters the moment you write a function signature for one:

c
// correct: the compiler needs the inner dimension to compute row strides
void sum_rows(int arr[][4], int rows) {
	for (int i = 0; i < rows; i++)
		for (int j = 0; j < 4; j++)
			/* arr[i][j] */;
}

// this does NOT work for a true 2D array, it's the signature for an array of pointers
void wrong(int **arr, int rows);

Reading pointer declarations right to left

const next to a pointer means different things depending on which side of the * it's on. Read the declaration starting from the variable name, moving outward:

  • const int *p: pointer to a const int. You can repoint p, you can't modify *p.
  • int *const p: const pointer to an int. You can modify *p, you can't repoint p.
  • const int *const p: both. Can't repoint, can't modify what it points to.

Function pointers, briefly

A typedef makes these tolerable to read, and they're the backbone of any callback or dispatch-table pattern in C: there's no closures, no first-class functions, just addresses of functions with a matching signature:

c
typedef int (*binop_fn)(int, int);

int add(int a, int b) { return a + b; }

binop_fn op = add;
int result = op(2, 3); // 5
Try it yourself: build a tiny opcode dispatch table

Define binop_fn add, sub, mul functions, put them in an array indexed by an enum { OP_ADD, OP_SUB, OP_MUL }, and call through table[op](a, b) instead of writing a switch. This pattern is how a lot of real interpreters and VMs dispatch instructions.