Chapter 12: Intermediate Topics: Advanced Pointers and Function Pointers
In Chapter 5, we introduced the fundamental concepts of pointers. Now, we’ll delve into more advanced aspects of pointers that are essential for handling complex data structures, dynamic memory management, and flexible program design.
This chapter will cover:
- Pointers to Pointers: When you need to modify a pointer itself from a function.
- Arrays of Pointers: Storing multiple pointers in an array.
- Pointers to Arrays: A pointer that points to an entire array.
- Pointers to Structures: Advanced usage with dynamically allocated structs.
- Function Pointers: Pointers that point to functions, enabling callback mechanisms and dynamic function calls.
- Command-Line Arguments: Understanding
argcandargvas an array of character pointers.
12.1 Pointers to Pointers (Double Pointers) Revisited
We briefly touched upon this in Chapter 5. A pointer to a pointer stores the address of another pointer. This is particularly useful when a function needs to modify a pointer variable that was passed to it from the calling function.
Recall the swap function example from Chapter 5, where we modified the values pointed to by a and b. If we wanted to change where a and b point (i.e., change the pointers themselves), we’d need a pointer to a pointer.
Syntax:
dataType **ptr_to_ptr_name;
Example: Modifying a Pointer from a Function
Imagine you have a function that dynamically allocates memory, and you want this function to update a pointer variable in the main function.
#include <stdio.h>
#include <stdlib.h> // For malloc, free
// Function that allocates memory and updates the pointer in the caller
void allocate_int(int **ptr_holder) { // ptr_holder is a pointer to an int pointer
// Dynamically allocate memory for an integer
int *temp = (int *) malloc(sizeof(int));
if (temp == NULL) {
perror("Memory allocation failed in allocate_int");
*ptr_holder = NULL; // Indicate failure by setting caller's pointer to NULL
return;
}
*temp = 123; // Assign a value to the newly allocated memory
// Update the pointer in the calling function
// *ptr_holder dereferences ptr_holder to get the original pointer variable (e.g., 'my_ptr' in main)
// and then assigns 'temp' (the address of the newly allocated memory) to it.
*ptr_holder = temp;
printf(" Inside allocate_int: Allocated memory at %p, value = %d\n", (void *)*ptr_holder, **ptr_holder);
}
int main() {
int *my_ptr = NULL; // A pointer in main, initially NULL
printf("In main: Before allocation, my_ptr = %p\n", (void *)my_ptr); // Output: (nil) or 0x0
// Pass the ADDRESS of my_ptr to the function
allocate_int(&my_ptr);
printf("In main: After allocation, my_ptr = %p\n", (void *)my_ptr); // Output: (some address)
if (my_ptr != NULL) {
printf("In main: Value via my_ptr = %d\n", *my_ptr); // Output: 123
}
// Clean up
free(my_ptr);
my_ptr = NULL;
printf("In main: After freeing, my_ptr = %p\n", (void *)my_ptr); // Output: (nil) or 0x0
return 0;
}
12.2 Arrays of Pointers
An array of pointers is an array where each element is a pointer. This is commonly used for:
- Storing an array of strings: Each element points to the first character of a string.
- Implementing polymorphic behavior: When combined with
voidpointers or pointers to base types (in C++, for example). - Arrays of dynamic arrays: Where each pointer in the array points to a dynamically allocated array.
Syntax:
dataType *arrayName[arraySize];
Example: Array of Strings (Character Pointers)
#include <stdio.h>
int main() {
// An array of pointers to char (i.e., an array of strings)
char *fruits[] = {
"Apple",
"Banana",
"Orange",
"Grape"
};
int num_fruits = sizeof(fruits) / sizeof(fruits[0]);
printf("List of Fruits:\n");
for (int i = 0; i < num_fruits; i++) {
printf("Fruit %d: %s (Address: %p)\n", i + 1, fruits[i], (void *)fruits[i]);
}
// You can modify where a pointer in the array points (but not string literals directly)
// fruits[0] = "Cherry"; // Valid: fruits[0] now points to "Cherry"
// fruits[0][0] = 'a'; // INVALID: "Apple" is a string literal, read-only!
return 0;
}
12.3 Pointers to Arrays
A pointer to an array is a single pointer variable that points to an entire array, not just its first element. This is less common but useful in specific scenarios, particularly when dealing with multi-dimensional arrays or functions that need to work with arrays of a fixed size.
Syntax:
dataType (*ptr_to_array_name)[arraySize];
The parentheses around *ptr_to_array_name are crucial due to operator precedence. Without them, dataType *ptr_to_array_name[arraySize] would declare an array of pointers.
Example:
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
// ptr_to_arr is a pointer to an array of 5 integers
int (*ptr_to_arr)[5];
ptr_to_arr = &arr; // Assign the address of the entire array
printf("Address of array arr: %p\n", (void *)&arr);
printf("Value of ptr_to_arr (address of arr): %p\n", (void *)ptr_to_arr);
// Accessing elements using pointer to array
// (*ptr_to_arr) dereferences to the array itself (arr)
// then [index] accesses the element
printf("Element at (*ptr_to_arr)[0]: %d\n", (*ptr_to_arr)[0]); // Output: 10
printf("Element at (*ptr_to_arr)[3]: %d\n", (*ptr_to_arr)[3]); // Output: 40
// Using pointer arithmetic on ptr_to_arr would advance it by sizeof(arr) bytes.
// This is distinct from an 'int *' pointer, which advances by sizeof(int) bytes.
int *p_first_element = (int *)ptr_to_arr; // Cast to pointer to first element
printf("First element via cast pointer: %d\n", *p_first_element); // Output: 10
printf("Second element via cast pointer: %d\n", *(p_first_element + 1)); // Output: 20
return 0;
}
12.4 Pointers to Structures
While we covered the -> operator for accessing structure members via a pointer in Chapter 8, let’s look at more complex scenarios involving arrays of structures and dynamic allocation.
Array of Structures vs. Array of Pointers to Structures
- Array of Structures: The entire array of structs is allocated contiguously.
struct Student { int id; char name[50]; }; struct Student class_roster[100]; // Allocates space for 100 Student structs // Access: class_roster[i].id - Array of Pointers to Structures: Only the pointers are stored contiguously. Each struct they point to can be allocated separately (e.g., dynamically). This is useful when structs have varying sizes (not typical in C, but for conceptual understanding) or when you need a flexible collection where elements can be added/removed without large memory shifts.
struct Student *class_pointers[100]; // Allocates space for 100 pointers to Student structs // Each pointer needs to be allocated: class_pointers[i] = malloc(sizeof(struct Student)); // Access: class_pointers[i]->id
Example: Dynamically Allocated Array of Structures
This is a common and practical pattern.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int id;
char name[50];
float gpa;
} Student;
void print_student(const Student *s) {
if (s != NULL) {
printf("ID: %d, Name: %s, GPA: %.2f\n", s->id, s->name, s->gpa);
} else {
printf("Null student pointer.\n");
}
}
int main() {
int num_students = 3;
// Allocate an array of 'num_students' Student structures dynamically
Student *students_arr = (Student *) malloc(num_students * sizeof(Student));
if (students_arr == NULL) {
perror("Failed to allocate student array");
return 1;
}
// Initialize students using array-style access on the pointer
students_arr[0].id = 1;
strcpy(students_arr[0].name, "Alice");
students_arr[0].gpa = 3.9f;
students_arr[1].id = 2;
strcpy(students_arr[1].name, "Bob");
students_arr[1].gpa = 3.5f;
// Or using pointer arithmetic and -> operator
(students_arr + 2)->id = 3;
strcpy((students_arr + 2)->name, "Charlie");
(students_arr + 2)->gpa = 3.2f;
printf("--- Dynamically Allocated Students ---\n");
for (int i = 0; i < num_students; i++) {
print_student(&students_arr[i]); // Pass address of each student
}
free(students_arr);
students_arr = NULL;
return 0;
}
12.5 Function Pointers
A function pointer is a variable that stores the memory address of a function. This allows you to:
- Pass functions as arguments to other functions (callback functions).
- Store functions in arrays or data structures.
- Call different functions dynamically based on runtime conditions.
12.5.1 Declaring Function Pointers
The declaration of a function pointer resembles a function prototype, but with an asterisk and parentheses around the pointer name.
Syntax:
return_type (*pointer_name)(parameter_type1, parameter_type2, ...);
Example:
int (*add_func_ptr)(int, int); // A pointer to a function that takes two ints and returns an int
void (*greet_func_ptr)(char *); // A pointer to a function that takes a char* and returns void
12.5.2 Assigning and Calling Function Pointers
To assign a function’s address to a function pointer, simply use the function’s name (without parentheses). The & operator is optional but can be used (e.g., &add).
To call a function via its pointer, you can either dereference it explicitly (*add_func_ptr)(a, b) or simply use the pointer name add_func_ptr(a, b). Both are typically valid.
Example:
#include <stdio.h>
#include <string.h> // For strcmp
// Regular functions
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
void say_hello(char *name) { printf("Hello, %s!\n", name); }
// Function that takes a function pointer as an argument (a callback)
void perform_operation(int num1, int num2, int (*operation)(int, int)) {
int result = operation(num1, num2);
printf("Operation result: %d\n", result);
}
int main() {
// 1. Declare and assign function pointers
int (*math_op_ptr)(int, int); // Declare
void (*greeting_ptr)(char *);
math_op_ptr = add; // Assign the 'add' function's address (can also use &add)
greeting_ptr = say_hello;
// 2. Call functions using the pointers
int sum = math_op_ptr(10, 5); // Call using pointer
printf("Sum (via func ptr): %d\n", sum); // Output: 15
math_op_ptr = subtract; // Reassign to 'subtract' function
int diff = (*math_op_ptr)(10, 5); // Another way to call
printf("Difference (via func ptr): %d\n", diff); // Output: 5
greeting_ptr("World"); // Call say_hello via greeting_ptr
// 3. Using a function pointer in a callback context
printf("\n--- Callback Example ---\n");
perform_operation(50, 20, add); // Pass 'add' function
perform_operation(50, 20, subtract); // Pass 'subtract' function
// 4. Array of function pointers (e.g., for a menu system)
printf("\n--- Array of Function Pointers ---\n");
int (*operations[2])(int, int); // Array of 2 function pointers
operations[0] = add;
operations[1] = subtract;
int num1 = 100, num2 = 30;
printf("Operation 0 (add): %d\n", operations[0](num1, num2)); // 130
printf("Operation 1 (subtract): %d\n", operations[1](num1, num2)); // 70
return 0;
}
12.6 Command-Line Arguments: argc and argv
The main function can receive arguments from the command line when your program is executed. This is how you pass configuration options or file paths to your programs.
Syntax of main with arguments:
int main(int argc, char *argv[]) {
// ...
}
argc(argument count): An integer that indicates the number of command-line arguments. It’s always at least1because the program’s name itself is considered the first argument.argv(argument vector): An array of character pointers (char *[]). Each pointer in this array points to a string (C-style string) representing one command-line argument.argv[0]is typically the name of the executable program.argv[1]is the first actual argument provided by the user.argv[argc - 1]is the last argument.argv[argc]is aNULLpointer (useful for iterating).
Example:
./myprogram arg1 123 "hello world"
In this case:
argcwould be4.argv[0]would point to./myprogramargv[1]would point toarg1argv[2]would point to123argv[3]would point tohello world
Code Example: Command-Line Arguments
#include <stdio.h>
#include <stdlib.h> // For atoi
int main(int argc, char *argv[]) {
printf("Number of command-line arguments: %d\n", argc);
printf("Arguments received:\n");
for (int i = 0; i < argc; i++) {
printf(" argv[%d]: \"%s\"\n", i, argv[i]);
}
// Example: process numeric arguments
if (argc > 2) {
printf("\n--- Processing Numeric Arguments ---\n");
int num1 = atoi(argv[1]); // Convert string to integer
int num2 = atoi(argv[2]); // Convert string to integer
printf("First argument as integer: %d\n", num1);
printf("Second argument as integer: %d\n", num2);
printf("Sum of first two arguments: %d\n", num1 + num2);
} else {
printf("\nUsage: %s <number1> <number2> ...\n", argv[0]);
}
return 0;
}
To Compile and Run:
gcc command_line_args.c -o command_line_args
./command_line_args
./command_line_args 10 20
./command_line_args hello 123 "another argument"
Note on atoi(): atoi() (ASCII to integer) from <stdlib.h> converts a string to an int. It returns 0 if the string isn’t a valid number. For more robust string-to-number conversions, consider strtol() or strtod().
Exercise 12.1: Sorting Pointers to Strings
Write a C program that takes a list of names (strings) as input and sorts them alphabetically. Instead of sorting the actual strings, sort an array of pointers to these strings.
Instructions:
- Declare an array of
char *(e.g.,char *names[] = {"Charlie", "Alice", "Bob", "David"};). - Implement a function
void sort_strings(char *arr[], int count)that takes an array of string pointers and their count. - Inside
sort_strings, use a sorting algorithm (e.g., Bubble Sort for simplicity) to arrange thechar *pointers in the array such that they point to the strings in alphabetical order. Usestrcmp()to compare strings. - Print the names before and after sorting.
Example output:
Original Names:
Charlie
Alice
Bob
David
Sorted Names:
Alice
Bob
Charlie
David
Exercise 12.2: Dynamic Calculator with Function Pointers (Mini-Challenge)
Create a command-line calculator that uses function pointers to perform operations.
Instructions:
- Define four simple functions:
int add(int a, int b),int subtract(int a, int b),int multiply(int a, int b),int divide(int a, int b). Handle division by zero individe. - Declare an array of function pointers, perhaps along with an array of corresponding operator strings (
char *). - Modify
mainto accept three command-line arguments:<number1> <operator> <number2>.- Example:
./calc 10 + 5
- Example:
- Parse the arguments: convert numbers using
atoi(). - Use a loop to iterate through your array of operator strings and function pointers. If the input operator matches one in your array, call the corresponding function pointer with the parsed numbers.
- Print the result. If the operator is not recognized, print an error.
Example usage and output:
$ ./calculator 10 + 5
Result: 15
$ ./calculator 20 - 7
Result: 13
$ ./calculator 4 * 6
Result: 24
$ ./calculator 10 / 0
Error: Division by zero!
$ ./calculator 10 ^ 3
Error: Invalid operator!
You’ve now ventured into the more advanced realms of C pointers, including their intricate relationship with arrays and structures, and the powerful concept of function pointers for highly flexible program design. You’ve also seen how to interact with command-line arguments. These intermediate topics are critical for writing robust, efficient, and dynamic C applications. In the next chapter, we’ll continue our exploration of intermediate C topics by looking at command-line arguments and environment variables.