Extended Types of Pointers II

This lesson covers the following two types of pointer and their applications:

  • Void pointer

  • Function pointer

The void pointer

In C++, a void pointer is a special pointer type that can hold the address of any type of object but doesn't provide information about the type itself. It is often used when dealing with generic or unknown types. In this part, we explore the syntax and usage of void pointers with a simple example.

A void pointer is declared using the void* type. Its generic syntax is as follows:

void* vptr = any_address.

Here’s an example that demonstrates the usage of a void pointer:

Press + to interact
#include <iostream>
using namespace std;
int main()
{
char * msg = new char[15] {'A','n', 'n', 'a'};
void * vptr = msg; // or &msg[0]
cout << "&msg[0]: " << vptr << " and string is: "<< msg <<endl
<< "or it also be printed through: "<<endl
<< "&msg[0]: " << (void*)&msg[0] << " " << &msg[0] << endl // will print "Anna"
<< "&msg[1]: " << (void*)&msg[1] << " " << &msg[1] << endl; // will print "nna"
return 0;
}
  • In this code, a character pointer msg is dynamically allocated to store an array of characters with a size of 15. The characters 'A', 'n', 'n', 'a', '\0', '\0', ... are initialized in the array. When we use cout to print an address corresponding to a character data type, C++ considers it a null-terminated string and prints the characters until it encounters the '\0' character. To print the address of a character, it is best to cast it to void*, which we've done in the above code to ensure the address is printed correctly.

Example: Writing generic swapping

Let’s witness the versatility of the generic_swap function, an application of void* that allows us to perform value swapping across different data types. From integers to doubles, characters, and even same-sized arrays, this powerful function showcases the magic of generic programming.

Press + to interact
#include <iostream>
using namespace std;
void generic_swap(void* ptr1, void* ptr2, size_t size)
{
char* charPtr1 = static_cast<char*>(ptr1);
char* charPtr2 = static_cast<char*>(ptr2);
for (size_t i = 0; i < size; i++)
{
char temp = charPtr1[i];
charPtr1[i] = charPtr2[i];
charPtr2[i] = temp;
}
}
void printArray(int A[], int size)
{
for(int i=0; i<size; i++)
cout << A[i] << " ";
}
int main()
{
int inum1 = 5;
int inum2 = 10;
cout << "Before swapping (two integers): num1 = " << inum1 << ", num2 = " << inum2 << endl;
generic_swap(&inum1, &inum2, sizeof(int));
cout << "\tAfter swapping (two integers):: num1 = " << inum1 << ", num2 = " << inum2 << endl;
double dnum1 = 5;
double dnum2 = 10;
cout << "Before swapping (two doubles): num1 = " << dnum1 << ", num2 = " << dnum2 << endl;
generic_swap(&dnum1, &dnum2, sizeof(double));
cout << "\tAfter swapping (two doubles):: num1 = " << dnum1 << ", num2 = " << dnum2 << endl;
char cnum1 = 'A';
char cnum2 = 'B';
cout << "Before swapping (two characters): num1 = " << cnum1 << ", num2 = " << cnum2 << endl;
generic_swap(&cnum1, &cnum2, sizeof(char));
cout << "\tAfter swapping (two characters):: num1 = " << cnum1 << ", num2 = " << cnum2 << endl;
int A[] = {1,2,3,4,5};
int B[] = {6,7,8,9,10};
cout << "Before swapping (two Array): A = { "; printArray(A, 5);
cout << "} B = { ";printArray(B, 5); cout << "}";
generic_swap(A, B, sizeof(A));
cout << "\n\tAfter swapping (two Array): A = { "; printArray(A, 5);
cout << "} B = { ";printArray(B, 5); cout << "}";
return 0;
}

The generic_swap() function is a powerful swapping function that allows for swapping values of different data types. It achieves this flexibility by using void pointers as input parameters.

  • Lines 3–14:The function takes two void pointers (ptr1 and ptr2) and the size of the data type (size) as input. It then casts the void pointers to char* pointers to enable byte-level swapping. The function uses a loop to iterate over each byte in the memory block of the given size. During each iteration, it swaps every ith byte of ptr1 and ptr2 by storing the value of charPtr1[i] in a temporary variable temp, assigning charPtr2[i] to charPtr1[i], and finally, assigning the temporary value to charPtr2[i]. This byte-level swapping ensures that the values stored in ptr1 and ptr2 are swapped correctly, regardless of the data type.

  • Lines 20–55:In the main() function, the generic_swap() function is demonstrated by swapping integers, doubles, characters, and arrays of integers. The function is called with appropriate arguments, such as the addresses of the variables and the size of the respective data types. The output shows the values before and after the swapping process, demonstrating that the function successfully swaps the values regardless of the data type.

The generic_swap() function provides a convenient and powerful way to swap values of different types, making it versatile and reusable across various scenarios.

Function pointers

Have you ever made a small mistake similar to the one shown below?

Press + to interact
#include <iostream>
using namespace std;
void function1(int param)
{
cout << "Hello...! I am doing nothing" << param << endl;
}
void function2(int param)
{
cout << "Hello...! I am doing nothing" << param << endl;
}
int main()
{
// your code goes here
cout << function1 << endl; // You forgot to pass the argument and parentheses ()
cout << (void*)function1 << endl; // You forgot to pass the argument and parentheses ()
cout << function2 << endl; // You forgot to pass the argument and parentheses ()
cout << (void*)function2 << endl; // You forgot to pass the argument and parentheses ()
return 0;
}

Tip: Run the code above, and you’ll see some very long addresses (if you’re running the code on Visual Studio) or a boolean value 1 (in lines 16 and 19, we have explicitly cast it to void*). What is that address doing here?

Remember: It's important to remember that every function name in a program represents the address of its corresponding location in memory where the machine code (as 0s and 1s) of that function resides.

Function pointers in C++ allow us to store and manipulate the memory addresses of functions. They provide a way to treat functions as variables, allowing us to pass functions as arguments to other functions, store them in data structures, and call them dynamically at runtime. The syntax for declaring a function pointer is as follows:

return_type (*pointer_name)(arguments);

Here, return_type represents the function’s return type, the function pointer’s name, and the arguments expected by the function.

In this section, we'll explore the application of function pointers with examples.

Example: Simple calculator

Let’s consider a simple calculator program that performs basic arithmetic operations. We can use function pointers to switch between different operations based on user input.

Here’s the implementation:

Press + to interact
#include <iostream>
using namespace std;
// Function declarations for arithmetic operations
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);
int main() {
int a, b;
char op;
int (*operation)(int, int); // Function pointer declaration
cout << "Enter two numbers: ";
cin >> a >> b;
cout << "Enter an operator (+, -, *, /): ";
cin >> op;
// Assign the appropriate function pointer based on user input
switch (op)
{
case '+':
operation = add; // operation is assigned the address of 'add()' function
break;
case '-':
operation = subtract; // operation is assigned the address of 'subtract()' function
break;
case '*':
operation = multiply; // operation is assigned the address of 'multiply()' function
break;
case '/':
operation = divide; // operation is assigned the address of 'divide()' function
break;
default:
cout << "Invalid operator!";
return 0;
}
// Perform the arithmetic operation using the function pointer
int result = operation(a, b);
cout << "Result: " << result << endl;
return 0;
}
// Function definitions for arithmetic operations
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
int multiply(int a, int b)
{
return a * b;
}
int divide(int a, int b)
{
if (b != 0)
{
return a / b;
} else
{
cout << "Cannot divide by zero!";
return 0;
}
}

Enter the input below

Note: Please enter any two numbers followed by the operator as input. For example: 2 3 +, 2 3 *, or 2 3 /.

  • The code declares four functions (add(), subtract(), multiply(), and divide()) to perform different arithmetic operations.

  • In the main() function, the user is prompted to enter two numbers and an operator.

  • A function pointer named operation is declared, which can point to any of the four arithmetic functions.

  • The appropriate function pointer is assigned using a switch statement based on the user’s input.

  • The selected arithmetic operation is performed using the function pointer, storing the result in the result variable.

  • Finally, the result is printed to the console.

Let’s look at another nice example that’ll help us solve a more interesting problem.

Example: Sorting based on comparison functions

Let’s implement a sorting function that sorts an array of integers in ascending or descending order based on a comparison.

Before knowing about pointers, what would we have done?

Implementing a sorting function for arrays of integers is a common task, but typically, we would need separate functions for sorting in ascending and descending order. As the complexity of the data increases, maintaining separate sorting functions becomes cumbersome. For instance, consider a more complex dataset like student records (we’ll talk about such records in the upcoming lessons), where each column requires a different kind of comparison. Writing separate sorting functions for each attribute would be impractical and inefficient.

However, by harnessing the power of function pointers, we can create a single generic sorting function that accepts a comparison function as a parameter. This enables us to sort the array based on the passed function without the need to know the specific comparison being performed. Function pointers allow us to dynamically bind the appropriate comparison function at runtime, resulting in a more powerful and elegant solution. Let’s see how we can do that:

Press + to interact
#include <iostream>
using namespace std;
// compare function
int compare_a (int a, int b)
{
return(a>b);
}
int compare_d (int a, int b)
{
return(a<b);
}
void sorting(int numArray[], int size, int (*compare)(int, int))
{
for(int i = 0; i<size - 1; i++)
for(int j = 0; j < size - i - 1; j++)
if(compare(numArray[j], numArray[j+1]))
swap(numArray[j], numArray[j+1]);
}
int main() {
// your code goes here
int numArray[10] = {5,2,3,2, 1, 10, 5, 8,4,7};
int (*compare[])(int, int) = {compare_a,compare_d};
// an array of function pointers,
// where [0]'s hold compare_a's address and [1]'s hold compare_d
int size = 10;
sorting(numArray, size/2, compare[0]); // sorting first half in ascending
sorting(numArray + size/2, size-size/2, compare[1]); // sorting 2nd half in descending
print(numArray, size);
return 0;
}
  • Lines 5–8: compare_a() returns true when the two passed values (a and b) violate the ascending condition (a <= b), indicating the need for swapping.

  • Lines 9–12: Similarly, compare_d() checks the descending condition.

  • Line 23: We have made compare[] an array of function pointers, where the first index has the address of compare_a and the second index has the address of compare_d.

  • Lines 27–28: The sorting function is called, and compare[0] and compare[1] are passed as arguments. This allows for dynamic decision-making within the sorting() function, where the first call on line 17 invokes compare_a(), and the second call invokes compare_d(). This flexible implementation enables the function to adapt to different comparison requirements.

Applications of function pointers

There are several applications of function pointers, including the following:

  1. Callback mechanism: Function pointers are often used in callback mechanisms, where a function can accept a function pointer as an argument. This allows the caller to specify a function that will be called back by the callee at a specific point in the program execution. This mechanism is commonly used in event-driven programming, where functions are triggered in response to specific events.

  2. Dynamic function selection: Function pointers can be used to dynamically select and invoke functions at runtime. Instead of using conditional statements or switch-case statements to determine which function to call, a function pointer can be used to directly invoke the desired function based on certain conditions or inputs. This provides flexibility and allows for dynamic behavior in the program.

  3. Function wrappers and decorators: Function pointers can be used to create function wrappers or decorators, which modify or enhance other functions’ behavior. Passing function pointers as arguments to wrapper functions makes it possible to add additional functionality to existing functions without modifying their code. This is commonly used in areas such as logging, error handling, and performance monitoring.