Double Pointers
int **, the rule of stars, and the argv pattern
Based on content from Dr. Stu Steiner, Eastern Washington University.
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 aT *” to pointer-typed targets - Write a function that allocates memory and hands the pointer back to the caller
- Read
int **argvand explain whymainneeds 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
int ** parameter named pp. Which statements are correct?- 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:
*pphas typeint *.**pphas typeint.
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.