Docs

Pointer to Pointer

Pointer to Pointer (Double Pointers)

Table of Contents

  1. Introduction
  2. What is a Pointer to Pointer?
  3. Declaration and Initialization
  4. Memory Layout and Visualization
  5. Dereferencing Double Pointers
  6. Triple and N-Level Pointers
  7. Practical Applications
  8. Double Pointers with Arrays
  9. Dynamic 2D Arrays
  10. Command Line Arguments
  11. Common Mistakes and Pitfalls
  12. Best Practices
  13. Summary

Introduction

A pointer to pointer (also called a double pointer) is a variable that stores the address of another pointer variable. Just as a regular pointer holds the address of a normal variable, a double pointer holds the address of a pointer.

Why Do We Need Double Pointers?

  1. Modify pointers in functions: Pass pointer by reference to change what it points to
  2. Dynamic 2D arrays: Create and manage multi-dimensional arrays at runtime
  3. Array of strings: Handle collections of strings efficiently
  4. Command-line arguments: Process argv in main function
  5. Complex data structures: Implement linked lists, trees, and graphs

What is a Pointer to Pointer?

Concept

Level 0:  Variable      →  Holds actual data
Level 1:  Pointer       →  Holds address of variable
Level 2:  Double Pointer →  Holds address of pointer

Visual Representation

+--------+       +--------+       +--------+
| 2000   |  ---> | 1000   |  ---> |  42    |
+--------+       +--------+       +--------+
   pp             ptr               var
(double ptr)    (pointer)        (integer)

pp stores address of ptr (2000)
ptr stores address of var (1000)
var stores the value 42

The Chain of References

int var = 42;       // A regular variable
int *ptr = &var;    // Pointer holding address of var
int **pp = &ptr;    // Double pointer holding address of ptr

// Accessing the value:
var    → 42         (direct access)
*ptr   → 42         (one dereference)
**pp   → 42         (two dereferences)

Declaration and Initialization

Syntax

datatype **pointer_name;

The ** indicates it's a pointer to a pointer.

Step-by-Step Declaration

// Step 1: Declare a normal variable
int number = 100;

// Step 2: Declare a pointer and point to the variable
int *ptr = &number;

// Step 3: Declare a double pointer and point to the pointer
int **pp = &ptr;

Complete Example

#include <stdio.h>

int main() {
    int value = 50;
    int *single_ptr = &value;
    int **double_ptr = &single_ptr;

    printf("value = %d\n", value);
    printf("&value = %p\n", (void*)&value);

    printf("\nsingle_ptr = %p (stores &value)\n", (void*)single_ptr);
    printf("*single_ptr = %d (dereferenced)\n", *single_ptr);
    printf("&single_ptr = %p\n", (void*)&single_ptr);

    printf("\ndouble_ptr = %p (stores &single_ptr)\n", (void*)double_ptr);
    printf("*double_ptr = %p (single_ptr value)\n", (void*)*double_ptr);
    printf("**double_ptr = %d (value)\n", **double_ptr);

    return 0;
}

Different Data Types

// Integer double pointer
int **int_pp;

// Character double pointer (common for string arrays)
char **char_pp;

// Float double pointer
float **float_pp;

// Double double pointer
double **double_pp;

// Void double pointer
void **void_pp;

Memory Layout and Visualization

Understanding the Memory Model

Let's trace through memory with concrete addresses:

int x = 10;        // x at address 0x1000
int *p = &x;       // p at address 0x2000, stores 0x1000
int **pp = &p;     // pp at address 0x3000, stores 0x2000
Memory Address    Variable    Value        Meaning
──────────────────────────────────────────────────────────
0x3000           pp          0x2000        Address of p
0x2000           p           0x1000        Address of x
0x1000           x           10            The actual data

Visual Memory Map

Address:     0x3000         0x2000         0x1000
           ┌─────────┐    ┌─────────┐    ┌─────────┐
           │ 0x2000  │───►│ 0x1000  │───►│   10    │
           └─────────┘    └─────────┘    └─────────┘
               pp              p              x
           (int **)        (int *)         (int)

Size of Double Pointers

printf("sizeof(int)    = %zu\n", sizeof(int));      // 4 bytes
printf("sizeof(int*)   = %zu\n", sizeof(int*));     // 8 bytes (64-bit)
printf("sizeof(int**)  = %zu\n", sizeof(int**));    // 8 bytes (64-bit)

All pointer types (regardless of levels) have the same size on a given system because they all store memory addresses.


Dereferencing Double Pointers

Levels of Dereferencing

ExpressionResultDescription
ppAddress of pointerThe double pointer itself
*ppAddress of variableThe pointer it points to
**ppThe actual valueThe variable's value

Detailed Example

#include <stdio.h>

int main() {
    int num = 42;
    int *ptr = &num;
    int **pp = &ptr;

    // Level 0: The double pointer
    printf("pp  = %p (address stored in pp)\n", (void*)pp);

    // Level 1: Single dereference
    printf("*pp = %p (address stored in ptr)\n", (void*)*pp);

    // Level 2: Double dereference
    printf("**pp = %d (value of num)\n", **pp);

    // Modifying through double pointer
    **pp = 100;
    printf("\nAfter **pp = 100:\n");
    printf("num = %d\n", num);  // 100

    return 0;
}

Dereferencing to Modify

int a = 5, b = 10;
int *p = &a;
int **pp = &p;

// Modify value through double pointer
**pp = 50;           // a is now 50

// Change which variable p points to
*pp = &b;            // p now points to b

// Now **pp accesses b
printf("%d\n", **pp);  // Prints 10

Triple and N-Level Pointers

Triple Pointer (Rarely Used)

int value = 5;
int *p1 = &value;       // Single pointer
int **p2 = &p1;         // Double pointer
int ***p3 = &p2;        // Triple pointer

printf("***p3 = %d\n", ***p3);  // 5

Memory Visualization

p3 (int***) → p2 (int**) → p1 (int*) → value (int)
   │              │            │           │
   └──────────────┴────────────┴───────────┴──► 5

When to Use Multi-Level Pointers

LevelCommon Use Case
* (Single)Accessing values by reference
** (Double)Modifying pointers, 2D arrays, string arrays
*** (Triple)3D arrays (rare), complex data structures
****+Almost never needed in practice

Rule of Thumb

If you need more than triple pointers, reconsider your design. Use structures or simpler approaches.


Practical Applications

1. Modifying a Pointer in a Function

Without double pointer (doesn't work as intended):

void changePtr(int *p) {
    int local = 100;
    p = &local;  // Only changes local copy of p
}

int main() {
    int x = 10;
    int *ptr = &x;
    changePtr(ptr);
    printf("%d\n", *ptr);  // Still 10, not 100
    return 0;
}

With double pointer (works correctly):

void changePtr(int **pp) {
    static int local = 100;  // Static so it persists
    *pp = &local;  // Modifies the original pointer
}

int main() {
    int x = 10;
    int *ptr = &x;
    changePtr(&ptr);
    printf("%d\n", *ptr);  // 100
    return 0;
}

2. Dynamic Memory Allocation in Functions

#include <stdio.h>
#include <stdlib.h>

void allocateArray(int **arr, int size) {
    *arr = (int*)malloc(size * sizeof(int));
    if (*arr == NULL) {
        printf("Allocation failed!\n");
        return;
    }
    for (int i = 0; i < size; i++) {
        (*arr)[i] = i * 10;
    }
}

void freeArray(int **arr) {
    free(*arr);
    *arr = NULL;  // Prevent dangling pointer
}

int main() {
    int *myArray = NULL;
    int size = 5;

    allocateArray(&myArray, size);

    for (int i = 0; i < size; i++) {
        printf("%d ", myArray[i]);
    }
    printf("\n");

    freeArray(&myArray);

    return 0;
}

3. Swapping Pointers

void swapPointers(int **p1, int **p2) {
    int *temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

int main() {
    int a = 10, b = 20;
    int *pa = &a, *pb = &b;

    printf("Before: *pa = %d, *pb = %d\n", *pa, *pb);
    swapPointers(&pa, &pb);
    printf("After:  *pa = %d, *pb = %d\n", *pa, *pb);

    return 0;
}

Double Pointers with Arrays

Array of Pointers

int a = 1, b = 2, c = 3;
int *arr[3] = {&a, &b, &c};  // Array of 3 pointers
int **pp = arr;              // Double pointer to first element

printf("%d\n", *arr[0]);   // 1
printf("%d\n", **pp);      // 1
printf("%d\n", **(pp+1));  // 2
printf("%d\n", **(pp+2));  // 3

Memory Layout

arr (array of pointers):
┌─────────┬─────────┬─────────┐
│ &a      │ &b      │ &c      │
└────┬────┴────┬────┴────┬────┘
     │         │         │
     ▼         ▼         ▼
   ┌───┐     ┌───┐     ┌───┐
   │ 1 │     │ 2 │     │ 3 │
   └───┘     └───┘     └───┘
     a         b         c

Array of Strings (char**)

char *names[] = {"Alice", "Bob", "Charlie"};
char **pp = names;

for (int i = 0; i < 3; i++) {
    printf("%s\n", pp[i]);
    // or: printf("%s\n", *(pp + i));
}

Dynamic 2D Arrays

Creating a 2D Array Dynamically

#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3, cols = 4;

    // Step 1: Allocate 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: Initialize values
    int count = 1;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = count++;
        }
    }

    // Step 4: Print matrix
    printf("Matrix:\n");
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%3d ", matrix[i][j]);
        }
        printf("\n");
    }

    // Step 5: Free memory (reverse order)
    for (int i = 0; i < rows; i++) {
        free(matrix[i]);
    }
    free(matrix);

    return 0;
}

Memory Layout of Dynamic 2D Array

matrix (int**)
    │
    ▼
┌─────────┐
│ row0_ptr│───► ┌───┬───┬───┬───┐
├─────────┤     │ 1 │ 2 │ 3 │ 4 │
│ row1_ptr│───► ├───┼───┼───┼───┤
├─────────┤     │ 5 │ 6 │ 7 │ 8 │
│ row2_ptr│───► ├───┼───┼───┼───┤
└─────────┘     │ 9 │10 │11 │12 │
                └───┴───┴───┴───┘

Alternative: Contiguous 2D Array

int rows = 3, cols = 4;

// Allocate pointers + all data contiguously
int **matrix = (int**)malloc(rows * sizeof(int*));
matrix[0] = (int*)malloc(rows * cols * sizeof(int));

// Set up row pointers
for (int i = 1; i < rows; i++) {
    matrix[i] = matrix[0] + i * cols;
}

// Now matrix[i][j] works normally
// Free with:
free(matrix[0]);
free(matrix);

Command Line Arguments

Understanding main(int argc, char **argv)

The main function can receive command-line arguments:

int main(int argc, char **argv) {
    // argc: argument count
    // argv: argument vector (array of strings)
}

argv Memory Layout

When program runs as: ./program hello world

argv (char**)
    │
    ▼
┌─────────┐
│ argv[0] │───► "./program\0"
├─────────┤
│ argv[1] │───► "hello\0"
├─────────┤
│ argv[2] │───► "world\0"
├─────────┤
│  NULL   │     (null terminator)
└─────────┘

Processing Command Line Arguments

#include <stdio.h>

int main(int argc, char **argv) {
    printf("Program name: %s\n", argv[0]);
    printf("Number of arguments: %d\n", argc);

    printf("\nAll arguments:\n");
    for (int i = 0; i < argc; i++) {
        printf("  argv[%d] = \"%s\"\n", i, argv[i]);
    }

    // Alternative: using pointer arithmetic
    printf("\nUsing pointer arithmetic:\n");
    char **ptr = argv;
    while (*ptr != NULL) {
        printf("  %s\n", *ptr);
        ptr++;
    }

    return 0;
}

char *argv[] vs char **argv

Both declarations are equivalent for function parameters:

int main(int argc, char *argv[])   // Array notation
int main(int argc, char **argv)    // Pointer notation

The array notation is syntactic sugar - in function parameters, arrays decay to pointers.


Common Mistakes and Pitfalls

1. Forgetting to Initialize Pointers

// WRONG
int **pp;        // Uninitialized - contains garbage
**pp = 10;       // CRASH! Dereferencing garbage address

// CORRECT
int value = 10;
int *ptr = &value;
int **pp = &ptr;
**pp = 20;       // Safe

2. Wrong Level of Dereferencing

int x = 5;
int *p = &x;
int **pp = &p;

// WRONG - type mismatch
int *wrong = pp;     // Should be int** not int*

// WRONG - too few dereferences
printf("%d\n", *pp);  // Prints address, not value

// CORRECT
printf("%d\n", **pp); // Prints 5

3. Memory Leaks with Dynamic Allocation

// WRONG - memory leak
int **matrix = malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++) {
    matrix[i] = malloc(cols * sizeof(int));
}
free(matrix);  // Rows are leaked!

// CORRECT - free rows first
for (int i = 0; i < rows; i++) {
    free(matrix[i]);
}
free(matrix);

4. Dangling Double Pointers

int** createMatrix() {
    int arr[3] = {1, 2, 3};  // Local array
    int *p = arr;
    return &p;  // WRONG! p is local, will be invalid
}

// CORRECT - use dynamic allocation
int** createMatrix() {
    int **matrix = malloc(sizeof(int*));
    *matrix = malloc(3 * sizeof(int));
    // ...initialize...
    return matrix;
}

5. Type Confusion

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

// WRONG - &p is int**, not int*
int *wrong = &p;   // Type mismatch!

// CORRECT
int **pp = &p;     // Proper type

Best Practices

1. Always Initialize

// Good practice
int **pp = NULL;  // Initialize to NULL

// Check before use
if (pp != NULL && *pp != NULL) {
    **pp = 100;
}

2. Use typedef for Clarity

typedef int* IntPtr;
typedef int** IntPtrPtr;

IntPtr p = &value;
IntPtrPtr pp = &p;

3. Clear Naming Convention

int value;
int *ptr_value;      // Prefix with ptr_
int **ptr_ptr_value; // Or ptr_ptr_

// Or suffix with _ptr
int *value_ptr;
int **value_ptr_ptr;

4. Free Memory in Reverse Order

// Allocation order: matrix -> rows
int **matrix = malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++) {
    matrix[i] = malloc(cols * sizeof(int));
}

// Free in reverse: rows -> matrix
for (int i = 0; i < rows; i++) {
    free(matrix[i]);
    matrix[i] = NULL;  // Prevent dangling
}
free(matrix);
matrix = NULL;

5. Document Pointer Ownership

/**
 * Creates a dynamic 2D array.
 * @param rows Number of rows
 * @param cols Number of columns
 * @return Pointer to 2D array. Caller is responsible for freeing.
 */
int** createMatrix(int rows, int cols);

/**
 * Frees a 2D array created by createMatrix.
 * @param matrix The matrix to free
 * @param rows Number of rows
 */
void freeMatrix(int **matrix, int rows);

Summary

Key Concepts

ConceptSyntaxDescription
Declarationint **pp;Pointer to pointer
Assignmentpp = &ptr;Store address of pointer
Single Deref*ppAccess the pointer
Double Deref**ppAccess the value
Sizesizeof(int**)Same as any pointer

When to Use Double Pointers

  1. Modifying pointers in functions - Pass &ptr to change what ptr points to
  2. Dynamic 2D arrays - int **matrix for runtime-sized matrices
  3. Array of strings - char **strings for string collections
  4. Command-line arguments - char **argv in main
  5. Linked list operations - Modify head pointer through function

Dereferencing Quick Reference

int x = 42;
int *p = &x;
int **pp = &p;

pp      →  Address of p     (e.g., 0x2000)
*pp     →  Value in p       (e.g., 0x1000 - address of x)
**pp    →  Value in x       (42)

Common Patterns

// Pattern 1: Modify pointer in function
void modify(int **pp) { *pp = newAddress; }
modify(&myPtr);

// Pattern 2: Allocate array in function
void allocate(int **pp, int n) { *pp = malloc(n * sizeof(int)); }
allocate(&myArray, 10);

// Pattern 3: Dynamic 2D array
int **matrix = malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++)
    matrix[i] = malloc(cols * sizeof(int));

Practice Exercises

  1. Write a function that swaps two integer pointers using a double pointer
  2. Create a dynamic 2D array, fill it with values, and print it
  3. Implement a function that allocates memory for an integer and returns via double pointer
  4. Write a program that processes command-line arguments and prints them in reverse
  5. Create an array of strings dynamically and sort them alphabetically

See the exercises.c file for hands-on practice problems with solutions.

Pointer To Pointer - C Programming Tutorial | DeepML