Skip to content
VLSI Mentor

C Part I · Module 7 · Pointers

Pointers to Pointers

A pointer is an object, and a pointer identifies an object, so nothing stops a pointer from identifying another pointer. That observation is easy and is where most explanations of this topic stop, leaving the reader with a notation and no reason to use it — which is why double pointers have a reputation for being gratuitously confusing. They are not confusing when they are being used for something. This chapter covers the two situations that genuinely require a second level of indirection, both of which you have already met as unfinished business: a function that needs to change which object its caller's pointer identifies, and what happens when the array of pointers from the previous chapter is passed to a function. Neither needs any machinery beyond what Module 7 has already built.

Advanced17 min readCPart IFundamentalsPointers

Part I · Module 7 · Chapter 7.10 · Pointers to Pointers

1. The Type

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int   value = 42;
    int  *p     = &value;
    int **pp    = &p;

Read each declaration with Chapter 7.9 §3's technique — start at the name, go right while you can, then left:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int  *p      p is a pointer to int
int **pp     pp is a pointer to a pointer to int

pp is an ordinary object. It holds an address, like every pointer. What is at that address happens to be another pointer.

Dereferencing works one level at a time:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pp       an int **  — the address of p
*pp      an int *   — p itself
**pp     an int     — value

There is no new rule here. * removes one level, exactly as it always has.

2. Why You Should Be Suspicious of It

Before the uses, the caution — because this is a construct that gets reached for far more often than it is justified.

Every level of indirection is something a reader has to hold in their head, and something that can be null, stale or wrong. A second level needs a reason.

In ordinary C, two levels is the practical ceiling. Three is almost always a sign that something should have been a different shape. The two sections that follow are the reasons that do justify a second level, and they are specific.

3. Problem One: Changing the Caller's Pointer

Chapter 7.5 §4 established that assigning to a pointer parameter changes nothing in the caller:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
void attempt_retarget(int *p) { int local = 99; p = &local; }

and said, at the time, that if you need the caller to see the change you want a pointer to a pointer. Here is that situation for real.

A function is to search an array and tell the caller which element it chose — not the value, the element, so the caller can modify it or compute its index. The answer is a pointer, and the caller has a pointer object waiting for it.

Azvya Education Pvt. Ltd.VLSI Mentor
one level apart
#include <stdio.h>
#include <stddef.h>

void select_largest(int *values, size_t count, int **chosen_out)
{
    if (chosen_out == NULL || count == 0u) {
        return;
    }

    int *best = &values[0];
    for (size_t i = 1; i < count; i++) {
        if (values[i] > *best) {
            best = &values[i];
        }
    }

    *chosen_out = best;
}

void select_largest_broken(int *values, size_t count, int *chosen)
{
    (void)count;
    chosen = &values[0];
}

int main(void)
{
    int readings[5] = {12, 45, 7, 45, 31};
    size_t count = sizeof readings / sizeof readings[0];

    int *chosen = NULL;
    select_largest(readings, count, &chosen);
    printf("chosen != NULL        %d\n", chosen != NULL);
    printf("*chosen               %d\n", *chosen);
    printf("index of chosen       %td\n", chosen - readings);

    int *broken = NULL;
    select_largest_broken(readings, count, broken);
    printf("broken still NULL     %d\n", broken == NULL);

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
chosen != NULL        1
*chosen               45
index of chosen       1
broken still NULL     1

The last line is Chapter 7.5 §4 restated: select_largest_broken assigned to its own parameter, and the caller's pointer is untouched. The compiler even notices that the assignment is pointless:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: parameter 'chosen' set but not used [-Wunused-but-set-parameter]

select_largest works because the caller passed &chosen — the address of its own pointer object — and the function wrote through it with *chosen_out = best;.

This is Chapter 7.5's out-parameter pattern with one difference: the thing being written back is itself a pointer, so the out-parameter needs one more level.

To let a function change a caller's T, pass a T *. To let it change a caller's T *, pass a T **. It is the same rule, applied to a type that happens to be a pointer.

Three levels of indirection shown as a vertical chain. At the top, the object readings index 1, an int holding 45, described as level 0, the value you finally want. In the middle, chosen, an int star object belonging to main, described as level 1, the pointer the caller owns; an arrow labelled star chosen runs from it up to the int object. At the bottom, chosen_out, the function's parameter, an int star star object, described as level 2, how the callee reaches it; an arrow labelled star chosen_out runs from it up to the caller's pointer.level 0the value you finally wantreadings[1]an int object · holds 45level 1the pointer the caller ownschosen — in mainan int * objectlevel 2how the callee reaches itchosen_out — theparameteran int ** object*chosen*chosen_out12
Figure 1 — the three levels in §3's call. At the bottom is the parameter chosen_out, which is an int ** holding the address of main's pointer. Dereferencing it once reaches chosen, which is main's own int * object — that is the object the function writes to. Dereferencing again reaches the element itself. Each arrow is one application of the indirection operator, and the labels name which one.

Three details of select_largest are worth copying.

The null guard covers the out-parameter, as Chapter 7.4 §5 requires for any pointer arriving from a caller — and here the pointer being guarded is the int **, not what it points at.

count == 0 is handled before values[0] is read, which is Chapter 6.3 §7's warning about the find-the-extreme pattern needing at least one element.

The caller initialises chosen to NULL and could check it afterwards. If the function returns without choosing anything, the caller can tell — which is Chapter 7.4 §5's deliberate use of NULL.

4. Problem Two: Passing an Array of Pointers

This one requires no new idea at all — it is Chapter 7.6 and Chapter 7.9 composed.

Chapter 7.9 built int *counters[4], an array whose elements are int *. Chapter 7.6 established that an array in an expression converts to a pointer to its first element. The first element of an array of int * is an int *, so a pointer to it is an int **.

Azvya Education Pvt. Ltd.VLSI Mentor
an array of pointers, passed
#include <stdio.h>
#include <stddef.h>

int total_of(int *table[], size_t n)
{
    int total = 0;
    for (size_t i = 0; i < n; i++) {
        total = total + *table[i];
    }
    return total;
}

int main(void)
{
    int a = 1, b = 2, c = 3;
    int *t[3] = { &a, &b, &c };

    printf("sizeof t in main   %zu\n", sizeof t);
    printf("total_of(t, 3)     %d\n", total_of(t, 3));

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sizeof t in main   24
total_of(t, 3)     6

int *table[] in that parameter list is, by Chapter 7.7's adjustment rule, int **table. The two spellings are identical, and you will see both.

The count still has to be passed separately, for exactly Chapter 7.7's reason — sizeof t is 24 in main and would be a pointer's size inside the function.

table[i] is one pointer; *table[i] is the int it identifies. The same two steps as Chapter 7.9 §2, now one function boundary away.

This is where most working C programmers meet T ** for the first time, and it is worth recognising it as an array of pointers that has decayed rather than as a mysterious new type.

5. Reading Deeper Declarations

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int    x        an int
int   *p        a pointer to int
int  **pp       a pointer to a pointer to int
int ***ppp      a pointer to a pointer to a pointer to int

The notation extends indefinitely, and the reading technique from Chapter 7.9 §3 keeps working. That does not make the deeper forms a good idea.

6. The Shape You Will Meet Next

Both problems in this chapter had a second level because something genuinely lived at each one. There is a third situation, which this curriculum has not reached: a function that obtains storage and hands it back to the caller through a pointer parameter. That is Chapter 9.6's, and it is Problem One with the object being newly acquired rather than selected from an array.

Mentioning it now is useful because it is the case most code uses, and because it is a direct instance of §3 rather than a new pattern — when you meet it, the only new thing will be where the pointer came from.

7. What You Should Be Able to Say Now

  1. int **pp is a pointer to a pointer to int — an ordinary object holding an address.
  2. * removes one level at a time: pp → *pp → **pp.
  3. Every level needs a reason; two is the practical ceiling in ordinary code.
  4. Problem one: to let a function change a caller's pointer, pass the pointer's address.
  5. That is Chapter 7.5's out-parameter with the result type being a pointer — same rule, one more level.
  6. The broken version assigns to the parameter and the caller's pointer stays NULL — Chapter 7.5 §4, demonstrated.
  7. Problem two: an array of pointers decays to T ** when passed — Chapter 7.6 and 7.9 composed.
  8. int *table[] and int **table are the same parameter, by Chapter 7.7's adjustment.
  9. The count still travels separately, for Chapter 7.7's reason.
  10. Three or more levels is a design smell — usually the data wants a structure instead.

8. Exercises

Exercise 1 — read the declarations. Say in words what each declares.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
char **a;
double *b[4];
int **c[2];
int (*d)[3];

Exercise 2 — peel the levels. Given int v = 7; int *p = &v; int **pp = &p;, give the value or meaning of each.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
v        p        *p        pp        *pp        **pp        &pp

Exercise 3 — predict the output. Do not compile yet.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int a = 1, b = 2;
int *p = &a;
int **pp = &p;
printf("%d\n", **pp);
*pp = &b;
printf("%d %d\n", **pp, *p);
**pp = 20;
printf("%d %d\n", a, b);

The second line is the one to reason about — say what *pp = &b changed.

Exercise 4 — fix the signature. This is meant to leave the caller's pointer aimed at the first element. Say why it does not, give the corrected signature and body, and give the corrected call.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
void aim_at_start(int *values, int *out) { out = &values[0]; }
...
int *where = NULL;
aim_at_start(data, where);

Exercise 5 — identify the decay. For double *table[6]; passed to a function, say what the parameter's type really is, give two spellings of it, and say what sizeof of that parameter would be.

Exercise 6 — justify or refuse. For each, say whether a second level of indirection is justified, and if not, what you would do instead.

  • a function that must set the caller's pointer to the chosen element
  • a function that must set the caller's int to a computed value
  • a traversal over an array of pointers passed from elsewhere
  • a function that returns one of three pointers it was given

Exercise 7 — count the null checks. In §3's select_largest, say which pointers could be null, which ones the function checks, and why the ones it does not check do not need checking.

Exercise 8 — argue the ceiling. A colleague proposes int ***data for a structure with three levels of grouping. Give two concrete reasons to resist, and name the Module that provides the better tool.

9. Summary

  • int **pp is an ordinary object whose value is the address of an int *.
  • * removes one level at a time, with no new rule.
  • Every level must earn its place; two is the practical ceiling.
  • Problem one — change the caller's pointer: pass its address, write *out = ....
  • Assigning to the parameter instead leaves the caller's pointer unchanged, as verified.
  • Problem two — an array of pointers decays to T **; int *t[] and int **t are the same parameter.
  • The count still travels separately.
  • Three or more levels usually means the data wants a structure — Module 10.
  • A function that acquires storage and hands it back is Problem One again, in Chapter 9.6.

Several signatures in this module have carried a qualifier that has been used without being explained. Chapter 7.7's sum took a const int *, and the text said only that it meant the function would not write through the pointer. That was accurate and incomplete, because const can attach to two different things in a pointer declaration — the object being pointed at, or the pointer itself — and the two placements look nearly identical and forbid completely different operations. The next chapter reads them properly.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.