student@ubuntu:~$
pointers-memory Lesson 9 10 min read

Double Pointers

int **, the rule of stars, and the argv pattern

Based on content from Dr. Stu Steiner, Eastern Washington University.

Reading: K&R §5.6 (pp. 107–108); supplemental

In a nutshell

int ** looks scary, but it is not a new kind of beast. It is a pointer whose pointed-to type happens to be a pointer. Reading right-to-left, int **pp means “pp is a pointer to a pointer to int.” You reach for it in exactly one situation: when a function must modify the caller’s pointer (not just the object the pointer refers to). The rule is a direct generalization of last lesson: to modify a T from inside a function, pass a T *. Applied to T = int, you pass int *. Applied to T = int *, you pass int **. That is where int **argv comes from, where the int **array idiom appears in dynamic-resize code, and why &p shows up on the caller side whenever the callee is going to reassign the pointer.

Practice this topic: Double Pointers drill, or browse the practice gallery.

After this lesson, you will be able to:

  • Declare int ** and say what type each level of dereference produces
  • Apply the rule “to modify a T, pass a T *” to pointer-typed targets
  • Write a function that allocates memory and hands the pointer back to the caller
  • Read int **argv and explain why main needs two levels of indirection
  • Draw the two-arrow diagram for any double-pointer expression

Quick reference

Declaration Reading Level of indirection
int x plain object 0
int *p pointer to int 1
int **pp pointer to pointer to int 2
&x produces int * adds one level
*pp produces int * strips one level
**pp produces int strips two levels

Coming from CSCD 210

In Java, you could never write a method that replaced the caller’s reference. void swap(Point a, Point b) cannot swap which object the caller’s variables refer to; it can only mutate fields inside the objects. C exposes the missing level: pass the address of the pointer, and the callee can write through it to change where the caller’s pointer aims. This is powerful (it is how realloc-style APIs and linked-list heads work) and it is ugly (two levels of * confuse everyone the first time). The confusion goes away once you draw the two-arrow diagram.


The rule of stars

Last lesson’s rule was: to modify an int, pass an int *. The rule generalizes. To modify a T, pass a T *, no matter what T is.

To modify Caller passes Callee parameter Callee writes
int x &x int *p *p = newValue;
int *p &p int **pp *pp = newPointer;
int **pp &pp int ***ppp *ppp = newDoublePointer;

That third row is rare enough that most careers never meet it. The second row is the one you will write often: every time a function must reassign the caller’s pointer.

Walking the types

int x = 10;                /* int      */
int *p = &x;               /* int *    */
int **pp = &p;             /* int **   */

/* Strip one level at a time: */
*pp      /* int *, same value as p   (address of x) */
**pp     /* int,   same value as x   (10)           */

/* Add one level at a time: */
&x       /* int *,  address of x                    */
&p       /* int **, address of p                    */
&pp      /* int ***, address of pp                  */

Every * applied on the outside of an expression strips one level from the type. Every & adds one. They are inverses: *&x is x, and &*p is p (when the expression is legal). This mechanical rule is the single most useful tool for reading pointer expressions at speed; the full treatment with worked examples is in the pointer type algebra deep dive.

The two-arrow diagram

pp           p            x
+--------+   +--------+   +----+
| 0x7600 | → | 0x7540 | → | 10 |
+--------+   +--------+   +----+
    ^            ^          ^
    |            |          |
  pp lives   p lives    x lives
  at 0x?     at 0x7600  at 0x7540

*pp follows the first arrow: you land on p, which holds 0x7540. **pp follows both arrows: you land on x, which holds 10. Writing *pp = q; redirects the first arrow to whatever q points at. Writing **pp = 99; follows both arrows and overwrites x.

Check your understanding (predict the type and value)

Given:

int a = 7;
int *p = &a;     /* p stored at 0x8000, value 0x4000  */
int **pp = &p;   /* pp stored at 0x9000, value 0x8000 */

Fill in the type and value for *pp, **pp, &p, &pp.

Reveal answer
Expression Type Value
*pp int * 0x4000 (same as p)
**pp int 7 (same as a)
&p int ** 0x8000 (where p itself lives)
&pp int *** 0x9000 (where pp itself lives)

Each * on the outside strips one star from the type. Each & adds one.


Why you reach for int **

Pattern 1: a function that reassigns the caller’s pointer

/* Broken: assigns to the local copy, caller sees nothing */
void give_me_memory_broken(int *arr, int n)
{
    arr = calloc(n, sizeof(int));
}

/* Working: writes through the double pointer, caller's pointer updates */
void give_me_memory(int **arr, int n)
{
    *arr = calloc(n, sizeof(int));
}

int main(void)
{
    int *data = NULL;
    give_me_memory(&data, 10);    /* pass the address of the pointer */
    if (data == NULL) {           /* still check allocation */
        return 1;
    }
    data[0] = 42;
    free(data);
    return 0;
}

The broken version has the same bug as last lesson’s “forgot the *” example: arr is a copy, so arr = calloc(...) updates the copy and throws it away when the function returns. The working version takes int **arr, dereferences once to reach the caller’s data, and writes the new address through it.

Lab 6 uses a close cousin of this pattern. int *addItem(int *length, int *array) takes int *length so the callee can update the caller’s length, and returns the new array pointer (a different design choice than int **array). Both designs are common; both rely on the same pass-by-value rules.

Pattern 2: argv is char **

The full prototype of main:

int main(int argc, char **argv)

is the same as the more common int main(int argc, char *argv[]). The [] in the parameter decays to *, giving you char **. argv is a pointer to an array of char *, where each char * is a C string (the program name in argv[0], then each command-line argument).

/* Print every command-line argument: */
for (int i = 0; i < argc; i++) {
    printf("argv[%d] = %s\n", i, argv[i]);
}

Two levels of indirection make sense here because the OS kernel needs a way to hand your program a variable-length list of variable-length strings. The outer pointer gets you into the array of pointers; each inner pointer gets you into one string.

Pattern 3: an address of a pointer variable

Any time you take & of something whose type is already T *, you get a T **. That is why &p has type int ** when p is int *. You rarely hold onto the int ** in a variable; more often it shows up as an argument to a function parameter of type int **.

Check your understanding (which is broken)

Both functions try to initialize the caller’s pointer. One works; one does not. Which is which, and why?

/* A */
void init_a(int *p)
{
    p = malloc(sizeof(int));
    *p = 42;
}

/* B */
void init_b(int **p)
{
    *p = malloc(sizeof(int));
    **p = 42;
}
Reveal answer

A is broken. The caller passes &x where x is an int *, and init_a receives a copy of that address in its local p. p = malloc(...) overwrites the local copy with a new heap address; *p = 42 writes 42 into the new heap block; then the function returns, the local p dies, and the allocation is leaked. The caller’s original pointer is unchanged.

B is correct. The caller passes &x where x is an int *, giving init_b an int **. *p = malloc(...) dereferences once to reach the caller’s int * and overwrites it with the allocation. **p = 42 dereferences twice to reach the allocated int and writes 42 there. When the function returns, the caller’s pointer now aims at the new allocation.

The caller for the working version:

int *x = NULL;
init_b(&x);
/* x now points at a freshly allocated int holding 42 */
free(x);

Reading messy declarations

C’s declarator grammar gets hard to parse with two stars plus arrays plus functions. When that happens, use the right-left rule: start at the identifier, read what is on its right (arrays [] and functions ()), then read what is on its left (*).

Declaration Reading What it is
int **pp pp is a pointer to a pointer to int double pointer
int *arr[10] arr is an array of 10 pointers to int array of pointers
int (*p)[10] p is a pointer to an array of 10 int pointer to array
char **argv argv is a pointer to a pointer to char the main parameter
int *f(void) f is a function returning pointer to int function returning pointer

You will rarely write int (*p)[10]. You will read it. If you hit one in the wild that stumps you, paste it into cdecl.org for a plain-English translation.

const with double pointers

const placement gets tricky with two stars. The same right-to-left reading applies.

Declaration What is const
const int **pp the final int (cannot write **pp = ...)
int * const *pp the middle pointer (cannot write *pp = somePtr)
int ** const pp pp itself (cannot reassign pp)
const int * const * const pp all three levels

You will not write these often. When you do need one, read right-to-left and describe what you want the compiler to forbid.


What comes next

A function receives an int ** parameter named pp. Which statements are correct?
A*pp = malloc(sizeof(int)); updates the caller's int * to point at a new heap allocation.
Bpp = malloc(sizeof(int)); has the same effect as option A.
CThe caller must pass &myPtr, where myPtr has type int *.
D**pp has type int *.
Eargv in int main(int argc, char **argv) is an example of this parameter shape.
Correct: A, C, E.
  • B is wrong: pp = malloc(...) overwrites the local copy of the parameter. The caller's pointer is unchanged and the allocation leaks.
  • D is wrong: *pp has type int *. **pp has type int.

You now have the full pointer model: addresses, dereferencing, scaling, and indirection at any depth. Next, Stack, Heap & Memory Layout switches focus from the pointer itself to the memory it points into. Where does a stack variable live? Where does a malloc allocation live? What is the actual geometry of your program’s address space? Drill this page with the Double Pointers skill card, or browse the practice gallery.