Pointers, Arrays & Strings
The heart of C — and the source of most of its bugs. A pointer is an address plus a type. The type decides how big a step is, what you are allowed to read, and what the compiler checks for you.
2.1 The two operators: & and *
int value = 42;
int *p = &value; /* & = "address of" — p holds value's address */
*p = 43; /* * = "follow the pointer" — writes through p */
/* value is now 43 */
int *q = NULL; /* NULL = "points nowhere". Never dereference it. */
if (p != NULL) { *p = 0; } /* check first, dereference second */- A pointer has two things: an address (where) and a pointed-to type (what).
int *andchar *holding the same address mean different things — the compiler reads/writes different sizes through them. *in a declaration is not the dereference operator.int *p;declares “*pis anint”. Write the*next to the name (int *p), not the type —int* a, b;declaresaas pointer,bas plainint.- Initialise every pointer.
int *p;holds garbage.int *p = NULL;fails loudly and testably instead of corrupting a random address.
2.2 Pointer arithmetic: steps are sized by the type
p + 1 does not add one byte — it adds sizeof(*p) bytes, moving to the next element. This is what makes pointers and arrays interchangeable in expressions, and what makes a wrong pointer type silently read the wrong bytes.
🧭 Pointer arithmetic visualizer
uint32_t buf[6] = { 10, 20, 30, 40, 50, 60 };
uint32_t *p = buf; /* array decays to &buf[0] */
p[2] == *(p + 2); /* indexing IS pointer arithmetic */
*(p + 2) == 30;
p++; /* now points at buf[1] */
p - buf == 1; /* pointer subtraction counts ELEMENTS, not bytes */
&buf[5] - &buf[0] == 5;Out-of-bounds arithmetic is undefined behavior — even forming a pointer more than one past the end, without dereferencing it, is UB. One-past-the-end is allowed (as a loop sentinel); two-past is not.
2.3 Array decay: where sizeof stops working
int buf[10];
sizeof buf; /* 40 — whole array, in this scope only */
void f(int a[]) {
sizeof a; /* 4 or 8 — POINTER size! a decayed on entry */
}
void f_good(int *a, size_t len) {
for (size_t i = 0; i < len; i++) { /* ... */ }
}- Decay exceptions: an array does not decay when it is the operand of
sizeof,&, or a string-literal initialiser. Everywhere else, it becomes a pointer to its first element. &bufvsbuf: same address, different types.bufdecays toint *(step = oneint);&bufisint (*)[10](step = the whole array, 40 bytes).- String literals are read-only.
char *s = "hello"; s[0] = 'H';is undefined behavior. Useconst char *s = "hello";or a mutable arraychar s[] = "hello";.
2.4 const and pointers: read it right-to-left
const int *p; /* pointer to const int: *p read-only, p movable */
int * const p; /* const pointer to int: p fixed, *p writable */
const int * const p; /* both fixed */
void print_buf(const uint8_t *buf, size_t len); /* "I will not modify buf" */Rule: read the declaration right-to-left — const applies to what is on its left (or to the right if nothing is on the left). const on a function parameter is a promise to callers and a compiler-checked guard against accidental writes. It does not make the data immutable — another non-const pointer to the same memory can still change it.
2.5 Pointers to pointers: changing the caller’s pointer
/* A function gets a COPY of its arguments — including pointers.
To change the caller's pointer, pass a pointer TO the pointer. */
bool alloc_buf(uint8_t **out, size_t n) {
*out = malloc(n);
return *out != NULL;
}
uint8_t *buf = NULL;
if (!alloc_buf(&buf, 64)) { return ERR_NOMEM; }
free(buf); buf = NULL;- One level per job:
int *lets a function change anint;int **lets it change anint *. The same pattern appears inmain(int argc, char **argv)(an array of strings) and inchar **tables. void *is the generic pointer. It converts to/from any object pointer implicitly, but you cannot dereference or do arithmetic on it — cast to a concrete type first:uint8_t *bytes = data;- Function pointers are covered in depth in Stage 8 (Interfaces & Polymorphism) — the same idea, applied to behavior instead of data.
Never return a pointer to a local. int *f(void) { int x = 5; return &x; } hands back an address whose stack frame is gone. The pointer dangles before the caller uses it. Return the value, take an out-pointer, or allocate — see Stage 3.