Pointers in Depth
Introduction
You know that a pointer holds an address. Now the question is what you do with that power when functions, arrays, and callbacks enter the picture.
Most beginner bugs in embedded C come from this area: "I passed my variable to a function but it did not change", "my array works in main but not inside the function", or "this pointer worked once and then random things broke". This article explains why those happen and how experienced firmware code avoids them.
Why bother with pointers at all?
Without pointers, every function argument is a copy. That is fine for a single int, but imagine copying a 200-byte sensor packet on every function call — slow and wasteful on a small MCU. Pointers let functions look at the same memory you already have.
| Situation | Without pointers | With pointers |
|---|---|---|
| Swap two variables | Need clumsy workarounds | Pass addresses |
| Large struct | Copy entire struct | Pass one address |
| Modify caller data | Impossible by value | *p = new_value |
| Hardware register | Cannot "pass" hardware | Pass or use fixed address |
| Callback on interrupt | — | Function pointer |
Call by value — the photocopy problem
1 2 3 4 5 6 7 8 9 10 11 | |
When you call swap(a, b), C makes photocopies of a and b into p and q. Swapping the photocopies does not change the originals on your desk.
In plain terms
You gave your friend a copy of your house key diagram. They redraw their copy — your real key does not move.
Call by reference — give the real address
1 2 3 4 5 6 7 8 9 10 11 | |
&a means "address of a". Inside swap, *p reads and writes the real a in main.
This pattern appears everywhere: HAL_UART_Receive(&huart, buf, len, timeout) — the driver needs your buffer's address so it can fill it in place.
Arrays and functions — why size disappears
When you pass an array to a function, C does not pass the whole array. It passes a pointer to the first element only. The function has no idea how long the array is unless you tell it.
1 2 3 4 5 6 7 8 9 10 11 | |
These declarations mean the same thing to the compiler:
1 2 | |
p[k] is simply shorthand for *(p + k) — the value at offset k from the start.
Modifying the caller's buffer
1 2 3 4 5 6 7 | |
The pointer p starts at the first character of the caller's string and walks forward. Every *p = ... changes the original buffer in main. There is no copy.
Lesson: if a function should fill or change your data, pass a pointer (and the buffer size for safety).
Returning more than one value
A C function can only return one value directly. For quotient and remainder, use output parameters:
1 2 3 4 5 6 7 8 9 10 | |
You pass addresses of q and r; the function writes through them. HAL functions often use this style: HAL_GPIO_ReadPin(...) returns state; more complex APIs return status through pointer arguments.
Returning pointers — the dangling trap
Wrong: address of a local variable
1 2 3 4 5 6 | |
m and n exist only inside getMax. When the function returns, that stack space is reused for something else. The pointer you return points at a mailbox that already has new mail — dangling pointer.
Right: point at caller's variables
1 2 3 4 5 6 7 8 9 10 11 12 | |
x and y live in main — they stay valid. Alternatively return data through an argument, use static storage carefully (not re-entrant), or heap memory you manage.
Pointer to pointer — when one level is not enough
Sometimes you need a pointer to a pointer:
1 2 3 | |
Array of strings is the classic case:
1 | |
seasons is an array of four pointers. Each pointer points at the first letter of a string. Passing to a function:
1 2 3 4 5 6 | |
q[k] is the k-th string. *(q[k] + 2) would be the third character of that string — useful for parsing command tokens in a UART CLI.
Function pointers — choosing which function runs
A function pointer stores the address of a function, not data. You can call it like a normal function:
1 2 3 4 5 6 7 8 9 10 11 12 | |
Read the type inside-out: pf is a pointer to a function that takes two int and returns int.
Embedded uses:
- ISR registration — vector table holds addresses of handlers
- HAL callbacks —
void (*callback)(void)runs when transfer completes - State machines — table of functions, one per state action
- RTOS — task entry point is a function pointer
In plain terms
Instead of pressing a fixed button on a remote, you program which button the remote calls. At runtime you can switch between "add mode" and "multiply mode" by changing the function pointer.
Relevant topics
Starting points
- Prove call-by-value failure with
swap— then fix with pointers. - Write
makecapitalsand pass a buffer fromfgets— see bytes change in debugger. - Explain aloud why
return &mis illegal for a localm. - Sketch a tiny state machine with three function pointers in an array.
Focus points
- Always pass array length — the compiler does not do it for you.
- Never return pointers to locals unless you understand lifetime.
- Function pointer signature must match exactly.
- Use
constwhen the function must not modify data:void print(const char *s).
Key points
- Call by value copies; pointers let functions change caller data.
- Array parameters are pointers — size is lost, pass
nexplicitly. - Output parameters return multiple values through addresses.
- Function pointers enable callbacks and runtime dispatch — common in HAL and RTOS.