What is Pointer to Pointer Pitfalls- Free C Programming Course

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.

free c programming coursefree c tutorialc programming for embedded systemsembedded c programmingpointer to pointer in c

What You Will Learn in This Lecture

  • What a pointer to pointer is and how **pp reaches the final value
  • Why p + 1 and pp + 1 move 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

pp address of p → p
p address of level → level
level 2.5
Reading pp = addresspp = p*pp = 2.5
ExpressionTypeMeaning
ppfloat **Address of the pointer p
*ppfloat *The pointer p itself
**ppfloatThe 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)

wrong caller q → mine|callee copy of q → fallback_a (lost on return)
right caller q → fallback_b|callee pp holds &q → pp = &fallback_b
#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_ptr

GCC 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   : 222

retarget_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 through out.
  • **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 **argv is an array of string pointers.

Common Mistakes and Troubleshooting

MistakeSymptomFix
Passing p when the function must change pCaller’s pointer unchangedPass &p and use int **
Forgetting one * when dereferencingType mismatch warning, or wrong value printedCount the stars: each * strips one level
Dereferencing *pp when it is NULLSegmentation faultCheck pp != NULL and *pp != NULL before use
Returning the address of a local through *outDangling pointer after returnHand back heap, static or caller-owned storage
Printing a pointer with %dWrong output on 64-bitCast 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

  • **pp reaches the value; *pp is the pointer; pp is the address of the pointer.
  • Arithmetic on T ** steps by sizeof(T *), not by sizeof(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

Leave a Reply

Your email address will not be published. Required fields are marked *