In C, pointers are powerful tools that allow direct memory manipulation and efficient data handling. But what happens when you need a pointer that points to another pointer? Enter pointer to pointer, also known as double pointer or pointer-to-pointer. This concept of multiple indirection opens up sophisticated programming techniques used in dynamic data structures, multi-dimensional arrays, and function parameter manipulation.
What is a Pointer to Pointer?
A pointer to pointer is exactly what it sounds like: a variable that stores the address of another pointer variable. Just as a regular pointer points to a data value, a double pointer points to a memory location that itself contains a pointer.
Visual Representation:
Single Pointer: ptr1 ──→ [value] | address of value Double Pointer: ptr2 ──→ ptr1 ──→ [value] | | address address of ptr1 of value
Basic Syntax and Declaration
#include <stdio.h>
int main() {
int value = 42;
int *ptr = &value; // Single pointer to int
int **dptr = &ptr; // Double pointer to int
printf("Value: %d\n", value);
printf("Value via single pointer: %d\n", *ptr);
printf("Value via double pointer: %d\n", **dptr);
printf("\nAddresses:\n");
printf("Address of value: %p\n", (void*)&value);
printf("Address stored in ptr: %p\n", (void*)ptr);
printf("Address of ptr: %p\n", (void*)&ptr);
printf("Address stored in dptr: %p\n", (void*)dptr);
return 0;
}
Output:
Value: 42 Value via single pointer: 42 Value via double pointer: 42 Addresses: Address of value: 0x7ffd8a3b5a4c Address stored in ptr: 0x7ffd8a3b5a4c Address of ptr: 0x7ffd8a3b5a50 Address stored in dptr: 0x7ffd8a3b5a50
Multiple Levels of Indirection
You can have multiple levels of indirection, though more than two or three levels rarely improve code clarity.
#include <stdio.h>
int main() {
int value = 42;
int *ptr = &value; // Level 1
int **ptr2 = &ptr; // Level 2
int ***ptr3 = &ptr2; // Level 3
int ****ptr4 = &ptr3; // Level 4
printf("Value access:\n");
printf("Direct: %d\n", value);
printf("One level: %d\n", *ptr);
printf("Two levels: %d\n", **ptr2);
printf("Three levels: %d\n", ***ptr3);
printf("Four levels: %d\n", ****ptr4);
return 0;
}
Common Use Cases
1. Modifying Pointer Arguments in Functions
One of the most common uses of double pointers is to modify a pointer argument inside a function.
#include <stdio.h>
#include <stdlib.h>
// Without double pointer - doesn't work as expected
void allocate_bad(int *ptr, int size) {
ptr = malloc(size * sizeof(int));
if (ptr) {
printf(" Allocated memory at %p (bad)\n", (void*)ptr);
}
}
// With double pointer - works correctly
void allocate_good(int **ptr, int size) {
*ptr = malloc(size * sizeof(int));
if (*ptr) {
printf(" Allocated memory at %p (good)\n", (void*)*ptr);
}
}
int main() {
int *array1 = NULL;
int *array2 = NULL;
printf("Before allocation:\n");
printf(" array1: %p\n", (void*)array1);
printf(" array2: %p\n", (void*)array2);
printf("\nTrying allocate_bad:\n");
allocate_bad(array1, 10);
printf(" After allocate_bad, array1: %p\n", (void*)array1);
printf("\nTrying allocate_good:\n");
allocate_good(&array2, 10);
printf(" After allocate_good, array2: %p\n", (void*)array2);
// Clean up
free(array2);
return 0;
}
Output:
Before allocation: array1: (nil) array2: (nil) Trying allocate_bad: Allocated memory at 0x55a8c3d526b0 (bad) After allocate_bad, array1: (nil) Trying allocate_good: Allocated memory at 0x55a8c3d526b0 (good) After allocate_good, array2: 0x55a8c3d526b0
2. Dynamic 2D Arrays
Double pointers are essential for creating and manipulating dynamic 2D arrays.
#include <stdio.h>
#include <stdlib.h>
// Create a dynamic 2D array
int** create_matrix(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) {
// Clean up on failure
for (int j = 0; j < i; j++) {
free(matrix[j]);
}
free(matrix);
return NULL;
}
}
return matrix;
}
// Initialize matrix with values
void init_matrix(int **matrix, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix[i][j] = i * cols + j + 1;
}
}
}
// Print matrix
void print_matrix(int **matrix, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%4d ", matrix[i][j]);
}
printf("\n");
}
}
// Free matrix memory
void free_matrix(int **matrix, int rows) {
for (int i = 0; i < rows; i++) {
free(matrix[i]);
}
free(matrix);
}
int main() {
int rows = 3, cols = 4;
int **matrix = create_matrix(rows, cols);
if (matrix == NULL) {
printf("Failed to allocate matrix\n");
return 1;
}
init_matrix(matrix, rows, cols);
printf("Dynamic 2D Array (%d x %d):\n", rows, cols);
print_matrix(matrix, rows, cols);
// Access elements using pointer notation
printf("\nElement at [1][2]: %d\n", *(*(matrix + 1) + 2));
printf("Equivalent: matrix[1][2] = %d\n", matrix[1][2]);
free_matrix(matrix, rows);
return 0;
}
3. Array of Strings
Double pointers are naturally suited for arrays of strings.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Array of strings (array of char*)
char *fruits[] = {
"Apple",
"Banana",
"Cherry",
"Date",
"Elderberry"
};
void print_string_array(char **arr, int size) {
for (int i = 0; i < size; i++) {
printf(" arr[%d] = %s (at %p)\n", i, arr[i], (void*)arr[i]);
}
}
int main() {
int num_fruits = sizeof(fruits) / sizeof(fruits[0]);
printf("Array of strings (char**):\n");
print_string_array(fruits, num_fruits);
printf("\nString lengths:\n");
for (int i = 0; i < num_fruits; i++) {
printf(" %s: %zu characters\n", fruits[i], strlen(fruits[i]));
}
// Dynamic array of strings
char **dynamic_strings = malloc(3 * sizeof(char*));
dynamic_strings[0] = strdup("Hello");
dynamic_strings[1] = strdup("World");
dynamic_strings[2] = strdup("!");
printf("\nDynamic string array:\n");
for (int i = 0; i < 3; i++) {
printf(" dynamic_strings[%d] = %s\n", i, dynamic_strings[i]);
free(dynamic_strings[i]);
}
free(dynamic_strings);
return 0;
}
4. Linked List Operations with Double Pointers
Double pointers simplify certain linked list operations, especially insertion at the beginning.
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node* next;
} Node;
// Insert at beginning - using double pointer
void insert_beginning(Node **head, int value) {
Node *new_node = malloc(sizeof(Node));
new_node->data = value;
new_node->next = *head;
*head = new_node;
printf(" Inserted %d at beginning\n", value);
}
// Insert at end - using double pointer for head modification if empty
void insert_end(Node **head, int value) {
Node *new_node = malloc(sizeof(Node));
new_node->data = value;
new_node->next = NULL;
if (*head == NULL) {
*head = new_node;
} else {
Node *current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = new_node;
}
printf(" Inserted %d at end\n", value);
}
// Delete a node with given value
void delete_value(Node **head, int value) {
if (*head == NULL) return;
// If head needs to be deleted
if ((*head)->data == value) {
Node *temp = *head;
*head = (*head)->next;
free(temp);
printf(" Deleted %d from beginning\n", value);
return;
}
// Search for node to delete
Node *current = *head;
while (current->next != NULL && current->next->data != value) {
current = current->next;
}
if (current->next != NULL) {
Node *temp = current->next;
current->next = temp->next;
free(temp);
printf(" Deleted %d\n", value);
}
}
void print_list(Node *head) {
printf("List: ");
while (head != NULL) {
printf("%d -> ", head->data);
head = head->next;
}
printf("NULL\n");
}
void free_list(Node **head) {
Node *current = *head;
while (current != NULL) {
Node *next = current->next;
free(current);
current = next;
}
*head = NULL;
printf("List freed\n");
}
int main() {
Node *head = NULL;
printf("Linked List Operations with Double Pointers:\n");
insert_beginning(&head, 10);
insert_beginning(&head, 20);
insert_beginning(&head, 30);
print_list(head);
insert_end(&head, 40);
insert_end(&head, 50);
print_list(head);
delete_value(&head, 20);
print_list(head);
delete_value(&head, 30);
print_list(head);
free_list(&head);
print_list(head);
return 0;
}
5. Function Pointer Arrays
Double pointers can be used with function pointers for more complex scenarios.
#include <stdio.h>
// Some example 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; }
int divide(int a, int b) { return b ? a / b : 0; }
// Array of function pointers
int (*ops[])(int, int) = {add, subtract, multiply, divide};
const char *op_names[] = {"Add", "Subtract", "Multiply", "Divide"};
// Function that takes array of function pointers
void apply_operations(int (**functions)(int, int), int count, int x, int y) {
for (int i = 0; i < count; i++) {
printf("%s(%d, %d) = %d\n",
op_names[i], x, y, functions[i](x, y));
}
}
int main() {
int x = 10, y = 5;
int num_ops = sizeof(ops) / sizeof(ops[0]);
printf("Applying operations using function pointer array:\n");
apply_operations(ops, num_ops, x, y);
// Double pointer to function pointer (rare, but possible)
int (**ptr_to_op_array)(int, int) = ops;
printf("\nAccessing via double pointer:\n");
printf(" First operation result: %d\n", (*ptr_to_op_array)(x, y));
return 0;
}
Advanced Techniques
1. Pointer to Pointer to Pointer (Triple Pointer)
While rare, triple pointers can be useful in specific scenarios.
#include <stdio.h>
#include <stdlib.h>
// Function that modifies a double pointer
void create_string_array(char ***array, int size) {
*array = malloc(size * sizeof(char*));
for (int i = 0; i < size; i++) {
(*array)[i] = malloc(20 * sizeof(char));
snprintf((*array)[i], 20, "String %d", i + 1);
}
}
// Function that modifies a triple pointer
void create_and_fill(char ****array, int rows, int cols) {
// Allocate array of double pointers (rows)
*array = malloc(rows * sizeof(char**));
for (int i = 0; i < rows; i++) {
// Allocate row of char pointers
(*array)[i] = malloc(cols * sizeof(char*));
for (int j = 0; j < cols; j++) {
// Allocate actual strings
(*array)[i][j] = malloc(30 * sizeof(char));
snprintf((*array)[i][j], 30, "Cell[%d][%d]", i, j);
}
}
}
int main() {
// Triple pointer example
char ***matrix;
int rows = 2, cols = 3;
create_and_fill(&matrix, rows, cols);
printf("3D String Matrix (char***):\n");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf(" %s", matrix[i][j]);
}
printf("\n");
}
// Clean up
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
free(matrix[i][j]);
}
free(matrix[i]);
}
free(matrix);
return 0;
}
2. Jagged Arrays (Arrays with Different Row Lengths)
Double pointers are perfect for jagged arrays where each row has different length.
#include <stdio.h>
#include <stdlib.h>
int** create_jagged_array(int rows, int *row_sizes) {
int **jagged = malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++) {
jagged[i] = malloc(row_sizes[i] * sizeof(int));
for (int j = 0; j < row_sizes[i]; j++) {
jagged[i][j] = i * 10 + j + 1;
}
}
return jagged;
}
void print_jagged_array(int **jagged, int rows, int *row_sizes) {
for (int i = 0; i < rows; i++) {
printf("Row %d (size %d): ", i, row_sizes[i]);
for (int j = 0; j < row_sizes[i]; j++) {
printf("%3d ", jagged[i][j]);
}
printf("\n");
}
}
int main() {
int rows = 4;
int row_sizes[] = {1, 3, 2, 4};
int **jagged = create_jagged_array(rows, row_sizes);
printf("Jagged Array (different row lengths):\n");
print_jagged_array(jagged, rows, row_sizes);
// Clean up
for (int i = 0; i < rows; i++) {
free(jagged[i]);
}
free(jagged);
return 0;
}
3. Generic Swap Function
Double pointers enable writing generic functions that can swap any pointer type.
#include <stdio.h>
#include <string.h>
// Generic swap for any pointer type using void**
void generic_swap(void **a, void **b) {
void *temp = *a;
*a = *b;
*b = temp;
}
int main() {
// Swap integers (via pointers)
int x = 10, y = 20;
int *px = &x, *py = &y;
printf("Before swap: *px = %d, *py = %d\n", *px, *py);
generic_swap((void**)&px, (void**)&py);
printf("After swap: *px = %d, *py = %d\n", *px, *py);
// Swap strings
char *str1 = "Hello";
char *str2 = "World";
printf("\nBefore swap: str1 = %s, str2 = %s\n", str1, str2);
generic_swap((void**)&str1, (void**)&str2);
printf("After swap: str1 = %s, str2 = %s\n", str1, str2);
return 0;
}
Common Pitfalls and How to Avoid Them
1. Forgetting Levels of Indirection
#include <stdio.h>
#include <stdlib.h>
void demonstrate_indirection_mistakes() {
int value = 42;
int *ptr = &value;
int **dptr = &ptr;
// CORRECT USAGE
printf("Correct:\n");
printf(" value = %d\n", value);
printf(" *ptr = %d\n", *ptr);
printf(" **dptr = %d\n\n", **dptr);
// COMMON MISTAKES
printf("Common mistakes:\n");
printf(" dptr = %p (address of ptr)\n", (void*)dptr);
// printf(" *dptr = %p (value of ptr)\n", (void*)*dptr);
// printf(" dptr[0] = %p (same as *dptr)\n", (void*)dptr[0]);
// WRONG: Using wrong number of asterisks
// printf("%d\n", *dptr); // Prints pointer, not value
// printf("%d\n", **ptr); // Compiler error
int **ptr_to_free = malloc(sizeof(int*));
*ptr_to_free = malloc(sizeof(int));
**ptr_to_free = 100;
// Correct cleanup order
free(*ptr_to_free); // Free inner allocation first
free(ptr_to_free); // Then free outer pointer
// WRONG: Freeing in wrong order
// free(ptr_to_free); // Memory leak of inner allocation
// free(*ptr_to_free); // Undefined behavior (already freed)
}
int main() {
demonstrate_indirection_mistakes();
return 0;
}
2. Memory Leaks with Double Pointers
#include <stdio.h>
#include <stdlib.h>
void demonstrate_memory_leaks() {
// WRONG - Memory leak
int **matrix = malloc(3 * sizeof(int*));
for (int i = 0; i < 3; i++) {
matrix[i] = malloc(5 * sizeof(int));
}
// Missing cleanup!
// free(matrix); // This alone leaks all rows!
// CORRECT cleanup
int **good_matrix = malloc(3 * sizeof(int*));
for (int i = 0; i < 3; i++) {
good_matrix[i] = malloc(5 * sizeof(int));
}
// Correct order: free rows first, then the array of pointers
for (int i = 0; i < 3; i++) {
free(good_matrix[i]);
}
free(good_matrix);
printf("Memory properly cleaned up\n");
}
int main() {
demonstrate_memory_leaks();
return 0;
}
3. Dangling Pointers
#include <stdio.h>
#include <stdlib.h>
int** create_dangling() {
int local = 100;
int *ptr = &local;
int **dptr = &ptr;
// DANGER: Returning address of local variable
return &ptr; // ptr will cease to exist after function returns
}
int** create_valid() {
int **dptr = malloc(sizeof(int*));
*dptr = malloc(sizeof(int));
**dptr = 42;
return dptr; // Valid - heap memory persists
}
int main() {
// DANGER - Undefined behavior
// int **bad = create_dangling();
// printf("%d\n", **bad); // Crash or garbage
// Correct approach
int **valid = create_valid();
printf("Valid double pointer value: %d\n", **valid);
// Clean up
free(*valid);
free(valid);
return 0;
}
4. Type Compatibility Issues
#include <stdio.h>
void demonstrate_type_compatibility() {
int x = 10;
int *p = &x;
int **pp = &p;
// int to int* conversion
// int **bad = &x; // ERROR: incompatible pointer types
// void* is generic, but be careful
void *vp = p; // OK - any pointer can convert to void*
void **vpp = &vp; // OK - address of void*
// void** to int** is NOT safe
// int **ipp = vpp; // Compiler warning/error
// Correct way
int **ipp = (int**)vpp; // Explicit cast
printf("Type compatibility: use explicit casts when needed\n");
}
int main() {
demonstrate_type_compatibility();
return 0;
}
Performance Considerations
Double pointers involve an extra level of indirection, which has a small performance cost.
#include <stdio.h>
#include <time.h>
#define SIZE 10000
#define ITERATIONS 1000000
void benchmark_indirection() {
int value = 42;
int *ptr = &value;
int **dptr = &ptr;
clock_t start, end;
volatile int result; // Prevent optimization
// Direct access
start = clock();
for (int i = 0; i < ITERATIONS; i++) {
result = value;
}
end = clock();
double direct_time = ((double)(end - start)) / CLOCKS_PER_SEC;
// Single pointer access
start = clock();
for (int i = 0; i < ITERATIONS; i++) {
result = *ptr;
}
end = clock();
double single_time = ((double)(end - start)) / CLOCKS_PER_SEC;
// Double pointer access
start = clock();
for (int i = 0; i < ITERATIONS; i++) {
result = **dptr;
}
end = clock();
double double_time = ((double)(end - start)) / CLOCKS_PER_SEC;
printf("Performance comparison (%d iterations):\n", ITERATIONS);
printf(" Direct access: %.4f seconds\n", direct_time);
printf(" Single pointer: %.4f seconds\n", single_time);
printf(" Double pointer: %.4f seconds\n", double_time);
}
int main() {
benchmark_indirection();
return 0;
}
Best Practices
1. Use Meaningful Variable Names
// Clear naming helps understand indirection levels
int value; // Actual data
int *ptr_to_value; // Points to value
int **ptr_to_ptr_to_value; // Points to ptr_to_value
// In functions that modify pointers
void allocate_memory(int **pointer_to_pointer) {
*pointer_to_pointer = malloc(size);
}
// In linked list operations
void insert_node(Node **head_ref, int data) {
Node *new_node = malloc(sizeof(Node));
new_node->data = data;
new_node->next = *head_ref;
*head_ref = new_node;
}
2. Check for NULL
#include <stdio.h>
#include <stdlib.h>
void safe_double_pointer_operation(int **dptr) {
// Check outer pointer
if (dptr == NULL) {
printf("Double pointer is NULL\n");
return;
}
// Check inner pointer
if (*dptr == NULL) {
printf("Inner pointer is NULL\n");
return;
}
// Safe to dereference
printf("Value: %d\n", **dptr);
}
int main() {
int value = 42;
int *ptr = &value;
int **dptr = &ptr;
safe_double_pointer_operation(dptr);
safe_double_pointer_operation(NULL);
int **null_inner = malloc(sizeof(int*));
*null_inner = NULL;
safe_double_pointer_operation(null_inner);
free(null_inner);
return 0;
}
3. Document Indirection Levels
/**
* Creates a dynamic 2D array of integers.
*
* @param rows Number of rows
* @param cols Number of columns
* @return int** - pointer to array of row pointers
* Each row pointer points to array of ints
*/
int** create_matrix(int rows, int cols) {
int **matrix = malloc(rows * sizeof(int*));
// ... implementation
return matrix;
}
/**
* Frees a matrix created by create_matrix().
*
* @param matrix Pointer to array of row pointers
* @param rows Number of rows (needed to free each row)
*/
void free_matrix(int **matrix, int rows) {
for (int i = 0; i < rows; i++) {
free(matrix[i]); // Free each row (int*)
}
free(matrix); // Free array of row pointers (int**)
}
Advanced Example: Generic Data Structure
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Generic dynamic array using void**
typedef struct {
void **data; // Array of void pointers
size_t size; // Number of elements
size_t capacity; // Allocated capacity
size_t elem_size; // Size of each element
} GenericArray;
GenericArray* array_create(size_t elem_size) {
GenericArray *arr = malloc(sizeof(GenericArray));
arr->data = NULL;
arr->size = 0;
arr->capacity = 0;
arr->elem_size = elem_size;
return arr;
}
void array_push(GenericArray *arr, void *value) {
if (arr->size >= arr->capacity) {
arr->capacity = arr->capacity == 0 ? 4 : arr->capacity * 2;
arr->data = realloc(arr->data, arr->capacity * sizeof(void*));
}
// Allocate space for element and copy
arr->data[arr->size] = malloc(arr->elem_size);
memcpy(arr->data[arr->size], value, arr->elem_size);
arr->size++;
}
void* array_get(GenericArray *arr, size_t index) {
if (index >= arr->size) return NULL;
return arr->data[index];
}
void array_free(GenericArray *arr) {
for (size_t i = 0; i < arr->size; i++) {
free(arr->data[i]);
}
free(arr->data);
free(arr);
}
int main() {
// Array of integers
GenericArray *int_array = array_create(sizeof(int));
int values[] = {10, 20, 30, 40, 50};
for (int i = 0; i < 5; i++) {
array_push(int_array, &values[i]);
}
printf("Integer array:\n");
for (size_t i = 0; i < int_array->size; i++) {
int *val = array_get(int_array, i);
printf(" arr[%zu] = %d\n", i, *val);
}
// Array of strings
GenericArray *str_array = array_create(sizeof(char*));
char *strings[] = {"Hello", "World", "Generic", "Array"};
for (int i = 0; i < 4; i++) {
array_push(str_array, &strings[i]);
}
printf("\nString array:\n");
for (size_t i = 0; i < str_array->size; i++) {
char **str = array_get(str_array, i);
printf(" arr[%zu] = %s\n", i, *str);
}
array_free(int_array);
array_free(str_array);
return 0;
}
Conclusion
Pointers to pointers are a powerful feature in C that enable sophisticated programming techniques:
Key Use Cases:
- Modifying pointer arguments in functions
- Creating and manipulating dynamic 2D arrays
- Implementing arrays of strings
- Linked list operations (especially insertion/deletion)
- Building generic data structures
- Managing arrays of function pointers
Best Practices:
- Always initialize double pointers to NULL
- Check for NULL at both levels before dereferencing
- Free memory in correct order (inner pointers first, then outer)
- Use meaningful names that indicate indirection levels
- Document your code when using multiple indirections
- Avoid excessive indirection (more than 2-3 levels)
- Be careful with type conversions and use explicit casts when needed
Common Mistakes to Avoid:
- Forgetting the correct number of asterisks when dereferencing
- Memory leaks from improper cleanup order
- Dangling pointers from returning addresses of local variables
- Assuming void** can be safely cast to other pointer types
- Not checking for NULL at both levels
Mastering pointers to pointers elevates your C programming skills, enabling you to write more flexible, efficient, and maintainable code. While the concept may seem daunting at first, practice and understanding of memory layout will make it second nature.