Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Pointer to Pointer Pitfalls
Understand double pointers, why functions cannot repoint your pointer, plus a quiz and interview questions. Free C programming course for embedded systems.
What You Will Learn in This Lecture
- What a pointer to pointer is and how
**ppreaches the final value - Why
p + 1andpp + 1move by different numbers of bytes - Why a function that changes the caller’s pointer needs a double pointer
- How to spot and fix the classic “my pointer did not change” bug
- A quiz and a set of interview questions that revise the whole Pointers chapter
Prerequisites
You should know pointer basics, pointer arithmetic and how C passes arguments by value. These topics are covered in the earlier lectures of this free C programming course, and the previous lecture explained why pointer arithmetic scales by element size.
Pointer to Pointer in C: The Idea
A pointer stores the address of a variable. A pointer variable is itself a variable, so it occupies memory and has its own address. A **pointer to pointer** stores that address. Declare it with two stars: float **pp = &p;. Dereference once and you get the pointer; dereference twice and you reach the final value.
Two levels of indirection
| Expression | Type | Meaning |
|---|---|---|
pp | float ** | Address of the pointer p |
*pp | float * | The pointer p itself |
**pp | float | The value 2.5 stored in level |
Why pp + 1 Moves Further Than p + 1
Pointer arithmetic scales by the size of the pointed-to type. For float *p, one step is sizeof(float), usually 4 bytes. For float **pp, the pointed-to type is float *, a pointer, so one step is sizeof(float *), 8 bytes on a 64-bit machine and 4 bytes on a 32-bit microcontroller. The base type of the data does not matter for the double pointer; only the size of a pointer does.
Passing a Pointer to a Function: Changing the Pointer Itself
C passes every argument by value. When you pass a pointer, the function receives a **copy** of the address. Through that copy it can change the data the pointer points to. But assigning a new address to the copy changes only the copy. If the function must repoint the caller’s pointer, it needs the address of that pointer, which is a pointer to pointer.
retarget_wrong(int *p) versus retarget_right(int **pp)
#include <stdio.h>
static int fallback_a = 111;
static int fallback_b = 222;
static void retarget_wrong(int *p) { p = &fallback_a; }
static void retarget_right(int **pp) { *pp = &fallback_b; }
int main(void)
{
float level = 2.5f;
float *p = &level;
float **pp = &p;
printf("p step : %td bytes\n", (char *)(p + 1) - (char *)p);
printf("pp step : %td bytes\n", (char *)(pp + 1) - (char *)pp);
printf("**pp : %.1f\n", **pp);
int mine = 5;
int *q = &mine;
printf("before : %d\n", *q);
retarget_wrong(q);
printf("after wrong : %d\n", *q);
retarget_right(&q);
printf("after right : %d\n", *q);
return 0;
}gcc -std=c17 -Wall -Wextra -o ptr_ptr ptr_ptr.c
./ptr_ptrGCC will warn that the parameter p in retarget_wrong is “set but not used”. That warning is the compiler pointing at the bug: the assignment has no visible effect. Expected output of the program on 64-bit Linux:
p step : 4 bytes
pp step : 8 bytes
**pp : 2.5
before : 5
after wrong : 5
after right : 222retarget_wrong assigned to its private copy, so q still pointed at mine and the program printed 5 again. retarget_right received &q, wrote through it with *pp = &fallback_b, and *q became 222.
Rule of thumb: To change a value in the caller, pass a pointer to that value. If the value is itself a pointer, pass a pointer to the pointer.
Real-World Uses in Embedded C Programming
- **Allocators:**
int alloc_buffer(uint8_t **out, size_t n)returns a status code and hands back the buffer throughout. - **Parsers:**
parse_token(const char **cursor)advances the caller’s cursor past the token it consumed. - **Linked lists:** insert at the head by passing the address of the head pointer, so no special case is needed for an empty list.
- **Argument vectors:**
char **argvis an array of string pointers.
Common Mistakes and Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
Passing p when the function must change p | Caller’s pointer unchanged | Pass &p and use int ** |
Forgetting one * when dereferencing | Type mismatch warning, or wrong value printed | Count the stars: each * strips one level |
Dereferencing *pp when it is NULL | Segmentation fault | Check pp != NULL and *pp != NULL before use |
Returning the address of a local through *out | Dangling pointer after return | Hand back heap, static or caller-owned storage |
Printing a pointer with %d | Wrong output on 64-bit | Cast to void * and use %p |
Best Practices, Performance and Security
- Prefer returning a pointer from a function when you only need one result; use the double pointer when you must also return a status code.
- Document ownership: say who frees what the double pointer hands back.
- Each extra level of indirection costs one more memory load, which matters in tight interrupt handlers.
- Security: after
free(*pp), set*pp = NULL. This prevents a stale pointer in the caller from being used again. - Validate output parameters (
out != NULL) at the top of public functions.
Summary and Key Takeaways
**ppreaches the value;*ppis the pointer;ppis the address of the pointer.- Arithmetic on
T **steps bysizeof(T *), not bysizeof(T). - Arguments are passed by value, so changing a pointer inside a function needs a double pointer.
- A “set but not used” warning on a pointer parameter often reveals this very bug.
Conclusion
You have now covered the full pointer toolkit in this free embedded C programming course: declaration, arithmetic, arrays, functions, dynamic memory, function pointers and the many traps around them. Work through the quiz below without compiling first, then run each snippet to confirm. After that, try the interview questions out loud, because explaining a concept is the real test of understanding it.
Quiz: Predict the Output
Assume a 64-bit Linux system and that stdio.h is included. Answers are hidden under each question; cover them and try first.
Q1. int v[3] = {7, 8, 9}; int *p = v + 1; printf("%d %d", p[-1], *(p + 1));
Show answer
7 9. p points at 8, p[-1] is v[0] and *(p + 1) is v[2].
Q2. int x = 3; int *p = &x; int **pp = &p; **pp = **pp + 4; printf("%d", x);
Show answer
7. Both **pp reads and the write refer to x.
Q3. int w[4] = {1, 2, 3, 4}; int *q = w; q += 2; printf("%d ", *q--); printf("%d", *q);
Show answer
3 2. The first print uses the old position, then q steps back to w[1].
Q4. int a[3] = {1, 2, 3}; printf("%zu", sizeof(a) / sizeof(a[0]));
Show answer
3. Total bytes divided by element bytes gives the element count.
Q5. int arr[5] = {10, 20, 30, 40, 50}; int *e = &arr[4]; printf("%td", e - arr);
Show answer
4. Pointer difference counts elements.
Q6. double d = 0; double *dp = &d; double **ddp = &dp; printf("%td", (char *)(ddp + 1) - (char *)ddp);
Show answer
8. One step of a double ** is sizeof(double *), the size of a pointer. It is not sizeof(double); the two merely coincide at 8 bytes on this platform.
Q7. int b[4] = {5, 10, 15, 20}; printf("%d", (b + 1)[2]);
Show answer
20. It is *(b + 1 + 2), which is b[3].
Q8. int c[4] = {2, 4, 6, 8}; int *r = c; ++*r; printf("%d %d", c[0], *r);
Show answer
3 3. The element is incremented and the pointer does not move.
Q9. int m[3] = {5, 6, 7}; int *s = m; int t = *s++; printf("%d %d", t, *s);
Show answer
5 6. t gets the old element, then s moves on.
Q10. A function void f(int *p) { p = &g; } is called as f(ptr);. Does ptr change?
Show answer
No. p is a copy. Use void f(int **pp) { *pp = &g; } and call f(&ptr);.
Interview Questions on C Pointers
What is the difference between an array name and a pointer?
An array name labels a fixed block of elements and cannot be reassigned or incremented; a pointer is a variable holding an address that can change. sizeof and & treat them differently, and array parameters are really pointers.
How do you read int *(*fp)(void)?
fp is a pointer to a function that takes no arguments and returns a pointer to int. Read outward from the name: parentheses first, then the parameter list, then the return type.
Why does swapping two pointers inside a function need a double pointer?
Arguments are passed by value, so the function would only swap copies. Passing the addresses of the pointers lets it swap the originals.
What is a wild pointer, and how is it different from a dangling pointer?
A wild pointer was never initialised. A dangling pointer once pointed to a valid object that has since been freed or gone out of scope. Both are undefined behaviour to dereference.
What is the difference between *p++ and (*p)++?
*p++ yields the current element and then moves the pointer. (*p)++ yields the current element and then increments the element itself.
Why can you not add two pointers?
The sum of two addresses has no meaning. Valid operations are pointer plus or minus an integer, pointer minus pointer, and comparison.
How do you safely loop backward over an array with a pointer?
Start at one past the end and use for (p = a + N; p != a; ) { --p; ... }, so the pointer never goes before the first element.
Why is sizeof unreliable on an array function parameter?
The parameter is adjusted to a pointer, so sizeof returns the pointer size. Pass the length as a separate argument.
Why use uintptr_t instead of int for an address as a number?
int may be too small to hold a pointer, especially on 64-bit systems. uintptr_t is guaranteed to be wide enough.
Where do you use function pointers in embedded C programming?
Interrupt vector tables, callbacks into drivers, command dispatch tables and state machines.
Frequently Asked Questions
What is a pointer to pointer in C?
It is a variable that stores the address of another pointer, declared with two stars such as int **pp. One dereference gives the inner pointer, two give the final value.
When do I need a double pointer in a function parameter?
When the function must change the caller’s pointer, for example to allocate memory and hand it back, or to advance a cursor through a string.
Why does pp + 1 move by 8 bytes on my machine?
Because arithmetic on T ** scales by sizeof(T *). On a 64-bit system a pointer is 8 bytes.
How many levels of pointers can I use?
The C standard guarantees support for at least 12 levels of pointer declarators, but more than two is rarely needed and quickly hurts readability.
Why did my function not change the pointer I passed in?
The function received a copy of the pointer. Pass the address of the pointer and assign through it.
Where can I find more free C programming lectures?
This free C programming course continues with the next chapter. Use the course index to follow the full free embedded C course in order.
Chapter Complete: Keep Learning Free
You finished the Pointers chapter. Move on to the next chapter of the free embedded C course.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

1 Comment