A pointer stores an address, so int ** matters when a function must change its caller's pointer rather than only the final integer. Reading each level lets you trace 18 and 42 to 50, carry an allocated address back to main, and distinguish arrays of pointers from true 2D arrays. The key question is which object each expression can reach.
1. What int ** Means: One Address Leads to Another
Here, value stores 24, p stores the address of value, and pp stores the address of p.
#include <stdio.h>
int main(void) {
int value = 24;
int *p = &value;
int **pp = &p;
printf("%d %d %d\n", value, *p, **pp);
**pp = 81;
printf("%d\n", value);
return 0;
}24 24 24
81The first * in **pp reaches p; the second follows its stored address to value. Thus, **pp = 81 changes the original integer from 24 to 81.
2. Read the Declaration and Every Expression by Type
Use the state before the assignment to 81 and track one pointer level at a time.
Expression | Type | Meaning or value before mutation |
|---|---|---|
|
|
|
|
| Address of |
|
| Address of |
|
| Address of |
|
| Address of |
|
| Equals |
|
|
|
Unary * removes one pointer level, while & adds one. Written as *(*pp), **pp performs two dereferences, not multiplication or exponentiation.
In int **pp, *p, value;, only pp is int **; p is int *, and value is int. One declaration per line makes pointer syntax easier to read.
3. Fully Worked Program: Redirect the Caller's Pointer, Then Change Its Integer
This function redirects the caller's pointer to the larger integer, then adds a delta to it.
#include <stdio.h>
void select_larger_and_add(int **choice,
int *left,
int *right,
int delta) {
*choice = (*left >= *right) ? left : right;
**choice += delta;
}
int main(void) {
int a = 18;
int b = 42;
int *selected = &a;
printf("before: selected=%d a=%d b=%d\n",
*selected, a, b);
select_larger_and_add(&selected, &a, &b, 8);
printf("after: selected=%d a=%d b=%d\n",
*selected, a, b);
return 0;
}Compile and run it:
cc -std=c17 -Wall -Wextra double-pointer.c -o double-pointer
./double-pointerThe output is:
before: selected=18 a=18 b=42
after: selected=50 a=18 b=50Trace it:
Initially,
selectedpoints toa, whose value is18;bis42.The call passes
&selected. Since18 >= 42is false, the conditional choosesright, which contains&b.*choice = rightredirectsselectedtob;**choicethen reachesb.**choice += 8changesbfrom42to50, whilearemains18.
The two assignments do different jobs: *choice redirects the pointer, while **choice modifies the selected integer.

4. Why an int * Parameter Cannot Redirect the Caller's Pointer
C passes every argument, including pointers, by value. Compare these functions:
void wrong(int *slot, int *target) {
slot = target;
}
void redirect(int **slot, int *target) {
*slot = target;
}Given int x = 5, y = 9; int *p = &x;, wrong(p, &y) changes only its local slot. Both arguments have type int *. Afterward, p still points to x, so *p remains 5.
The call redirect(&p, &y) passes &p (int **) and &y (int *). Assigning through *slot changes the caller's p, which then points to y, so *p is 9. Use a double pointer when a callee must update its caller's pointer, not merely read or write the pointed-to integer.
Pointers and Functions in C covers single-pointer parameters, safe pointer returns, and callbacks. Here, one extra indirection lets the callee replace the caller's pointer itself.
5. A Practical Output-Parameter Pattern with Dynamic Allocation
A helper can allocate a block and return its address through an output parameter:
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
int make_sequence(int **out, size_t n) {
*out = malloc(n * sizeof **out);
if (*out == NULL) {
return 0;
}
for (size_t i = 0; i < n; ++i) {
(*out)[i] = 10 + 5 * (int)i;
}
return 1;
}
int main(void) {
int *sequence = NULL;
if (!make_sequence(&sequence, 4)) {
return 1;
}
for (size_t i = 0; i < 4; ++i) {
printf("%d%c", sequence[i], i + 1 == 4 ? '\n' : ' ');
}
free(sequence);
sequence = NULL;
return 0;
}The program prints 10 15 20 25 because 10 + 5 * i is evaluated for i from 0 through 3. Assigning the allocated address through an int *out would change only a local copy; &sequence gives the helper an int ** that reaches sequence in main.
Ownership remains explicit: the helper allocates, then the caller owns and frees the block. Setting sequence to null clarifies later state but does not free memory.
Memory Management in C covers allocation, reallocation, ownership, and cleanup. Here, allocation only shows how int ** carries a new address back to main.
6. Arrays of Pointers Can Produce T **; 2D Arrays Usually Do Not
Consider an array whose elements are pointers:
char *colours[] = {"red", "green", "blue"};
char **cursor = colours;In this expression context, colours decays to a pointer to its first char * element, so cursor can have type char **. The expression *(cursor + 1) is the string "green". The expression *(*(cursor + 2) + 1) first selects "blue", moves one character forward, and produces 'l'.
Now compare char grid[2][6] = {"red", "blue"};. As a function argument, grid decays to char (*)[6], a pointer to an array of six characters, not char **. Similarly, int matrix[2][3] is not safely passed to an int ** parameter. Similar a[i][j] spelling does not make these memory layouts or pointer types interchangeable.
Multidimensional Arrays in C covers contiguous row-major arrays and width-aware parameters. Here, the narrower point is that T ** cannot substitute for a pointer-to-array type.
7. Common Double-Pointer Errors and Safe Fixes
Dereferencing an uninitialised chain:
int **pp; **pp = 10;has undefined behaviour becauseppdoes not point to a validint *. Build the chain first:int n = 10; int *p = &n; int **pp = &p;.Passing the wrong pointer level: if a function expects
int **, passing anint *is a type mismatch. Pass&ponly when the function is meant to changep. Do not silence the compiler with a cast.Allocating only the outer row array: after allocating
row_count * sizeof *rowsfor anint **rows, everyrows[i]must be allocated or assigned beforerows[i][j]is used. Free owned row blocks first, then free the outer array.Keeping a pointer after lifetime ends: a pointer to a finished local object, or a pointer used after
free, is dangling. Setting an owning pointer to null afterfreecan clarify later state, but it is not a substitute for callingfree.
8. Double-Pointer Questions and Further Practice
Questions commonly ask you to determine expression types, trace *p against **pp, predict whether a function redirects its caller's pointer, or distinguish an array of pointers from a real 2D array. Separate two checks: what value does the expression produce, and can the operation legally reach that object?
Answer these before running them:
int n = 7; int *p = &n; int **pp = &p; **pp += 5;With
x = 31,y = 46, andp = &x, callredirect(&p, &y).char *names[] = {"Ada", "Linus", "Ken"}; char **q = names; ++q;
In exercise 1, n becomes 7 + 5 = 12. In exercise 2, redirect receives &p, so *slot = target changes p to &y; *p is 46 while x remains 31. In exercise 3, q advances from names[0] to names[1], so *q is "Linus" and **q is 'L'.
A T ** points to a T *. One dereference reaches the pointer, and two reach the T object. Use a double pointer when a function must replace its caller's pointer; use a single pointer when it only needs the pointee. Revise the wider syllabus in the C Language Course, then use Coding and Skill Development Courses for broader practice.




