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…

Dell Firmware
Dell Firmware

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]

Bootloader loading Linux
Bootloader_loading_Linux

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

MistakeWhat goes wrongFix
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

ExpressionMeaning
int **pDeclare a double pointer to int
p = &qStore address of pointer q in p
*pThe pointer q (address of the final variable)
**pThe final int value
**p = 5Write 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

📬 Get new articles in your inbox

Deep dives on SystemC, C++, and embedded systems — no spam, unsubscribe any time.

No spam, unsubscribe any time. Privacy Policy

Aditya Gaurav

Aditya Gaurav

Embedded systems engineer specializing in SystemC, ARM architecture, and C/C++ internals. Writing deep technical dives for VLSI and embedded engineers.