C Double Pointers (int**), Function Pointers (Callbacks) & Pointer Safety
Welcome to Phase 9 (Chapter 23): Advanced C Pointers โ Double Pointers, Function Pointers & Pointer Safety Masterclass! Once you have mastered single pointers, advanced systems software engineering requires manipulating pointers themselves and treating executable code instructions as first-class memory addresses. Double Pointers (Type**) allow functions to modify caller pointer addresses and construct dynamic 2D matrices, while Function Pointers enable event-driven callback architectures, pluggable algorithm strategies, and object-oriented polymorphism in pure C. In this comprehensive textbook-grade guide, you will master multi-level indirection, explore callback architecture, and memorize the 5 Golden Commandments of pointer safety.
A Double Pointer is a variable that stores the Memory Address of another Pointer Variable:
RAM Address: 0x1000 0x2000 0x3000
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
Stored Content: โ 100 โ <โโโ โ 0x1000 โ <โโโ โ 0x2000 โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
Variable: val ptr ptrToPtr
Type: int int* int**
โข *ptrToPtr yields 0x1000 (the address of val).
โข **ptrToPtr yields 100 (the actual value of val)!
โก Why Do We Need Double Pointers in Real Software?
1. Modifying Pointer Addresses in Functions: If a function needs to allocate or redirect a caller's pointer (e.g. allocateBuffer(&ptr, size)), passing int* by value only copies the address. Passing int** allows mutating the caller's pointer directly!
2. Dynamic 2D Matrices: Array of pointers to dynamically allocated row buffers (int** matrix).
In compiled C binaries, functions reside in the Code (Text) Segment of RAM. Just like variables, Every Function Has an Exact RAM Memory Address (the entry point of its machine instructions)!
๐ Function Pointer Syntax Blueprint:
$$\mathbf{Return\_Type\; (*Pointer\_Name)(Param\_Types);}$$
โข Example: int (*operation)(int, int); declares a function pointer that can point to any function accepting two ints and returning an int (like add or multiply)!
#include <stdio.h>
// Mathematical Operations
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
// Higher-Order Callback Function: accepts a function pointer!
void executeMath(int x, int y, int (*operation)(int, int)) {
int result = operation(x, y);
printf("Computed Result: %d\n", result);
}
// Double pointer memory modification demo
void redirectPointer(int **pp, int *newTarget) {
*pp = newTarget; // Modifies the caller's pointer address!
}
int main(void) {
int a = 10, b = 20;
printf("1. Calling with add callback: ");
executeMath(a, b, add);
printf("2. Calling with multiply callback: ");
executeMath(a, b, multiply);
// Double Pointer Demonstration
int val1 = 50, val2 = 999;
int *p = &val1;
printf("\nBefore redirect: *p = %d (points to val1)\n", *p);
redirectPointer(&p, &val2);
printf("After redirect: *p = %d (points to val2!)\n", *p);
return 0;
}
๐ The 5 Absolute Rules Every C Developer Must Follow:
1. Initialize Every Pointer Immediately: Never leave a pointer uninitialized (Wild Pointer). If you don't have an address yet, assign int *ptr = NULL;.
2. Always Validate Before Dereference: Guard every pointer access with if (ptr != NULL).
3. Never Dereference Freed Memory: Once you call free(ptr), immediately set ptr = NULL; to eliminate dangling pointers.
4. Never Return Addresses of Local Stack Variables: Returning a pointer to a stack-allocated variable triggers fatal memory corruption.
5. Cast to (void*) When Printing Addresses: Always use printf("%p", (void*)ptr); for standard compliance.
Q1: Why are parentheses mandatory in int (*fp)(int)?
Without parentheses, int *fp(int); declares a function named fp that returns a pointer to an integer (int*). The parentheses (*fp) bind the asterisk to the identifier, declaring a pointer to a function.
Q2: How do function pointers enable Object-Oriented Programming (OOP) in C?
In C structures, you can embed function pointers as "methods" (e.g. struct Button { void (*onClick)(void); };). This is the exact mechanism used by the Linux Kernel (VFS file operations) and the COM/GTK architectures to achieve polymorphism.
Run this modular math dispatcher in our live GCC compiler:
#include <stdio.h>
int square(int x) { return x * x; }
int cube(int x) { return x * x * x; }
int main(void) {
int (*dispatcher[2])(int) = {square, cube};
printf("Square of 5 = %d\n", dispatcher[0](5));
printf("Cube of 5 = %d\n", dispatcher[1](5));
return 0;
}