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.
- 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 + 1advances bysizeof(*p)bytes, not one byte — unlesspis achar*.- 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
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
- Hardware register access — a pointer to a fixed address, qualified
volatile, is how C reads and writes memory-mapped peripherals — the full mechanism is covered in Registers & volatile. - Driver "operations" structs — a struct of function pointers acting as a simple interface/vtable is the same pattern Linux device drivers use — see Linux drivers.
- DMA buffer handoff — handing a peripheral a pointer to a buffer it will write into directly, with no CPU involvement per byte, is a pointer being passed to hardware itself, not just to other code.
Gotchas
- Dereferencing an uninitialized ("wild") pointer is undefined behavior that can "work" by accident during testing and fail unpredictably once deployed.
- Returning the address of a local variable hands back a pointer into stack memory that's about to be reused — see Storage Classes.
- Off-by-one pointer arithmetic that walks one element past the end of an array is a buffer overrun even though the arithmetic itself "looks" correct.
- Comparing pointers into two unrelated arrays is undefined behavior in strict C, even when the comparison superficially compiles and appears to work.