Embedded C

Pointers

A pointer is just a variable holding a memory address — but that one idea underpins register access, interrupt vector tables, driver callback APIs, and most of the bug classes that make C notorious. Embedded code leans on pointers harder than most application code ever does, because talking to hardware is reading and writing specific memory addresses.

Quick reference
What it is
A variable whose value is a memory address, typed so the compiler knows how to interpret what's stored there.
Mental model
A pointer is a sticky note with an address on it, not the thing at that address — losing track of whether you're holding the note or the thing itself is where most pointer bugs come from.
Key numbers
p + 1 advances by sizeof(*p) bytes, not one byte — unless p is a char*.
Reach for it when
Passing large data efficiently, accessing hardware registers, or building a callback-based driver API — not as a default way to pass every argument.

Address-of and dereference

int x = 42;
int *p = &x;   // p now holds x's address
*p = 7;        // writes through p — x is now 7, not p itself
Variable x lives at address 0x1000 holding the value 42; pointer p lives at a different address, 0x2004, and its own value is 0x1000 — the address of x address: 0x1000 x value: 42 address: 0x2004 p value: 0x1000 p points to x p's own value is just a number — an address — stored at p's own, separate location in memory.
A pointer variable has two addresses worth tracking: where it lives, and the address it holds — confusing the two is the root of most pointer confusion for beginners.

Pointer arithmetic

Adding 1 to a pointer advances it by sizeof the pointed-to type, not by one byte — int *p; p + 1; moves 4 bytes forward on a platform where int is 4 bytes, because that's the address of the next int, which is what pointer arithmetic is actually for.

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
printf("%d\n", *(p + 2));   // 30 — the third element, arr[2]

Array decay and the sizeof trap

An array passed to a function decays to a pointer to its first element — the function receiving it never actually sees the array's full size, only a pointer. This produces one of the most common beginner bugs in all of embedded C:

void print_size(int arr[]) {
    printf("%zu\n", sizeof(arr));   // prints 4 or 8 — the pointer's
                                     // size, NOT the array's size!
}

int main(void) {
    int numbers[10];
    printf("%zu\n", sizeof(numbers)); // prints 40 (10 * sizeof(int)) — correct, full array
    print_size(numbers);               // decayed to a pointer at the call boundary
}

This is exactly why a function that needs to know an array's length has to be told explicitly, as a separate parameter — sizeof cannot recover information that's already been lost by decay.

Function pointers

A pointer can hold the address of a function, not just data — declared with the return type and parameter list built into the pointer's own type: void (*callback)(int);. This is the mechanism underneath driver callback APIs (a UART driver's "on_receive" hook) and, more fundamentally, an MCU's own interrupt vector table — literally an array of function pointers stored at a fixed address that the CPU jumps through the moment an interrupt fires.

typedef void (*uart_callback_t)(uint8_t byte);

void register_rx_callback(uart_callback_t cb) {
    rx_callback = cb;          // stash the function pointer
}

void UART_RX_Handler(void) {
    if (rx_callback != NULL) {
        rx_callback(read_data_register());   // call through the pointer
    }
}

Pointers to pointers

A pointer to a pointer (int **pp) is how a function modifies a caller's own pointer — passing a plain int * by value only lets the callee change what it points to, not make the caller's pointer variable itself point somewhere else.

void allocate_buffer(uint8_t **out_ptr, size_t size) {
    *out_ptr = malloc(size);   // sets the CALLER's pointer variable
}

uint8_t *buf = NULL;
allocate_buffer(&buf, 64);   // buf now actually points at the new memory

void pointers

void* is a generic pointer with no known pointed-to type — it can hold the address of anything, but can't be dereferenced directly without first casting it to a concrete type. It's what malloc returns (a block of memory with no inherent type until the caller decides), and what callback APIs commonly use for a generic "context" or "user data" parameter passed through untouched.

Real-world applications

Gotchas

Field notes