C Programming — Double Pointers and Function Pointers
Prerequisites
The prerequisite knowledge required for this article includes: (1). Pointers (2). Dynamic Memory Allocation
In our introductory article on pointers, we covered the most basic concepts. A pointer variable can store the address of another variable, and we can then read the value at that address, as shown below:
int number = 100; // Declare a normal variable with a value of 100
int *ptr; // Declare a pointer variable ptr
ptr = &number; // Store the "address" of number into ptr
// Now ptr stores an address (e.g., 0x101), and *ptr evaluates to 100
printf("Address is: %p\n", ptr); // Prints the address
printf("Content is: %d\n", *ptr); // Goes to the address and prints 100In this article, we are going to take a step further and introduce Double Pointers and Function Pointers.
Double Pointers
We can intuitively imagine that if the first pointer stores the address of a variable, a double pointer likely stores the address of the first pointer. Therefore, a double pointer is also known as a Pointer to Pointer. So, how do we define a double pointer?
We know that a pointer to an integer is int *ptr. The way we define a pointer is by adding a * after the type we want to point to. Therefore, a pointer pointing to an integer pointer can be written as int **ptr.
Dereferencing works the same way. When we write *ptr, we tell the computer to read the content stored at the address ptr. Thus, **ptr represents dereferencing twice. The first dereference reads the value inside the pointer it points to (which is the address of an integer), and the second dereference reads the actual value of that variable, as shown below:
int val = 500; // Original variable
int *ptr = &val; // First-level pointer: points to the address of val
int **pptr = &ptr; // Second-level pointer: points to the address of ptr
printf("Value of val: %d\n", val);
printf("Accessed via *ptr: %d\n", *ptr);
printf("Accessed via **pptr: %d\n", **pptr); // Dereferenced twice: finds ptr first, then val
printf("\nChain reaction of addresses:\n");
printf("Address stored in ptr (address of val): %p\n", ptr);
printf("Address stored in pptr (address of ptr): %p\n", pptr);Triple Pointers
Similarly, we can define a triple pointer, which is a pointer to a double pointer. If a double pointer is an address of an address, a triple pointer is the address of an address of an address. The logic for definition and dereferencing is exactly the same. int ***ppp means that the variable ppp is used to store the memory address of an int **type variable.
Dereferencing ***ppp follows the preceding logic:
*pppgoes to the address stored inpppand finds a double pointer.**pppgoes to the double pointer's address and grabs a normal pointer.***pppgrabs the actual variable.
int number = 100;
int *ptr = &number; // First-level pointer
int **pptr = &ptr; // Second-level pointer
int ***ppptr = &pptr; // Third-level pointer
printf("ppptr stores the address of pptr: %p\n", ppptr);
printf("*ppptr gets the content stored in pptr: %p\n", *ppptr);
printf("**ppptr gets the content stored in ptr: %p\n", **ppptr);
printf("***ppptr gets the final value: %d\n", ***ppptr); // Prints 100This logic can be extended to even more levels of pointers, but in practice, you usually won’t use more than this, otherwise, the code becomes very difficult to maintain.
Test Your Understanding: Use the following questions to see if you have grasped multiple pointers (answers are at the very end).
int a = 10;
int b = 20;
int *p = &a; // p points to a
int **pp = &p; // pp points to p
int ***ppp = &pp; // ppp points to pp- What will be printed if you print
**ppp? - If you execute
*pp = &b;, what will be the result of calling*p? - Following the previous question, if you execute
***ppp = 99;, which variable's value (aorb) will be changed to99?
Practical Use Cases of Double Pointers
Double pointers might look fancy, but they aren’t just for showing off; they have highly practical applications. A common scenario for using double pointers is handling arrays of strings.
In C, there is no built-in string data type. A string in C is essentially a continuous array of characters, with a \0 appended at the end as a termination marker. Therefore, when we write char* str = "Hello";, the computer actually stores H-e-l-l-o-\0 in a block of memory, and lets the pointer str point to the address of the first letter, 'H'. As long as the program knows where it starts, it can read sequentially down the memory until it hits \0, signaling the end of the string. Thus, "a string" in the eyes of C is simply a "character pointer char *".
Understanding this, if we want to create a “list full of strings” — meaning a collection of multiple char * (e.g., ["Apple", "Banana", "Cherry"])—it is actually an "array of pointers". When this "array containing pointers" is passed as a parameter to a function, it naturally decays into a pointer to a pointer, which is a double pointer char **.
The most classic example is how the main function in C receives external arguments:
// Here, argv is a double pointer
// It points to an array, and every element in that array is a string (char *)
int main(int argc, char **argv) {
printf("You entered a total of %d arguments\n", argc);
for (int i = 0; i < argc; i++) {
// argv[i] is equivalent to *(argv + i), which fetches the i-th string pointer in the array
printf("Argument %d: %s\n", i, argv[i]);
}
return 0;
}This way of writing the main function is C's standard mechanism for receiving command-line arguments. Simply put, when we run a program in a terminal or command prompt, we don't just start it; we can also pass settings or data to it at launch. To catch this data coming from the outside, we need the argc and argv parameters.
Let’s break this down with a concrete example. Suppose we compile this code into an executable named my_program. We then enter the following command in the terminal to execute it:
./my_program Apple Banana Cherry
At this point, the operating system automatically splits the space-separated text, packages them up, and sends them into the main function:
argc(Argument Count): The first parameter of themainfunction, its goal is to count the total number of arguments. In this example, there are four items:./my_program,Apple,Banana, andCherry, so the value ofargcwill be4(the program's own name counts as well).argv: The second parameter,char argv, is the double pointer we just learned about. It's an array filled with pointers pointing to each string.
When you hit Enter, the operating system (like Linux or Windows) steps in and does the following:
- Splits the text string.
- Finds a place in memory to store the strings
./my_program,Apple,Banana,Cherry(each of them is achar *). - Creates a real array of pointers to hold the addresses of these 4 strings:
char *system_array[5] = {"./my_program", "Apple", "Banana", "Cherry", NULL};
Next, the operating system is ready to call your main function and pass this system_array in. However, in C, when you pass an array as a parameter to a function, C does not copy the entire array over (that would waste too much memory). It only passes "the memory address of the first element of this array". This phenomenon is known as Array Decay. That is to say:
system_arrayis an array.- Every element inside the array is a
char *(string pointer). - When it is passed into the
mainfunction, C only passes the address of the first element. - What is the type of the address of a
char *variable? It is a pointer pointing to achar *, which ischar **.
This is why the original “array of strings” naturally decays into a “double pointer char **argv" the moment it crosses the function boundary.
Function Pointers
Besides pointing to variables, pointers can also point to functions, known as Function Pointers. Just like variables, functions also have their memory addresses.
Get Ac Studio’s stories in your inbox
Join Medium for free to get updates from this writer.
In C, compiled functions are stored in the code segment of memory. The function name itself is actually a pointer pointing to the starting memory address of that function. A function pointer is a type of variable used to store this memory address, allowing us to dynamically decide which function to call at runtime.
In other words, since the function name itself is a memory location, how do we declare a variable to store the function’s location? When we declare a normal integer pointer, we write: int *ptr;. This tells the compiler: ptr is a pointer, and the content it points to is an int.
But for functions, just knowing it is a function is not enough. The compiler must know:
- What data type will this function return?
- What types of parameters does it need to receive?
Therefore, the standard syntax for declaring a function pointer is as follows:
return_type (*pointer_variable_name)(parameter_type1, parameter_type2...);Note that the parentheses around (*pointer_variable_name) are crucial. For example, if you write: int (*ptr)(int, int); This is a function pointer pointing to a function that returns an int. But if you write: int *ptr(int, int); This means you are declaring a normal function that receives two int parameters and returns an integer pointer int *. The meanings of the two are completely different.
Let’s look at an example to implement a function pointer. Suppose we have a simple addition function:
int add(int a, int b) {
return a + b;
}The signature of this function is “receives two ints, and returns an int". Let's declare a function pointer that matches this signature and store the address of add into it:
// 1. Declare the function pointer
int (*func_ptr)(int, int);
// 2. Assign the address of the add function to func_ptr
func_ptr = add;Once the function is stored in the pointer, we can execute the function through this pointer.
// Call the function via the function pointer, passing in parameters 3 and 5
int result = func_ptr(3, 5);
printf("The result is: %d\n", result); // Output: The result is: 8You might feel this is pointless — why not just write add(3,5) directly instead of taking a long detour using a function pointer? Let's look at a more complex example. Suppose we want to design a calculator that can do both addition and subtraction. We can dynamically let the user decide whether to use addition or subtraction at runtime.
#include <stdio.h>
// Define two functions with identical signatures
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
// Declare a function pointer
int (*operation)(int, int);
int choice = 1; // Assuming the user inputs 1 (for addition), or 2 for subtraction
int x = 10, y = 5;
// "Dynamically" change where the pointer points based on user choice
if (choice == 1) {
operation = add; // Point to addition
} else {
operation = subtract; // Point to subtraction
}
// We don't need to write if-else blocks here to call the function
// Just call operation directly, and it will execute the function that was just assigned!
int result = operation(x, y);
printf("Calculation result: %d\n", result);
return 0;
}Advanced Usage: Callback Functions
Let’s look at a more advanced use of function pointers.
Suppose we have an array of integers, and we want to print out specific numbers from it. Initially, we want to print out all the even numbers, so we write this function:
void print_evens(int arr[], int size) {
for (int i = 0; i < size; i++) {
if (arr[i] % 2 == 0) { // Check if it is an even number
printf("%d ", arr[i]);
}
}
printf("\n");
}Then we want to change it to find all numbers greater than 50. This means we have to write another condition for checking if it’s greater than 50, eventually turning our code into a messy pile. However, we can elegantly solve this problem using function pointers.
#include <stdio.h>
#include <stdbool.h>
// This is our "Strategy 1": Check if it's an even number
bool is_even(int num) {
return num % 2 == 0;
}
// This is our "Strategy 2": Check if it's greater than 50
bool is_greater_than_50(int num) {
return num > 50;
}
// This function only handles the "framework"; it doesn't know what the specific filtering rule is.
// It requires you to pass in a function pointer (Callback) that "receives an int, returns a bool".
void filter_and_print(int arr[], int size, bool (*condition)(int)) {
for (int i = 0; i < size; i++) {
// Here we "call back" the function passed in; it decides whether to print or not
if (condition(arr[i])) {
printf("%d ", arr[i]);
}
}
printf("\n");
}
int main() {
int my_array[] = {12, 55, 8, 99, 42, 3};
int size = 6;
printf("Print even numbers: ");
filter_and_print(my_array, size, is_even); // Pass in Strategy 1
printf("Print numbers greater than 50: ");
filter_and_print(my_array, size, is_greater_than_50); // Pass in Strategy 2
return 0;
}This technique is called a Callback Function (Callback).
Next, let’s look at the ultimate boss of Callbacks in C — the built-in quicksort function (qsort). This function is defined in <stdlib.h>, and its signature looks like this:
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void *));This is a sorting machine. It knows how to sort things at top speed (using the QuickSort algorithm), but it doesn’t know what kind of things you are passing in (because it uses the universal void * pointer, which could be integers, floats, or even strings), and it certainly doesn't know which one is bigger or smaller.
Therefore, it forces you to pass in a fourth parameter: int (*compar)(const void *, const void *). This is a Callback function pointer used to tell qsort the rules for comparing two elements.
#include <stdio.h>
#include <stdlib.h>
// This is the Callback function we will pass to qsort
// qsort dictates that the comparison function must return an int and receive two const void * parameters
int compare_ints(const void *a, const void *b) {
// 1. Because we know what is passed in are integers, we first cast void * back to int *
int *ptr_a = (int *)a;
int *ptr_b = (int *)b;
// 2. Extract the values inside
int val_a = *ptr_a;
int val_b = *ptr_b;
// 3. Tell qsort which is bigger
// Rule: if a < b return negative; if a == b return 0; if a > b return positive
return val_a - val_b;
}
int main() {
int data[] = {88, 56, 100, 2, 25};
int size = 5;
// Call qsort: pass in the array, the number of items, the size of each element, and our comparison rule (Callback)
qsort(data, size, sizeof(int), compare_ints);
// Print the result
printf("After sorting: ");
for (int i = 0; i < size; i++) {
printf("%d ", data[i]);
}
printf("\n"); // Output: 2 25 56 88 100
return 0;
}Many people become highly proficient at using qsort in their school programming classes, yet fail to realize that function pointers are quietly working behind the scenes.
Answers to the Questions
- The address of variable
a. If changed to***ppp, it would print10. *ppis exactly equal top. Therefore,*pp = &bis equivalent to changing the target thatppoints to fromatob.- Variable
b, because the previous sub-question already changedp's arrow to point tob.








