Chapter 12: Intermediate Topics: Advanced Pointers and Function Pointers

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 argc and argv as 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 void pointers 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 least 1 because 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 a NULL pointer (useful for iterating).

Example:

./myprogram arg1 123 "hello world"

In this case:

  • argc would be 4.
  • argv[0] would point to ./myprogram
  • argv[1] would point to arg1
  • argv[2] would point to 123
  • argv[3] would point to hello 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:

  1. Declare an array of char * (e.g., char *names[] = {"Charlie", "Alice", "Bob", "David"};).
  2. Implement a function void sort_strings(char *arr[], int count) that takes an array of string pointers and their count.
  3. Inside sort_strings, use a sorting algorithm (e.g., Bubble Sort for simplicity) to arrange the char * pointers in the array such that they point to the strings in alphabetical order. Use strcmp() to compare strings.
  4. 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:

  1. 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 in divide.
  2. Declare an array of function pointers, perhaps along with an array of corresponding operator strings (char *).
  3. Modify main to accept three command-line arguments: <number1> <operator> <number2>.
    • Example: ./calc 10 + 5
  4. Parse the arguments: convert numbers using atoi().
  5. 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.
  6. 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.