Double Pointer in C
A pointer that points to another pointer is known as double pointer.
A pointer that contains the memory address of another pointer and that pointer contains memory adders of a variable of its type. Let's understand it via a scenario, There are three people A, B, C. No one knows where does A & B live except C therefore if you want to meet A or B, first you will have to meet to C and ask for the location of B, Once you find out the location of B, you go to the location of B and ask for the location of A then you finally be able to meet to A. This same happens in computer memory, There is someone who knows the memory address of a pointer and that pointer has the memory address of a variable. Let's look at the program.
#include<iostream>
using namespace std;
int main(){
int A = 123; //a variable
int *B = &A; // pointer B that has address of A
int **C= &B; // double pointer that has address of pointer B
// <---------Some Code are described below---------->
return 0;
}
As a beginner we make mistake that we declare pointer and considered it as double pointer.
// int * C = &B ; // (Incorrect) This is still a pointer but not a double pointer .
Proper way to initialize is as using **(double Asterisk) than we assign address of a pointer .
int ** C = &B; //Correct
Application
Double pointer is heavily used at an embedded level so If you are Electronics or Electrical engineer you are going to notice it anyway. Let me explain one example…
Have you wonder, how does your operating system get booted on a laptop even if you have multiple operating systems installed on a computer. So the point is Once firmware[A lightweight program installed by the manufacturer in FLASH which shows hardware level information] operation get completed and before giving control to the operating system, Firmware holds a double-pointer, which first points to boot loader's vector table[Generally at base Address of Vector table in ROM]
and at that vector table, there is an address to a structure. which has function and data address and using this function and data, booting sequence get started[Hardware checks are done and OS get started].
NXP's Kinetis KL13 series microcontroller, this Example code snippet is used to run bootloader:-
//May not exact but procedure is same uint32_t runBootloaderAddress; void (*runBootloader)(void * arg); // Read the function address from the ROM API tree. runBootloaderAddress = **(uint32_t *)(0x1c00001c); runBootloader = (void (*)(void * arg))runBootloaderAddress; // Start the bootloader. runBootloader(NULL); // This is just for example use of double pointer.
Memory Layout of a Double Pointer
A double pointer adds one extra level of indirection. Here is how three related variables sit in memory at run-time:
int A = 123; // sits at, say, 0x1000
int *B = &A; // sits at 0x2000, contains 0x1000
int **C = &B; // sits at 0x3000, contains 0x2000
Address Variable Value
──────── ───────── ──────────────
0x1000 A 123
0x2000 B 0x1000 ← address of A
0x3000 C 0x2000 ← address of B
Dereferencing:
*C == 0x1000 (value of B)
**C == 123 (value of A)
Each level of * in the declaration adds one hop to reach the final value. A single *C gives you what B holds; a double **C gives you what A holds.
Modifying a Pointer from Inside a Function
The most common real-world use of double pointers is when you need a function to change what a pointer points to — not just the value it points at. A single pointer cannot do this because C is pass-by-value.
#include <stdio.h>
#include <stdlib.h>
/* Wrong: single pointer — caller's ptr is unchanged */
void bad_alloc(int *ptr, int val) {
ptr = (int *)malloc(sizeof(int)); /* local copy only */
*ptr = val;
}
/* Correct: double pointer — caller's ptr is updated */
void good_alloc(int **ptr, int val) {
*ptr = (int *)malloc(sizeof(int)); /* modifies caller's pointer */
**ptr = val;
}
int main(void) {
int *p = NULL;
bad_alloc(p, 42);
printf("after bad_alloc: p = %p\n", (void *)p); /* still NULL */
good_alloc(&p, 42);
printf("after good_alloc: p = %p, *p = %d\n", (void *)p, *p); /* valid */
free(p);
return 0;
}
Pass &p (address of the pointer) whenever the function needs to update what p itself points to — for example, initialising a linked list head, growing a buffer, or returning heap-allocated memory through an output parameter.
Dynamic 2D Arrays with Double Pointers
Double pointers are the standard way to create a true heap-allocated 2D array whose dimensions are known only at run-time.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int rows = 3, cols = 4;
/* Step 1 — allocate an array of row pointers */
int **matrix = (int **)malloc(rows * sizeof(int *));
/* Step 2 — allocate each row */
for (int i = 0; i < rows; i++)
matrix[i] = (int *)malloc(cols * sizeof(int));
/* Step 3 — use like a normal 2D array */
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
matrix[i][j] = i * cols + j;
printf("matrix[1][2] = %d\n", matrix[1][2]); /* 6 */
/* Step 4 — free in reverse order */
for (int i = 0; i < rows; i++)
free(matrix[i]);
free(matrix);
return 0;
}
Memory layout: matrix is a pointer to an array of pointers; each pointer in that array points to an independent row on the heap. This differs from a fixed 2D array (int a[3][4]) which is contiguous.
Double Pointers in Linked-List Operations
Inserting or removing the head node of a singly-linked list is awkward with a single pointer because the head pointer itself must change. A double pointer makes the code uniform — no special case for the head.
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
/* Insert at front — updates *head directly */
void push(Node **head, int val) {
Node *n = (Node *)malloc(sizeof(Node));
n->data = val;
n->next = *head;
*head = n; /* caller's head pointer now points to new node */
}
/* Delete first node whose data == val */
void delete_val(Node **head, int val) {
Node **cur = head; /* traverse using double pointer */
while (*cur) {
if ((*cur)->data == val) {
Node *tmp = *cur;
*cur = (*cur)->next; /* relink */
free(tmp);
return;
}
cur = &(*cur)->next;
}
}
int main(void) {
Node *head = NULL;
push(&head, 10);
push(&head, 20);
push(&head, 30); /* list: 30 → 20 → 10 */
delete_val(&head, 20); /* list: 30 → 10 */
for (Node *n = head; n; n = n->next)
printf("%d ", n->data); /* 30 10 */
printf("\n");
return 0;
}
The delete_val function uses Node **cur so it can rewrite any next pointer (including head) without needing a "previous node" variable.
Common Pitfalls
| Mistake | What goes wrong | Fix |
|---|---|---|
Using int *p = &q instead of int **p = &q |
Type mismatch; compiler warning, undefined dereference | Match the number of stars to the indirection level |
Dereferencing before malloc |
Segmentation fault — pointer is NULL or garbage | Allocate first, then dereference |
| Freeing only the outer pointer | Memory leak — each inner row is never freed | Free rows first, then the pointer array |
Passing ptr instead of &ptr to output function |
Function modifies its local copy; caller's pointer unchanged | Pass the address of the pointer when the function must update it |
Confusing **p (value) with *p (intermediate pointer) |
Reading/writing wrong memory level | Draw a memory diagram; trace each dereference step by step |
Double Pointer — Quick Reference
| Expression | Meaning |
|---|---|
int **p | Declare a double pointer to int |
p = &q | Store address of pointer q in p |
*p | The pointer q (address of the final variable) |
**p | The final int value |
**p = 5 | Write 5 into the final variable through two hops |
*p = malloc(...) | Allocate and update the intermediate pointer |
free(*p); free(p) | Release heap memory in reverse order |