Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Pointer to Pointer and void Pointer in Embedded C: Double Pointers, NULL and Dangling Pointers
Double pointers and T ** parameters, arrays of pointers, the generic void *, opaque contexts, NULL rules, wild and dangling pointers, safe_free, and static pools instead of malloc. Keep this in mind whenever you apply pointer to pointer and void pointer in embedded C.
What You Will Learn
int **) is and how to read itT ** to change the caller’s pointerargv-style tablesvoid *: what it allows and what it forbidsvoid * and a sizesafe_free pattern and why embedded code prefers poolsKey Terms
1. Pointer to Pointer: int **pp
A pointer is a variable, so it also has an address, and that address can be stored in another pointer. A pointer to pointer (double pointer) declared int **pp holds the address of an int *. Each star adds one level of indirection: pp is the address of a pointer, *pp is that pointer, **pp is the int. Many bugs with pointer to pointer and void pointer in embedded C start here.
Notice the two different effects: **pp = 99 changes the int; *pp = &other changes the pointer p itself, so it now refers to another variable. This is the core rule of pointer to pointer and void pointer in embedded C.
2. Changing the Caller’s Pointer: T ** Parameters
Lesson 8.2 showed that a function receives a copy of its pointer arguments. To let a function move the caller’s pointer, pass the address of the pointer. The parameter is then a pointer to pointer, and the function assigns through *param. Reviewers check pointer to pointer and void pointer in embedded C closely.
pool_get shows the second typical use: a function that returns a pointer through a parameter so that the return value can report success or failure. This is how many embedded APIs hand out buffers or descriptors. Interviewers test pointer to pointer and void pointer in embedded C on exactly this point.
| Goal | Parameter type | Call |
|---|---|---|
| Change an int | int * | f(&x) |
| Change a pointer to int | int ** | f(&p) |
| Advance a cursor over const text | const char ** | f(&cursor) |
| Receive a pointer to a buffer | char ** | f(&buf) |
3. Arrays of Pointers versus Two-Dimensional Arrays
An array of pointers stores addresses; a two-dimensional array stores the data itself. For tables of strings the difference matters in RAM and in flexibility. Master pointer to pointer and void pointer in embedded C by practising this case.
| Array of pointers | 2D array | |
|---|---|---|
| Memory | 4 pointers + the string literals (Flash) | All data in the array (RAM if not const) |
| Rows of different length | Yes, no waste | Padded to the declared row length |
| Rows swappable cheaply | Yes: swap two pointers | No: copy whole rows |
Can be const in Flash | Yes: static const char *const t[] | Yes: static const char t[][8] |
int main(int argc, char *argv[]) is an array of pointers to the argument strings, equivalent to char **argv. Embedded programs have no command line, but command parsers use the same shape.4. The Generic Pointer: void *
A void * can hold the address of any object. It carries no type information, so the compiler cannot know the size of what it points to. The consequences: Remember this when you review pointer to pointer and void pointer in embedded C in code reviews.
- Any object pointer converts to
void *and back implicitly in C, without a cast. (C++ requires a cast.) - It cannot be dereferenced: you must first convert it to a typed pointer.
- Pointer arithmetic on
void *is not standard C. GCC allows it as an extension, treatingsizeof(void)as 1; do not rely on it. Useuint8_t *for byte arithmetic. - It is the basis of generic code:
memcpy,qsortand callback contexts all use it.
One function swaps ints, doubles and structures because it works on raw bytes; the caller supplies the size. The price is type safety: nothing stops you from calling swap_any(&a, &x, sizeof a) with two different types. Generic drivers depend on pointer to pointer and void pointer in embedded C being understood.
5. void * as a Callback Context and in Handles
Lesson 8.4 used void *ctx to pass an object through a library that knows nothing about it. The same technique appears in RTOS task parameters (void *arg), HAL handles and event queues. The rule: the code that creates the context and the callback that receives it agree on the real type; the library in between treats it as an opaque pointer.
6. NULL: The Null Pointer
NULL is a macro for the null pointer constant (an integer constant expression with value 0, or such an expression cast to void *). Assigning it to any pointer gives a pointer that compares unequal to every valid object address. Rules:
- Initialise unused pointers to NULL; test with
if (p)orif (p != NULL). - Dereferencing NULL is undefined behavior; on the STM32F407 a read often succeeds (address 0 aliases Flash), a write is ignored or faults.
- Functions that return a pointer use NULL for failure; callers must check.
- NULL is not the same as address zero on every platform; always write
NULL(or0in a pointer context), never compare with a hand-written address. - Passing NULL to
memcpy,strlenand friends is undefined even with a length of 0; check first.
7. Wild and Dangling Pointers
| Kind | Cause | Symptom | Prevention |
|---|---|---|---|
| Wild | Used before initialisation | Writes to a random address | Initialise at declaration |
| Dangling (scope) | Address of a local returned or stored | Data changes unexpectedly | Never return &local; use static or caller buffers |
| Dangling (free) | Used after free() | Corrupted heap, random crashes | Set to NULL after free |
| Dangling (realloc) | Old pointer after realloc moved the block | Reads stale data | Use only the returned pointer |
| Out of range | Index or arithmetic beyond the object | Overwrites neighbours | Carry the length, check bounds |
A pointer-to-pointer gives a neat defence: a helper that frees a block and clears the caller’s pointer in one step.
int ** to void ** by a cast is technically not guaranteed to work (the two pointer types need not have the same representation). It does on all common compilers including arm-none-eabi-gcc, which is why this idiom is widely used, but a macro that takes the pointer variable itself is strictly portable: #define SAFE_FREE(p) do { free(p); (p) = NULL; } while (0).8. Dynamic Memory in Embedded C: Prefer Pools
malloc and free are available in newlib, but most embedded projects avoid them: the heap fragments over weeks of uptime, allocation time is unpredictable, failure must be handled everywhere, and safety standards (MISRA C rule 21.3) forbid them. The usual replacement is a static pool: a fixed array of equally sized blocks with a free list. Allocation and release take constant time and the total memory is known at link time.
9. Common Mistakes with Pointer to Pointer and void *
| Mistake | Result | Fix |
|---|---|---|
Passing p instead of &p to a T ** parameter | Wrong type or crash | Pass the address of the pointer |
Dereferencing a void * | Compile error | Convert to a typed pointer first |
Arithmetic on void * | GCC extension, not portable | Use uint8_t * |
Casting away const in a void * round trip | Writes to read-only data | Keep const void * for inputs |
| Using a pointer after free | Undefined behavior | Set to NULL; prefer pools |
Converting char ** to const char ** | Compiler warning (unsafe) | Use const char *const * or a cast with care |
The conversion is unsafe: through the const char ** the callee could store a pointer to a const object into s, and the caller could then modify it. Declare the parameter as const char *const * if the callee only reads.
10. Best Practices
- Use
T **only for real needs: changing the caller’s pointer or returning a pointer through a parameter. - Prefer a return value, or a struct, when only one result is needed.
- Use
const void *for inputs andvoid *only for data the callee writes. - Always pair a
void *buffer with a size. - Check every pointer from outside for NULL; document whether NULL is allowed.
- Avoid
malloc/freein firmware; use static pools or static buffers. - Set pointers to NULL when the object they refer to ends.
- Keep the levels of indirection low; more than two stars is a design smell.
Key Takeaways — Lesson 9.3
int **ppholds the address of anint *;*ppis the pointer,**ppthe int.- To change a pointer in the caller, pass its address and assign through
*param. - An array of pointers stores addresses (flexible, small); a 2D array stores the data.
void *holds any object address, cannot be dereferenced and has no standard arithmetic.- NULL means no object; check before use.
- Wild and dangling pointers cause undefined behavior; initialise, clear after free and prefer pools.
Frequently Asked Questions
Ready to practise?
Next: worked examples with cursors, string tables, generic helpers, a block pool and a linked list built with double pointers.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments