Pointers & Arrays
Addresses & Dereferencing
The address-of operator & produces a pointer to an object; the dereference operator * goes back from the pointer to the object. The type of &x is int * ("pointer to int"): it records both the address and the type of the thing it points to. A pointer that points nowhere is NULL — the one value you may compare with pointers safely, and the one you must check before dereferencing.
#include <stdio.h>
int main(void) {
int x = 42; // a plain int object on the stack
int *p = &x; // p holds the address of x; type: pointer to int
printf("x's value: %d\n", x); // 42
printf("p's address:%p\n", (void *)p); // some memory address
printf("deref *p: %d\n", *p); // 42 — *p means "the int at p"
*p = 99; // write THROUGH the pointer: x is now 99
printf("x is now: %d\n", x); // 99 — modified via p
return 0;
}
Figure 1 — p stores the address of x; *p reads the value stored at that address.
Dereferencing a NULL or an invalid pointer is undefined behavior — on most systems a crash, on some a security hole. The discipline: initialize pointers to NULL or a real address, check before use, and never dereference memory you do not own.
Pointer Arithmetic
Adding an integer to a pointer steps by the size of the pointee, not by one byte: p + 1 on an int * advances sizeof(int) bytes to the next int. This is how arrays and pointers unify — the whole array model of C is built on it.
#include <stdio.h>
int main(void) {
int arr[4] = {10, 20, 30, 40};
int *p = arr; // array decays to pointer to first element
printf("first: %d\n", *p); // 10 — p points at arr[0]
p++; // advance to the next int (4 bytes)
printf("second:%d\n", *p); // 20
p += 2; // two ints further
printf("fourth:%d\n", *p); // 40
// the pleasant surprise: p[i] is exactly *(p + i)
printf("arr[2] via ptr: %d\n", *(arr + 2)); // 30
return 0;
}
Pointer subtraction works too: p - arr yields the index distance between two pointers into the same array. Do not compare pointers from different arrays — that comparison is undefined.
Arrays Decay Into Pointers
An array name used as a value decays into a pointer to its first element. That is why arr and &arr[0] are the same pointer value — but they are not the same type as arr itself (which is "array of 4 int", a different beast that keeps its size). The famous consequence: inside a function parameter, int a[] is actually int *a, and sizeof(a) inside that function is the pointer size, not the array size. Always pass the length alongside the array.
#include <stdio.h>
// the parameter syntax "int a[]" is sugar for "int *a" — a decays
int total(int a[], int n) { // 'n' MUST travel with the array
int sum = 0;
for (int i = 0; i < n; i++) {
sum += a[i]; // a[i] is *(a + i) — pointer math again
}
return sum;
}
int main(void) {
int nums[] = {2, 4, 6, 8};
printf("total = %d\n", total(nums, 4)); // 20
return 0;
}
Pointer to Pointer
A int ** is a pointer that stores the address of another pointer. Two uses matter daily: mutating a caller's pointer from inside a function ("out parameter"), and building matrices as arrays of row pointers. The classic example is a function that must change which object the caller's pointer refers to — reach the caller's pointer through ** and assign through it.
#include <stdio.h>
// assign a = b by writing through a pointer-to-pointer
void swap_pointers(int **pa, int **pb) {
int *tmp = *pa; // deref once: the pointer a
*pa = *pb; // write through: now pa points to b's target
*pb = tmp;
}
int main(void) {
int x = 1, y = 2;
int *a = &x, *b = &y;
swap_pointers(&a, &b); // pass the ADDRESSES of the pointers
printf("a now points to: %d\n", *a); // 2 (was x, now y)
printf("b now points to: %d\n", *b); // 1
return 0;
}
Void Pointers
void * is the "generic pointer": it can hold any address without committing to a pointee type. The standard library's memory functions use it — malloc returns void * precisely because it does not know what you will store. You must cast it back before dereferencing or doing arithmetic, because arithmetic needs a pointee size.
#include <stdlib.h> // malloc, free
#include <stdio.h>
int main(void) {
void *raw = malloc(10 * sizeof(int)); // 10 ints, untyped handle
if (raw == NULL) { // always check allocation!
return 1;
}
int *nums = (int *)raw; // cast to the intended type
nums[0] = 7; // now arithmetic works: 4-byte steps
printf("first value: %d\n", nums[0]);
free(nums); // free the allocation, not the cast
return 0;
}
Pointers to Struct
Passing a struct by pointer avoids copying its bytes on every call and lets the callee modify the original. The arrow -> is shorthand for dereference-then-dot: p->field means (*p).field. This pattern is the foundation of every C data structure and the collections page.
#include <stdio.h>
struct Point { int x, y; };
// move the point in place — no copy, caller's struct is updated
void translate(struct Point *p, int dx, int dy) {
p->x += dx; // (*p).x += dx
p->y += dy;
}
int main(void) {
struct Point home = {0, 0};
translate(&home, 3, 4); // pass address, mutate original
printf("home = (%d, %d)\n", home.x, home.y); // (3, 4)
return 0;
}
Dangling Pointers — Return by Value Only
Never return the address of a local variable: its storage is reclaimed when the function returns, leaving a dangling pointer — reading through it is undefined behavior. Return the value, or allocate on the heap and transfer ownership (the memory page covers this properly).
int *bad(void) {
int local = 5;
return &local; // BUG: local dies here; the caller gets a dead address
}
Compilers warn about this with -Wall; sanitizers catch it at runtime. Both are why we enabled them on the setup page.
Const-Correct Pointers
The position of const changes the promise. Read right-to-left from the star to decode it:
| Declaration | Meaning |
|---|---|
const int *p | pointer to const int — may not write through p |
int *const p | const pointer to int — may not change p itself |
const int *const p | both: read-only object through a fixed pointer |
Declaring function parameters as const int * tells callers you will not mutate their data — and lets the compiler enforce that promise.
Next: functions, scope, and linkage — how C organizes executable code.