C Programming: Low-Level Mastery
Pointers

Pointer to Pointer

Master multi-level indirection with pointers to pointers. Learn double pointers for 2D arrays, function arguments, dynamic memory, and understand the syntax and use cases for multiple indirection.

Understanding Pointer to Pointer

A pointer to pointer stores the address of another pointer. Just as a pointer holds an address to data, a double pointer (pointer to pointer) holds an address to a pointer. This adds a level of indirection, enabling powerful patterns like modifying pointers through functions, dynamic 2D arrays, and pointer arrays.

C
#include <stdio.h>

int main(void) {
    int x = 42;
    int *ptr = &x;        // Pointer to int
    int **pptr = &ptr;    // Pointer to pointer to int
    
    /* Three ways to access x */
    printf("Direct: %d\n", x);           // 42
    printf("Through ptr: %d\n", *ptr);   // 42
    printf("Through pptr: %d\n", **pptr); // 42 (double dereference)
    
    /* Addresses */
    printf("Address of x: %p\n", (void*)&x);
    printf("Value of ptr (address of x): %p\n", (void*)ptr);
    printf("Value of pptr (address of ptr): %p\n", (void*)pptr);
    
    /* Modify through double pointer */
    **pptr = 100;
    printf("x = %d\n", x);  // 100
    
    return 0;
}

/* Visualization:
   x:     [42]
   ptr:   [address of x] -----&gt; x
   pptr:  [address of ptr] ---&gt; ptr -----&gt; x
   
   *ptr gives x
   **pptr gives x (two dereferences)
*/

Syntax and Declaration

Pointer to pointer syntax adds another asterisk. Each asterisk represents one level of indirection. You can have triple pointers, quadruple pointers, etc., though beyond double pointers is rare in practice.

C
/* Pointer declarations */
int x = 10;

int *ptr;         // Pointer to int
int **pptr;       // Pointer to pointer to int
int ***ppptr;     // Pointer to pointer to pointer to int

/* Initialization */
int *p1 = &x;
int **p2 = &p1;
int ***p3 = &p2;

/* Reading values */
printf("%d\n", x);      // 10
printf("%d\n", *p1);    // 10 (one deref)
printf("%d\n", **p2);   // 10 (two derefs)
printf("%d\n", ***p3);  // 10 (three derefs)

/* Different pointer types */
char c = 'A';
char *cp = &c;
char **cpp = &cp;

double d = 3.14;
double *dp = &d;
double **dpp = &dp;

/* Const with double pointers */
const int **pp1;           // Pointer to pointer to const int
int *const *pp2;           // Pointer to const pointer to int
int **const pp3 = NULL;    // Const pointer to pointer to int
const int *const *pp4;     // Pointer to const pointer to const int

/* Array of pointers vs pointer to array */
int *arr_of_ptrs[10];     // Array of 10 pointers to int
int (*ptr_to_arr)[10];    // Pointer to array of 10 ints

Modifying Pointers Through Functions

The most common use of double pointers: enabling functions to modify pointer variables. Since C is pass-by-value, passing a pointer to a pointer allows the function to change what the original pointer points to. Essential for functions that allocate memory or modify pointer arguments.

C
#include <stdlib.h>

/* Allocate memory and modify pointer */
void allocate_array(int **ptr, int size) {
    *ptr = malloc(size * sizeof(int));
    if (*ptr == NULL) {
        fprintf(stderr, "Allocation failed\n");
        return;
    }
    
    /* Initialize array */
    for (int i = 0; i < size; i++) {
        (*ptr)[i] = i * 10;
    }
}

/* Usage */
int main(void) {
    int *array = NULL;
    
    allocate_array(&array, 5);  // Pass address of array pointer
    
    if (array != NULL) {
        for (int i = 0; i < 5; i++) {
            printf("%d ", array[i]);
        }
        printf("\n");
        
        free(array);
        array = NULL;
    }
    
    return 0;
}

/* Swap two pointers */
void swap_pointers(int **p1, int **p2) {
    int *temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

void swap_example(void) {
    int x = 10, y = 20;
    int *ptr1 = &x;
    int *ptr2 = &y;
    
    printf("Before: *ptr1=%d, *ptr2=%d\n", *ptr1, *ptr2);
    
    swap_pointers(&ptr1, &ptr2);
    
    printf("After: *ptr1=%d, *ptr2=%d\n", *ptr1, *ptr2);
    // Pointers swapped, now ptr1-&gt;y and ptr2-&gt;x
}

/* Update pointer in linked list */
typedef struct Node {
    int data;
    struct Node *next;
} Node;

void insert_at_head(Node **head, int value) {
    Node *new_node = malloc(sizeof(Node));
    if (new_node == NULL) return;
    
    new_node-&gt;data = value;
    new_node-&gt;next = *head;
    *head = new_node;  // Update head pointer
}

/* Find and remove node */
int remove_node(Node **head, int value) {
    Node **indirect = head;
    
    while (*indirect != NULL) {
        if ((*indirect)-&gt;data == value) {
            Node *to_delete = *indirect;
            *indirect = (*indirect)-&gt;next;
            free(to_delete);
            return 1;  // Found and removed
        }
        indirect = &((*indirect)-&gt;next);
    }
    
    return 0;  // Not found
}

Dynamic 2D Arrays

Pointer to pointer enables dynamic 2D arrays with proper array syntax. This creates an array of pointers, each pointing to a row. Memory isn't contiguous like static 2D arrays, but you get runtime-determined dimensions and natural arr[i][j] indexing.

C
/* Allocate dynamic 2D array */
int** create_2d_array(int rows, int cols) {
    /* Allocate array of row pointers */
    int **matrix = malloc(rows * sizeof(int*));
    if (matrix == NULL) return NULL;
    
    /* Allocate each row */
    for (int i = 0; i < rows; i++) {
        matrix[i] = malloc(cols * sizeof(int));
        if (matrix[i] == NULL) {
            /* Cleanup on failure */
            for (int j = 0; j < i; j++) {
                free(matrix[j]);
            }
            free(matrix);
            return NULL;
        }
    }
    
    return matrix;
}

/* Free dynamic 2D array */
void free_2d_array(int **matrix, int rows) {
    for (int i = 0; i < rows; i++) {
        free(matrix[i]);
    }
    free(matrix);
}

/* Use dynamic 2D array */
void dynamic_2d_example(void) {
    int rows = 3, cols = 4;
    
    int **matrix = create_2d_array(rows, cols);
    if (matrix == NULL) {
        fprintf(stderr, "Allocation failed\n");
        return;
    }
    
    /* Initialize */
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = i * cols + j;
        }
    }
    
    /* Print */
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%2d ", matrix[i][j]);
        }
        printf("\n");
    }
    
    free_2d_array(matrix, rows);
}

/* Contiguous allocation (better cache performance) */
int** create_2d_contiguous(int rows, int cols) {
    /* Allocate pointer array */
    int **matrix = malloc(rows * sizeof(int*));
    if (matrix == NULL) return NULL;
    
    /* Allocate all data in one block */
    int *data = malloc(rows * cols * sizeof(int));
    if (data == NULL) {
        free(matrix);
        return NULL;
    }
    
    /* Set up row pointers */
    for (int i = 0; i < rows; i++) {
        matrix[i] = data + i * cols;
    }
    
    return matrix;
}

void free_2d_contiguous(int **matrix) {
    if (matrix != NULL) {
        free(matrix[0]);  // Free data block
        free(matrix);     // Free pointer array
    }
}

Array of Pointers

An array of pointers (char *argv[], for example) is different from a pointer to pointer, though they're used similarly. Array of pointers is a fixed-size array where each element is a pointer. Pointer to pointer is a variable that can point to such an array or to a single pointer.

C
/* Array of string pointers */
int main(int argc, char *argv[]) {
    /* argv is array of pointers to char
       argv[0] is char* (pointer to first string)
       argv[1] is char* (pointer to second string)
       etc.
    */
    
    printf("Program: %s\n", argv[0]);
    
    for (int i = 1; i < argc; i++) {
        printf("Arg %d: %s\n", i, argv[i]);
    }
    
    return 0;
}

/* Array of pointers to functions */
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }

void function_array_example(void) {
    int (*operations[3])(int, int) = {add, subtract, multiply};
    
    int a = 10, b = 5;
    
    for (int i = 0; i < 3; i++) {
        printf("Result: %d\n", operations[i](a, b));
    }
}

/* Array of string literals */
void string_array(void) {
    char *days[] = {
        "Monday",
        "Tuesday", 
        "Wednesday",
        "Thursday",
        "Friday",
        "Saturday",
        "Sunday"
    };
    
    int num_days = sizeof(days) / sizeof(days[0]);
    
    for (int i = 0; i < num_days; i++) {
        printf("%s\n", days[i]);
    }
}

/* Ragged array (rows with different lengths) */
void ragged_array_example(void) {
    char *words[] = {
        "Hi",
        "Hello",
        "Greetings",
        "Salutations"
    };
    
    for (int i = 0; i < 4; i++) {
        printf("%s (length: %zu)\n", words[i], strlen(words[i]));
    }
}

/* Pointer to array of pointers */
void ptr_to_array_of_ptrs(void) {
    int x = 1, y = 2, z = 3;
    int *arr[] = {&x, &y, &z};
    
    int **ptr = arr;  // Points to first element of array
    
    for (int i = 0; i < 3; i++) {
        printf("%d ", *ptr[i]);  // or *(ptr[i])
    }
    printf("\n");
}

Common Use Cases

Double pointers appear in several common C patterns. Understanding these use cases helps you recognize when and how to use pointer to pointer effectively.

C
/* Use case 1: main() arguments */
int main(int argc, char **argv) {  // Or: char *argv[]
    // argv is pointer to pointer to char
    // Each argv[i] points to a string
}

/* Use case 2: String array manipulation */
void sort_strings(char **strings, int count) {
    for (int i = 0; i < count - 1; i++) {
        for (int j = i + 1; j < count; j++) {
            if (strcmp(strings[i], strings[j]) &gt; 0) {
                /* Swap pointers */
                char *temp = strings[i];
                strings[i] = strings[j];
                strings[j] = temp;
            }
        }
    }
}

/* Use case 3: Returning allocated memory */
int allocate_buffer(char **buffer, size_t size) {
    *buffer = malloc(size);
    if (*buffer == NULL) {
        return -1;  // Error
    }
    return 0;  // Success
}

void buffer_example(void) {
    char *buf = NULL;
    
    if (allocate_buffer(&buf, 1024) == 0) {
        strcpy(buf, "Hello, World!");
        printf("%s\n", buf);
        free(buf);
    }
}

/* Use case 4: Iterator pattern */
typedef struct {
    int *current;
    int *end;
} Iterator;

int iterator_next(Iterator *it, int **value) {
    if (it-&gt;current >= it-&gt;end) {
        return 0;  // No more elements
    }
    
    *value = it-&gt;current;
    it-&gt;current++;
    return 1;  // Success
}

void iterator_example(void) {
    int arr[] = {1, 2, 3, 4, 5};
    Iterator it = {arr, arr + 5};
    
    int *value;
    while (iterator_next(&it, &value)) {
        printf("%d ", *value);
    }
    printf("\n");
}

/* Use case 5: Generic data structures */
typedef struct ListNode {
    void *data;
    struct ListNode *next;
} ListNode;

void list_insert(ListNode **head, void *data) {
    ListNode *new_node = malloc(sizeof(ListNode));
    if (new_node == NULL) return;
    
    new_node-&gt;data = data;
    new_node-&gt;next = *head;
    *head = new_node;
}

/* Use case 6: Command tables */
typedef struct {
    char *name;
    void (*handler)(int argc, char **argv);
} Command;

void cmd_help(int argc, char **argv) {
    printf("Help command\n");
}

void cmd_quit(int argc, char **argv) {
    exit(0);
}

Command commands[] = {
    {"help", cmd_help},
    {"quit", cmd_quit},
    {NULL, NULL}
};

Common Pitfalls

Double pointers add complexity and potential for errors. Understanding these pitfalls helps you avoid crashes and memory leaks.

C
/* Pitfall 1: Incorrect dereferencing */
int x = 10;
int *ptr = &x;
int **pptr = &ptr;

// printf("%d\n", *pptr);  // WRONG: Prints address, not value
printf("%d\n", **pptr);    // CORRECT: 10

/* Pitfall 2: Memory leak with double pointer */
void bad_allocate(int **ptr) {
    *ptr = malloc(100 * sizeof(int));
    // If caller doesn't free, memory leaked
}

/* Better: Document responsibility */
/**
 * Allocates array. Caller must free.
 * Returns 0 on success, -1 on failure.
 */
int good_allocate(int **ptr, int size) {
    *ptr = malloc(size * sizeof(int));
    return (*ptr == NULL) ? -1 : 0;
}

/* Pitfall 3: Not checking allocation */
void unsafe(int **ptr) {
    *ptr = malloc(100 * sizeof(int));
    (*ptr)[0] = 10;  // CRASH if malloc failed!
}

void safe(int **ptr) {
    *ptr = malloc(100 * sizeof(int));
    if (*ptr == NULL) {
        return;
    }
    (*ptr)[0] = 10;  // Safe
}

/* Pitfall 4: Dangling pointer through double pointer */
void create_dangling(int **ptr) {
    int local = 42;
    *ptr = &local;  // WRONG: local destroyed after return
}

/* Pitfall 5: Confusion with array indexing */
int **matrix;
// matrix[i][j]  // Correct
// **matrix[i][j]  // WRONG: Too many dereferences

/* Pitfall 6: Not freeing all levels */
int **matrix = create_2d_array(3, 4);
// free(matrix);  // WRONG: Leaks row allocations

// Correct:
for (int i = 0; i < 3; i++) {
    free(matrix[i]);
}
free(matrix);

/* Pitfall 7: Uninitialized double pointer */
int **pptr;
// **pptr = 10;  // CRASH: pptr not initialized

/* Correct: */
int x = 5;
int *ptr = &x;
int **pptr2 = &ptr;
**pptr2 = 10;  // OK

/* Best practices */

// 1. Initialize to NULL
int **ptr = NULL;

// 2. Check for NULL at each level
if (ptr != NULL && *ptr != NULL) {
    value = **ptr;
}

// 3. Document ownership and lifetime
/**
 * Returns allocated matrix. Caller must free with free_matrix().
 */
int** create_matrix(int rows, int cols);

// 4. Typedef for clarity
typedef int** IntMatrix;
IntMatrix matrix = create_matrix(3, 4);

// 5. Avoid excessive indirection
// Instead of int***, consider a struct

Summary & What's Next

Key Takeaways:

  • ✅ Double pointer stores address of a pointer
  • ✅ ** dereferences twice to get the value
  • ✅ Essential for modifying pointers in functions
  • ✅ Enables dynamic 2D arrays
  • ✅ Used in main(argc, argv)
  • ✅ Array of pointers vs pointer to pointer
  • ✅ Must free all allocation levels
  • ✅ Check NULL at each indirection level

What's Next?

Let's learn about const pointers and pointer qualifiers!