Dr. Roger Ianjamasimanana

How to use pointers in C programming?

By Dr. Roger Ianjamasimanana

1. What are pointers?

A pointer is a variable that stores the memory address of a variable. Instead of holding data directly, pointers hold the location where the data is stored in memory. This capability allows for efficient data manipulation, dynamic memory allocation, and the creation of complex data structures.

2. Pointers and memory allocation

Before digging into how pointers function, let's understand how computer memory works. Consider the simplified memory layout depicted below. A typical system has a series of consecutively numbered memory cells, each identified by a unique address. The contents of these cells are stored as bit patterns, representing their respective values.

Computer memory model for C programming consisting of an array of cells with different addresses

In this example, let's define a variable x and initialize it with the value 60 as represented in the memory model depicted above:

char x = 60;

Assuming that this variable is stored at the memory address number 30, we can define a pointer px that holds the address of x:

char *px = &x;

Here, the pointer px itself is a variable stored at address 10, and its value is 30, which is the address of x.

It's important to note that memory cells can be grouped to represent different data types. Typically, a single memory cell consists of 8 bits (1 byte). For example:

  • char: 1 byte
  • short int: 2 bytes
  • long int: 4 bytes

Each data type has a corresponding pointer type (e.g., double * for a double), which is aware of the size of the type it points to. This awareness allows the compiler to handle operations like array indexing correctly.

Additionally, pointers themselves occupy multiple bytes. For instance, on a 16-bit system, a pointer might use 2 bytes, whereas on a 32-bit system, it might use 4 bytes. However, it's best to avoid hardcoding pointer sizes to maintain portability across different systems.

According to the C standard, the only requirement for pointer sizes is that a void* pointer must be large enough to store the address of any other pointer type. The void* type serves as a generic pointer, facilitating advanced programming techniques discussed in later chapters.

The size of a pointer determines the range of addresses it can represent. For example:

  • A 16-bit pointer can address up to 65,535 memory locations.
  • A 32-bit pointer can address up to 4,294,967,295 memory locations.

The transition from 16-bit to 32-bit systems was primarily driven by the need for a larger addressable memory space.

3. Why use pointers?

Pointers offer several advantages in C programming:

  • Dynamic memory allocation: allocate memory at runtime using functions like malloc() and free().
  • Efficient array and string handling: manipulate arrays and strings effectively by accessing their memory addresses.
  • Function arguments: pass large data structures to functions without copying them, enhancing performance.
  • Building complex data structures: create linked lists, trees, graphs, and other dynamic data structures.

4. Pointer syntax

Understanding the syntax of pointers is essential. Here's a breakdown of how pointers are declared and used in C:

4.1 Declaring pointers

To declare a pointer, use the asterisk (*) before the pointer's name. The pointer's type should match the type of the variable it will point to.

int *ptr;      // Pointer to an integer
char *cptr;        // Pointer to a character
double *dptr;      // Pointer to a double

4.2 Initializing pointers

Pointers can be initialized to the address of a variable using the address-of operator (&).

int var = 10;
int *ptr = &var;   // ptr now holds the address of var

4.3 Dereferencing pointers

Dereferencing a pointer means accessing the value stored at the memory address the pointer holds. Use the asterisk (*) to dereference.

#include <stdio.h>

int var = 10;
int *ptr = &var;

int main(void) {
    printf("The value of var is: %d\n", *ptr);  // Outputs: 10
    return 0;
}

5. Pointer operations

Pointers can undergo various operations, including arithmetic operations and comparisons.

5.1. Pointer arithmetic

Pointer arithmetic allows you to traverse arrays and access different memory locations.

#include <stdio.h>
int arr[] = {10, 20, 30, 40, 50};
int *ptr = arr;  // Points to arr[0]
int main(void) {
    printf("The first element is: %d\n", *ptr);      // Outputs: 10
    ptr++;                                  // Now points to arr[1]
    printf("The second element is: %d\n", *ptr);     // Outputs: 20
    return 0;
}

5.2 Pointer comparisons

Pointers can be compared using relational operators to determine their positions relative to each other in memory.

#include <stdio.h>
int a = 5, b = 10;
int *ptr1 = &a;
int *ptr2 = &b;
int main(void) {
    if(ptr1 < ptr2) {
        printf("ptr1 points to a lower memory address than ptr2.\n");
    }
return 0;
}

6. Pointers and arrays

In C, arrays and pointers are closely related. The name of an array acts as a pointer to its first element.

#include <stdio.h>
int arr[] = {1, 2, 3, 4, 5};
int *ptr = arr;  // Equivalent to int *ptr = &arr[0];
int main(void) {
    printf("The first element is: %d\n", *ptr);      // Outputs: 1
    printf("The second element is: %d\n", *(ptr + 1)); // Outputs: 2
    return 0;
}

7. Pointers to pointers

C allows the creation of pointers that point to other pointers, enabling multi-level memory referencing.

#include <stdio.h>
int var = 30;
int *ptr = &var;
int **pptr = &ptr;
int main(void) {
    printf("The value of var is: %d\n", var);        // Outputs: 30
    printf("The value pointed by ptr is: %d\n", *ptr); // Outputs: 30
    printf("The value pointed by pptr is: %d\n", **pptr); // Outputs: 30
    return 0;
}

8. Dynamic memory allocation

Pointers are essential for dynamic memory allocation, allowing programs to request memory at runtime using functions like malloc() and free().

Using malloc()

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr;
    int n = 5;

    // Allocating memory for 5 integers
    ptr = (int*) malloc(n * sizeof(int));

    if(ptr == NULL) {
        printf("Memory not allocated.\n");
        return 1;
    }

    // Initializing allocated memory
    for(int i = 0; i < n; i++) {
        ptr[i] = i + 1;
    }

    // Printing values
    printf("Allocated memory values:\n");
    for(int i = 0; i < n; i++) {
        printf("%d ", ptr[i]);
    }

    // Freeing allocated memory
    free(ptr);
    return 0;
}

Output:

Allocated memory values:
1 2 3 4 5 

9. Function pointers

A function pointer is a variable that stores the address of a function. Unlike regular pointers that point to data, function pointers point to executable code blocks. This distinction is crucial for leveraging the full potential of function pointers in your programs.

Function: add Function: subtract Pointer: (*operation)(double, double)

9.1 Declaration and initialization of a function pointer

To declare a function pointer, you must specify the return type and the parameter types of the functions it can point to. Here's the general syntax:

return_type (*pointer_name)(parameter_types);

For example, a pointer to a function that takes two double arguments and returns a double can be declared as:

double (*operation)(double, double);

You can then assign it to point to any function matching its signature:

operation = &add;

Or simply:

operation = add;

Practical example: arithmetic operations

Let's consider a practical example where function pointers are used to perform different arithmetic operations based on user input.

#include <stdio.h>

// Function Definitions
double add(double a, double b) {
return a + b;
}

double subtract(double a, double b) {
return a - b;
}

double multiply(double a, double b) {
return a * b;
}

double divide(double a, double b) {
if(b != 0.0)
    return a / b;
else {
    printf("Error: Division by zero.\n");
    return 0.0;
}
}

// Function that accepts a function pointer as an argument
void execute_operation(double (*operation)(double, double), double x, double y) {
double result = operation(x, y);
printf("Result: %.2lf\n", result);
}

int main() {
double num1 = 10.0, num2 = 5.0;
double (*op)(double, double); // Function Pointer Declaration

// Assigning function pointers and executing operations
op = add;
printf("Adding %.2lf and %.2lf:\n", num1, num2);
execute_operation(op, num1, num2);

op = subtract;
printf("Subtracting %.2lf from %.2lf:\n", num2, num1);
execute_operation(op, num1, num2);

op = multiply;
printf("Multiplying %.2lf and %.2lf:\n", num1, num2);
execute_operation(op, num1, num2);

op = divide;
printf("Dividing %.2lf by %.2lf:\n", num1, num2);
execute_operation(op, num1, num2);

return 0;
}

The program above defines four arithmetic functions: add, subtract, multiply, and divide. A function pointer op is declared to point to functions matching the signature double (double, double).

The execute_operation function takes a function pointer and two double values as arguments. It calls the function pointed to by operation with the provided values and prints the result.

In the main function, the function pointer op is assigned to each arithmetic function in turn, and execute_operation is called to perform and display the operation.

Advanced usage: function pointers in arrays

Function pointers can also be stored in arrays, allowing for the creation of dispatch tables. This is particularly useful for implementing menus or handling various operations dynamically.

#include <stdio.h>

// Function Definitions
void greet() {
printf("Hello!\n");
}

void farewell() {
printf("Goodbye!\n");
}

void thank_you() {
printf("Thank you!\n");
}

int main() {
// Array of function pointers
void (*functions[])(void) = {greet, farewell, thank_you};
int choice;

printf("Choose an action:\n");
printf("1. Greet\n");
printf("2. Farewell\n");
printf("3. Thank You\n");
printf("Enter your choice: ");
fflush(stdout);
scanf("%d", &choice);

if(choice >= 1 && choice <= 3) {
    // Call the selected function
    functions[choice - 1]();
} else {
    printf("Invalid choice.\n");
}

return 0;
}

In this example, three functions greet, farewell, and thank_you are defined to print different messages. An array of function pointers, functions, is created to store the addresses of these functions.

The user is prompted to select an action by entering a choice. Based on the input, the corresponding function from the array is invoked using the selected index. This approach simplifies the process of calling different functions based on dynamic input.

9.2 Function pointers and memory management

It's crucial to ensure that the functions pointed to by function pointers remain valid throughout the program's execution. Here are some best practices and considerations:

  • Scope awareness: avoid returning pointers to local (stack-allocated) variables from functions, as they become invalid once the function exits.
  • Consistent signatures: ensure that the function pointers match the signature of the functions they point to.
  • Initialization: always initialize function pointers before using them to prevent undefined behavior.
Note: While function pointers offer great flexibility, they can also introduce complexity and potential security vulnerabilities if not managed carefully. Always validate inputs and ensure that function pointers are used safely within your applications.

10. Common mistakes with pointers

  • Dangling pointers: pointers that reference memory locations that have been freed or are out of scope.
  • Memory leaks: failing to free dynamically allocated memory leads to memory not being returned to the system.
  • Uninitialized pointers: using pointers that haven't been initialized can cause undefined behavior.
  • Incorrect pointer arithmetic: mismanaging pointer arithmetic can lead to accessing invalid memory locations.

11. Example C codes using pointers

  1. Swap two numbers: write a function that swaps two integers using pointers. Logic

    To swap two numbers using pointers, pass the addresses of the two variables to a function. Within the function, use a temporary variable to hold one value while you assign the other.

    Example C program using a pointer
    #include <stdio.h>
    
    // Function to swap two integers using pointers
    void swap(int *a, int *b) {
        int temp = *a;
        *a = *b;
        *b = temp;
    }
    
    int main() {
        int x = 10, y = 20;
    
        printf("Before swap: x = %d, y = %d\n", x, y);
        swap(&x, &y); // Passing addresses of x and y
        printf("After swap: x = %d, y = %d\n", x, y);
    
        return 0;
    }
    

    Sample output:

    Before swap: x = 10, y = 20
    After swap: x = 20, y = 10
    
  2. Reverse an array: implement a recursive function to reverse an array using pointers.

    Logic

    To reverse an array recursively, swap the first and last elements and then recursively call the function on the subarray excluding the first and last elements until the base case is reached.

    Example C program using a pointer
    #include <stdio.h>
    
    // Recursive function to reverse an array using pointers
    void reverse_array(int *start, int *end) {
        if(start >= end)
            return;
    
        // Swap the elements
        int temp = *start;
        *start = *end;
        *end = temp;
    
        // Recursive call for the next pair
        reverse_array(start + 1, end - 1);
    }
    
    int main() {
        int arr[] = {1, 2, 3, 4, 5};
        int n = sizeof(arr)/sizeof(arr[0]);
    
        printf("Original array: ");
        for(int i = 0; i < n; i++)
            printf("%d ", arr[i]);
        printf("\n");
    
        reverse_array(arr, arr + n - 1);
    
        printf("Reversed array: ");
        for(int i = 0; i < n; i++)
            printf("%d ", arr[i]);
        printf("\n");
    
        return 0;
    }
    

    Sample output:

    Original array: 1 2 3 4 5 
    Reversed array: 5 4 3 2 1 
    
  3. Find maximum in an array: create a recursive function to find the maximum element in an array using pointers.

    Logic

    To find the maximum element recursively, compare the first element with the maximum of the remaining array. The base case occurs when the array has only one element.

    Example C program using a pointer
    #include <stdio.h>
    
    // Recursive function to find maximum element in array
    int find_max(int *arr, int n) {
        // Base case: only one element
        if(n == 1)
            return arr[0];
    
        // Recursive call
        int max = find_max(arr + 1, n - 1);
    
        // Compare and return the greater value
        return (arr[0] > max) ? arr[0] : max;
    }
    
    int main() {
        int arr[] = {3, 5, 7, 2, 8, 6};
        int n = sizeof(arr)/sizeof(arr[0]);
    
        int max = find_max(arr, n);
        printf("The maximum element is %d\n", max);
    
        return 0;
    }
    

    Sample output:

    The maximum element is 8
    

12. Conclusion

Pointers offer direct memory access and enable the creation of dynamic and efficient programs. In this guide, we have looked at pointer syntax, operations, and the nuances of memory management. By practicing recursive functions, dynamic memory allocation, and pointer manipulations, you can harness the full potential of pointers to build robust and optimized applications.

feature-top
Readers’ comment
feature-top
Log in to add a comment
🔐 Access