Pointer Wizardry

C’s declaration syntax was designed in the 1970s under the principle “declaration mirrors use.” It made sense in a small language; it aged into a legendary reading exercise. This file is your survival guide: function pointers, int (*(*fp)(int))[3] decoded end to end, and the spiral rule so you can read anything without begging cdecl.org (though you should still use cdecl.org — that’s not weakness, that’s tooling).

Function Pointers

A function pointer holds the address of executable code. Declaration syntax mirrors call syntax:

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

int (*fp)(int, int);   // fp is a pointer to a function taking (int,int) returning int
fp = add;              // or: fp = &add; both work, function names decay to pointers
int r = fp(2, 3);      // or: int r = (*fp)(2, 3); same thing

The parens around *fp are mandatory. Without them:

int *fp(int, int);     // fp is a FUNCTION taking (int,int) returning int*

That’s not a pointer at all. That’s the second-most-common C bug in senior studies.

Typedefs Are Almost Always Better

typedef int (*BinOp)(int, int);
BinOp fp = add;
int r = fp(2, 3);

You do this once at the top of the file; everywhere else looks readable. The old-school C convention was to typedef the function type (typedef int BinOp(int,int); BinOp *fp;) to make the pointer-ness visible at each use site, but the pointer typedef (BinOp fp;) is what most modern C code does.

Callbacks with a Context Pointer

The standard idiom for callbacks in C is a function pointer plus a void* context:

typedef int (*Cmp)(const void *a, const void *b, void *ctx);

int for_each_sorted(int *arr, size_t n, Cmp cmp, void *ctx) { /* ... */ }

This is what qsort_r (POSIX) and modern C11 qsort_s provide over the older qsort — the missing context pointer is why old qsort callbacks routinely need global state.

Pointers to Arrays vs Arrays of Pointers

This is the confusion that eats freshmen alive. Two declarations, look almost identical, mean completely different things:

int *arr[10];       // arr is an array of 10 pointers to int
int (*ptr)[10];     // ptr is a pointer to an array of 10 ints

Memory layout:

int *arr[10]:                   int (*ptr)[10]:
+---+---+---+---+---+   ...     +---+
|p0 |p1 |p2 |p3 |p4 |           |ptr|---> [i0 i1 i2 i3 i4 i5 i6 i7 i8 i9]
+---+---+---+---+---+           +---+
 |   |   |   |   |
 v   v   v   v   v
[i] [i] [i] [i] [i]

arr[i] is an int*; you dereference with *arr[i]. (*ptr)[i] is an int; the parens are required because [] binds tighter than *.

When do you use int (*)[10]? Passing a 2D array to a function:

void print_row(int (*row)[10]) {
    for (int i = 0; i < 10; i++) printf("%d ", (*row)[i]);
}

int matrix[3][10];
print_row(&matrix[0]);

Or equivalently, void print_row(int row[10]) — which decays the same as int *row. The int (*)[10] form preserves the size, which matters for pointer arithmetic on the outer dimension.

The Spiral / Right-Left Rule

For any C declaration, read it in a spiral starting from the innermost identifier and going right when you can, left when you can’t, using this dictionary:

  • * → “pointer to”

  • [N] → “array N of”

  • [] → “array of”

  • (args) → “function taking args returning”

The rule: start at the identifier. Go right if you can (through [] and ()). When blocked by ), go left. Keep spiraling out.

Decoding int (*(*fp)(int))[3]

Let’s decode it slowly.

  1. Identifier is fp. Start there.

  2. Immediately to the right: ). Blocked. Go left.

  3. Immediately to the left: *. So fp is a pointer to

  4. We’re inside (*fp). Exit the parens. Go right.

  5. Right: (int). So we have … function taking int returning

  6. Right again: ). Blocked. Go left.

  7. Left: *. So … pointer to

  8. Exit that outer paren. Go right.

  9. Right: [3]. … array 3 of

  10. Right: nothing but the type qualifier. Base type is int.

Assemble: fp is a pointer to a function taking int returning a pointer to an array 3 of int.

Sanity-check against cdecl.org:

$ echo 'explain int (*(*fp)(int))[3]' | cdecl
declare fp as pointer to function (int) returning pointer to array 3 of int

Perfect match. Congratulations, you’re now literate.

void* and Its Rules

void* is C’s generic pointer type. You can:

  • Convert to and from any object pointer type without a cast (in C; in C++ you need a cast).

  • Pass it to memcpy, memset, free, and it works.

You cannot:

  • Dereference it. *(void*)p is a syntax error.

  • Do arithmetic on it (portably). ((void*)p) + 1 is a GCC extension; standard C doesn’t define sizeof(void).

The idiom is char* (or uint8_t*) for byte-wise arithmetic; convert to T* when you need typed access.

void *base = /* ... */;
uint8_t *b = base;         // implicit conversion, fine
Header *h = (Header*)(b + 4);

Const Placement (The Rule Nobody Explains)

const int *p;              // p points to const int  \  same
int const *p;              // p points to const int  /
int *const p;              // p is a const pointer to int
const int *const p;        // both

The rule: const binds to whatever’s to its left, unless there’s nothing to its left, in which case it binds to whatever’s to its right.

  • const int * — reading right-to-left: pointer to int that is const. Data is const, pointer is not.

  • int *const — pointer, const, to int. Pointer is const, data is not.

You’ll see both in real code. const int *p is far more common because functions typically want to promise “I won’t modify what you point to” without also promising “I won’t advance my copy of the pointer.”

Restrict: The Alias-Free Promise

C99 introduced restrict as a promise to the compiler: “for the lifetime of this pointer, the only way to access the pointed-to object is through this pointer or one derived from it.” The compiler uses this to skip reload-after-write and enable vectorization.

void axpy(size_t n, float a, const float *restrict x, float *restrict y) {
    for (size_t i = 0; i < n; i++) y[i] += a * x[i];
}

Without restrict, the compiler must assume y[i] writes might alias x[i] reads and can’t vectorize. With it, you’re promising they don’t overlap, and the compiler can generate SIMD code. If you lie, undefined behavior.

You’ll see restrict all over BLAS, image processing, and any C code that means business about performance. Add it whenever you can honestly promise it.

What Most People Get Wrong About This

They memorize declarations instead of learning to read them. The spiral rule is 30 seconds to learn and it means you’ll never be blocked by a declaration again. Then you use cdecl.org anyway, because you’re not being graded on flexing — you’re being graded on shipping correct code. Same with function pointer typedefs: make the code readable, don’t be a purist.


Return to README.md · Next: projects.md