Understanding and Using C Pointers (2013)
Chapter 3. Pointers and Functions
Pointers contribute immensely to a function’s capability. They allow data to be passed and modified by a function. Complex data can also be passed and returned from a function in the form of a pointer to a structure. When pointers hold the address of a function, they provide a means to dynamically control a program’s execution flow. In this chapter, we will explore the power of pointers as used with functions and learn how to use them to solve many real-world problems.
To understand functions and their use with pointers, a good understanding of the program stack is needed. The program stack is used by most modern block-structured languages, such as C, to support the execution of functions. When a function is invoked, its stack frame is created and then pushed onto the program stack. When the function returns, its stack frame is popped off of the program stack.
When working with functions, there are two areas where pointers become useful. The first is when we pass a pointer to a function. This allows the function to modify data referenced by the pointer and to pass blocks of information more efficiently.
The second area is declaring a pointer to a function. In essence, function notation is pointer notation. The function’s name evaluates to the address of the function, and the function’s parameters are passed to the function. As we will see, function pointers provide additional capability to control the execution flow of a program.
In this section, we will establish the foundation for understanding and working with functions and pointers. Because of the pervasiveness of functions and pointers, this foundation should serve you well.
Program Stack and Heap
The program stack and the heap are important runtime elements of C. In this section, we will carefully examine the structure and use of the program stack and heap. We will also look at the stack frame’s structure, which holds local variables.
NOTE
Local variables are also called automatic variables. They are always allocated to a stack frame.
Program Stack
The program stack is an area of memory that supports the execution of functions and is normally shared with the heap. That is, they share the same region of memory. The program stack tends to occupy the lower part of this region, while the heap uses the upper part.
The program stack holds stack frames, sometimes called activation records or activation frames. Stack frames hold the parameters and local variables of a function. The heap manages dynamic memory and is discussed in Dynamic Memory Allocation.
Figure 3-1 illustrates how the stack and heap are organized conceptually. This illustration is based on the following code sequence:
void function2() {
Object *var1 = ...;
int var2;
printf("Program Stack Example\n");
}
void function1() {
Object *var3 = ...;
function2();
}
int main() {
int var4;
function1();
}
As functions are called, their stack frames are pushed onto the stack and the stack grows “upward.” When a function terminates, its stack frame is popped off the program stack. The memory used by the stack frame is not cleared and may eventually be overridden by another stack frame when it is pushed onto the program stack.
Figure 3-1. Stack and heap
When memory is dynamically allocated, it comes from the heap, which tends to grow “downward.” The heap will fragment as memory is allocated and then deallocated. Although the heap tends to grow downward, this is a general direction. Memory can be allocated from anywhere within the heap.
Organization of a Stack Frame
A stack frame consists of several elements, including:
Return address
The address in the program where the function is to return upon completion
Storage for local data
Memory allocated for local variables
Storage for parameters
Memory allocated for the function’s parameters
Stack and base pointers
Pointers used by the runtime system to manage the stack
The typical C programmer will not be concerned about the stack and base pointers used in support of a stack frame. However, understanding what they are and how they are used provides a more in-depth understanding of the program stack.
A stack pointer usually points to the top of the stack. A stack base pointer (frame pointer) is often present and points to an address within the stack frame, such as the return address. This pointer assists in accessing the stack frame’s elements. Neither of these pointers are C pointers. They are addresses used by the runtime system to manage the program stack. If the runtime system is implemented in C, then these pointers may be real C pointers.
Consider the creation of a stack frame for the following function. This function has passed an array of integers and an integer representing the array’s size. Three printf statements are used to display the parameter’s and the local variable’s addresses:
float average(int *arr, int size) {
int sum;
printf("arr: %p\n",&arr);
printf("size: %p\n",&size);
printf("sum: %p\n",&sum);
for(int i=0; i<size; i++) {
sum += arr[i];
}
return (sum * 1.0f) / size;
}
When executed, you get output similar to the following:
arr: 0x500
size: 0x504
sum: 0x480
The gap in the addresses between the parameters and the local variables is due to other elements of the stack frame used by the runtime system to manage the stack.
When the stack frame is created, the parameters are pushed onto the frame in the opposite order of their declaration, followed by the local variables. This is illustrated in Figure 3-2. In this case, size is pushed followed by arr. Typically, the return address for the function call is pushed next, followed by the local variables. They are pushed in the opposite order in which they were listed.
Conceptually, the stack in this example grows “up.” However, the stack frame’s parameters and local variables and new stack frames are added at lower memory addresses. The actual direction the stack grows is implementation-specific.
Figure 3-2. Stack frame example
The variable i used in the for statement is not included as part of this stack frame. C treats block statements as “mini” functions and will push and pop them as appropriate. In this case, the block statement is pushed onto the program stack above the average stack frame when it is executed and then popped off when it is done.
While the precise addresses can vary, the order usually will not. This is important to understand, as it helps explain how memory is allocated and establishes the relative order of the parameters and variables. This can be useful when debugging pointer problems. If you are not aware of how the stack frame is allocated, the assignment of addresses may not make sense.
As stack frames are pushed onto the program stack, the system may run out of memory. This condition is called stack overflow and generally results in the program terminating abnormally. Keep in mind that each thread is typically allocated its own program stack. This can lead to potential conflicts if one or more threads access the same object in memory. This will be addressed in Sharing Pointers Between Threads.
Passing and Returning by Pointer
In this section, we will examine the impact of passing and returning pointers to and from functions. Passing pointers allows the referenced object to be accessible in multiple functions without making the object global. This means that only those functions that need access to the object will get this access and that the object does not need to be duplicated.
If the data needs to be modified in a function, it needs to be passed by pointer. We can pass data by pointer and prohibit it from being modified by passing it as a pointer to a constant, as will be demonstrated in the section Passing a Pointer to a Constant. When the data is a pointer that needs to be modified, then we pass it as a pointer to a pointer. This topic is covered in Passing a Pointer to a Pointer.
Parameters, including pointers, are passed by value. That is, a copy of the argument is passed to the function. Passing a pointer to an argument can be efficient when dealing with large data structures. For example, consider a large structure that represents an employee. If we passed the entire structure to the function, then every byte of the structure would need to be copied, resulting in a slower program and in more space being used in the stack frame. Passing a pointer to the object means the object does have to be copied, and we can access the object through the pointer.
Passing Data Using a Pointer
One of the primary reasons for passing data using a pointer is to allow the function to modify the data. The following sequence implements a swap function that will interchange the values referenced by its parameters. This is a common operation found in a number of sorting algorithms. Here, we use integer pointers and dereference them to affect the swap operation:
void swapWithPointers(int* pnum1, int* pnum2) {
int tmp;
tmp = *pnum1;
*pnum1 = *pnum2;
*pnum2 = tmp;
}
The following code sequence demonstrates this function:
int main() {
int n1 = 5;
int n2 = 10;
swapWithPointers(&n1, &n2);
return 0;
}
The pointers pnum1 and pnum2 are dereferenced during the swap operation. This will result in the values of n1 and n2 being modified. Figure 3-3 illustrates how memory is organized. The Before image shows the program stack at the beginning of the swap function, and the After image shows it just before the function returns.
Figure 3-3. Swapping with pointers
Passing Data by Value
If we do not pass them by pointers, then the swap operation will not occur. In the following function, the two integers are passed by value:
void swap(int num1, int num2) {
int tmp;
tmp = num1;
num1 = num2;
num2 = tmp;
}
In the following code sequence, two integers are passed to the function:
int main() {
int n1 = 5;
int n2 = 10;
swap(n1, n2);
return 0;
}
However, this will not work because the integers were passed by value and not by pointer. Only a copy of the arguments is stored in num1 and num2. If we modify num1, then the argument n1 is not changed. When we modify the parameters, we are not modifying the original arguments.Figure 3-4 illustrates how memory is allocated for the parameters.
Figure 3-4. Pass by value
Passing a Pointer to a Constant
Passing a pointer to constant is a common technique used in C. It is efficient, as we are only passing the address of the data and can avoid copying large amounts of memory in some cases. However, with a simple pointer, the data can be modified. When this is not desirable, then passing a pointer to a constant is the answer.
In this example, we pass a pointer to a constant integer and a pointer to an integer. Within the function, we cannot modify the value passed as a pointer to a constant:
void passingAddressOfConstants(const int* num1, int* num2) {
*num2 = *num1;
}
int main() {
const int limit = 100;
int result = 5;
passingAddressOfConstants(&limit, &result);
return 0;
}
No syntax errors will be generated, and the function will assign 100 to the variable result. In the following version of the function, we attempt to modify both referenced integers:
void passingAddressOfConstants(const int* num1, int* num2) {
*num1 = 100;
*num2 = 200;
}
This will cause a problem if we pass the constant limit to the function twice:
const int limit = 100;
passingAddressOfConstants(&limit, &limit);
This will generate syntax errors that complain of a type mismatch between the second parameter and its argument. In addition, it will complain that we are attempting to modify the presumed constant referenced by the first parameter.
The function expected a pointer to an integer, but a pointer to an integer constant was passed instead. We cannot pass the address of an integer constant to a pointer to a constant, as this would allow a constant value to be modified. This is detailed in the section Constants and Pointers.
An attempt to pass the address of an integer literal as shown below will also generate a syntax error:
passingAddressOfConstants(&23, &23);
In this case, the error message will indicate that an lvalue is required as the address-of operator’s operand. The concept of an lvalue is discussed in Dereferencing a Pointer Using the Indirection Operator.
Returning a Pointer
Returning a pointer is easy to do. We simply declare the return type to be a pointer to the appropriate data type. If we need to return an object from a function, the following two techniques are frequently used:
§ Allocate memory within the function using malloc and return its address. The caller is responsible for deallocating the memory returned.
§ Pass an object to the function where it is modified. This makes the allocation and deallocation of the object’s memory the caller’s responsibility.
First, we will illustrate the use of malloc type functions to allocate the memory returned. This is followed by an example where we return a pointer to a local object. This latter approach is not recommended. The approach identified in the second bullet is then illustrated in the sectionPassing Null Pointers.
In the following example, we define a function that is passed the size of an integer array and a value to initialize each element. The function allocates memory for an integer array, initializes the array to the value passed, and then returns the array’s address:
int* allocateArray(int size, int value) {
int* arr = (int*)malloc(size * sizeof(int));
for(int i=0; i<size; i++) {
arr[i] = value;
}
return arr;
}
The following illustrates how this function can be used:
int* vector = allocateArray(5,45);
for(int i=0; i<5; i++) {
printf("%d\n", vector[i]);
}
Figure 3-5 illustrates how memory is allocated for this function. The Before image shows the program’s state right before the return statement is executed. The After image shows the program’s state after the function has returned. The variable vector now contains the address of the memory allocated in the function. While the arr variable went away when the function terminated, the memory referenced by the pointer does not go away. This memory will eventually need to be freed.
Although the previous example works correctly, several potential problems can occur when returning a pointer from a function, including:
§ Returning an uninitialized pointer
§ Returning a pointer to an invalid address
§ Returning a pointer to a local variable
§ Returning a pointer but failing to free it
The last problem is typified by the allocateArray function. Returning dynamically allocated memory from the function means the function’s caller is responsible for deallocating it. Consider the following example:
int* vector = allocateArray(5,45);
...
free(vector);
Figure 3-5. Returning a pointer
We must eventually free it once we are through using it. If we don’t, then we will have a memory leak.
Pointers to Local Data
Returning a pointer to local data is an easy mistake to make if you don’t understand how the program stack works. In the following example, we rework the allocateArray function used in the section Returning a Pointer. Instead of dynamically allocating memory for the array, we used a local array:
int* allocateArray(int size, int value) {
int arr[size];
for(int i=0; i<size; i++) {
arr[i] = value;
}
return arr;
}
Unfortunately, the address of the array returned is no longer valid once the function returns because the function’s stack frame is popped off the stack. While each array element may still contain a 45, these values may be overwritten if another function is called. This is illustrated with the following sequence. Here, the printf function is invoked repeatedly, resulting in corruption of the array:
int* vector = allocateArray(5,45);
for(int i=0; i<5; i++) {
printf("%d\n", vector[i]);
}
Figure 3-6 illustrates how memory is allocated when this happens. The dashed box shows where other stack frames, such as those used by the printf function, may be pushed onto the program stack, thus corrupting the memory held by the array. The actual contents of that stack frame are implementation-dependent.
Figure 3-6. Returning a pointer to local data
An alternative approach is to declare the arr variable as static. This will restrict the variable’s scope to the function but allocate it outside of the stack frame, eliminating the possibility of another function overwriting the variable’s value:
int* allocateArray(int size, int value) {
static int arr[5];
...
}
However, this will not always work. Every time the allocateArray function is called, it will reuse the array. This effectively invalidates any previous calls to the function. In addition, the static array must be declared with a fixed size. This will limit the function’s ability to handle various array sizes.
If the function returns only a few possible values and it does not hurt to share them, then it can maintain a list of these values and return the appropriate one. This can be useful if we are returning a status type message, such as an error number that is not likely to be modified. In the sectionReturning Strings, an example of using global and static values is demonstrated.
Passing Null Pointers
In the following version of the allocateArray function, a pointer to an array is passed along with its size and a value that it will use to initialize each element of the array. The pointer is returned for convenience. Although this version of the function does not allocate memory, later versions will allocate memory:
int* allocateArray(int *arr, int size, int value) {
if(arr != NULL) {
for(int i=0; i<size; i++) {
arr[i] = value;
}
}
return arr;
}
When a pointer is passed to a function, it is always good practice to verify it is not null before using it.
The function can be invoked as follows:
int* vector = (int*)malloc(5 * sizeof(int));
allocateArray(vector,5,45);
If the pointer is NULL, then no action is performed and the program will execute without terminating abnormally.
Passing a Pointer to a Pointer
When a pointer is passed to a function, it is passed by value. If we want to modify the original pointer and not the copy of the pointer, we need to pass it as a pointer to a pointer. In the following example, a pointer to an integer array is passed, which will be assigned memory and initialized. The function will return the allocated memory back through the first parameter. In the function, we first allocate memory and then initialize it. The address of this allocated memory is intended to be assigned to a pointer to an int. To modify this pointer in the calling function, we need to pass the pointer’s address. Thus, the parameter is declared as a pointer to a pointer to an int. In the calling function, we need to pass the address of the pointer:
void allocateArray(int **arr, int size, int value) {
*arr = (int*)malloc(size * sizeof(int));
if(*arr != NULL) {
for(int i=0; i<size; i++) {
*(*arr+i) = value;
}
}
}
The function can be tested using the following code:
int *vector = NULL;
allocateArray(&vector,5,45);
The first parameter to allocateArray is passed as a pointer to a pointer to an integer. When we call the function, we need to pass a value of this type. This is done by passing the address of vector. The address returned by malloc is assigned to arr. Dereferencing a pointer to a pointer to an integer results in a pointer to an integer. Because this is the address of vector, we modify vector.
The memory allocation is illustrated in Figure 3-7. The Before image shows the stack’s state after malloc returns and the array is initialized. Likewise, the After image shows the stack’s state after the function returns.
NOTE
To easily identify problems such as memory leaks, draw a diagram of memory allocation.
Figure 3-7. Passing a pointer to a pointer
The following version of the function illustrates why passing a simple pointer will not work:
void allocateArray(int *arr, int size, int value) {
arr = (int*)malloc(size * sizeof(int));
if(arr != NULL) {
for(int i=0; i<size; i++) {
arr[i] = value;
}
}
}
The following sequence illustrates using the function:
int *vector = NULL;
allocateArray(&vector,5,45);
printf("%p\n",vector);
When the program is executed you will see 0x0 displayed because when vector is passed to the function, its value is copied into the parameter arr. Modifying arr has no effect on vector. When the function returns, the value stored in arr is not copied to vector. Figure 3-8 illustrates the allocation of memory. The Before malloc image shows the state of memory just before arr is assigned a new value. It contains the value of 500, which was passed to it from vector. The After malloc image shows the state of memory after the malloc function was executed in the allocateArray function and the array was initialized. The variable arr has been modified to point to a new place in the heap. The After return image shows the program stack’s state after the function returns. In addition, we have a memory leak because we have lost access to the block of memory at address 600.
Figure 3-8. Passing pointers
Writing your own free function
Several issues surround the free function that encourage some programmers to create their own version of this function. The free function does not check the pointer passed to see whether it is NULL and does not set the pointer to NULL before it returns. Setting a pointer to NULL after freeing is a good practice.
Given the foundation provided in the section Passing and Returning by Pointer, the following illustrates one way of implementing your own free function that assigns a NULL value to the pointer. It requires that we use a pointer to a pointer:
void saferFree(void **pp) {
if (pp != NULL && *pp != NULL) {
free(*pp);
*pp = NULL;
}
}
The saferFree function calls the free function that actually deallocates the memory. Its parameter is declared as a pointer to a pointer to void. Using a pointer to a pointer allows us to modify the pointer passed. Using the void type allows all types of pointers to be passed. However, we get a warning if we do not explicitly cast the pointer type to void when we call the function. If we explicitly perform the cast, then the warning goes away.
The safeFree macro, shown below, calls the saferFree function with this cast and uses the address-of operator, thus alleviating the need for a function’s user to perform the cast and to pass the pointer’s address.
#define safeFree(p) saferFree((void**)&(p))
The next sequence illustrates the use of this macro:
int main() {
int *pi;
pi = (int*) malloc(sizeof(int));
*pi = 5;
printf("Before: %p\n",pi);
safeFree(pi);
printf("After: %p\n",pi);
safeFree(pi);
return (EXIT_SUCCESS);
}
Assuming malloc returned memory from address 1000, the output of this sequence will be 1000 and then 0. The second use of the safeFree macro with a NULL value does not terminate the application, as the function detects and ignores it.
Function Pointers
A function pointer is a pointer that holds the address of a function. The ability of pointers to point to functions turns out to be an important and useful feature of C. This provides us with another way of executing functions in an order that may not be known at compile time and without using conditional statements.
One concern regarding the use of function pointers is a potentially slower running program. The processor may not be able to use branch prediction in conjunction with pipelining. Branch prediction is a technique whereby the processor will guess which multiple execution sequences will be executed. Pipelining is a hardware technology commonly used to improve processor performance and is achieved by overlapping instruction execution. In this scheme, the processor will start processing the branch it believes will be executed. If the processor successfully predicts the correct branch, then the instructions currently in the pipeline will not have to be discarded.
This slowdown may or may not be realized. The use of function pointers in situations such as table lookups can mitigate performance issues. In this section, we will learn how to declare function pointers, see how they can be used to support alternate execution paths, and explore techniques that exploit their potential.
Declaring Function Pointers
The syntax for declaring a pointer to a function can be confusing when you first see it. As with many aspects of C, once you get used to the notation, things start falling into place. Let’s start with a simple declaration. Below, we declare a pointer to a function that is passed void and returns void:
void (*foo)();
This declaration looks a lot like a function prototype. If we removed the first set of parentheses, it would appear to be a function prototype for the function foo, which is passed void and returns a pointer to void. However, the parentheses make it a function pointer with a name of foo. The asterisk indicates that it is a pointer. Figure 3-9 highlights the parts of a function pointer declaration.
Figure 3-9. Function pointer declaration
NOTE
When function pointers are used, the programmer must be careful to ensure it is used properly because C does not check to see whether the correct parameters are passed.
Other examples of function pointer declarations are illustrated below:
int (*f1)(double); // Passed a double and
// returns an int
void (*f2)(char*); // Passed a pointer to char and
// returns void
double* (*f3)(int, int); // Passed two integers and
// returns a pointer to a double
NOTE
One suggested naming convention for function pointers is to always begin their name with the prefix: fptr.
Do not confuse functions that return a pointer with function pointers. The following declares f4 as a function that returns a pointer to an integer, while f5 is a function pointer that returns an integer. The variable f6 is a function pointer that returns a pointer to an integer:
int *f4();
int (*f5)();
int* (*f6)();
The whitespace within these expressions can be rearranged so that it reads as follows:
int* f4();
int (*f5)();
It is clear that f4 is a function that returns a pointer to an integer. However, using parentheses with f5 clearly bind the “pointer” asterisk to the function name, making it a function pointer.
Using a Function Pointer
Below is a simple example using a function pointer where a function is passed an integer and returns an integer. We also define a square function that squares an integer and then returns the square. To simplify these examples, we ignore the possibility of integer overflow.
int (*fptr1)(int);
int square(int num) {
return num*num;
}
To use the function pointer to execute the square function, we need to assign the square function’s address to the function pointer, as shown below. As with array names, when we use the name of a function by itself, it returns the function’s address. We also declare an integer that we will pass to the function:
int n = 5;
fptr1 = square;
printf("%d squared is %d\n",n, fptr1(n));
When executed it will display: “5 squared is 25.” We could have used the address-of operator with the function name as follows, but it is not necessary and is redundant. The compiler will effectively ignore the address-of operator when used in this context.
fptr1 = □
Figure 3-10 illustrates how memory is allocated for this example. We have placed the square function below the program stack. This is for illustrative purposes only. Functions are allocated in a different segment than that used by the program stack. The function’s actual location is normally not of interest.
Figure 3-10. Location of functions
It is convenient to declare a type definition for function pointers. This is illustrated below for the previous function pointer. The type definition looks a little bit strange. Normally, the type definition’s name is the declaration’s last element:
typedef int (*funcptr)(int);
...
funcptr fptr2;
fptr2 = square;
printf("%d squared is %d\n",n, fptr2(n));
Function Pointers and Strings provides an interesting example with respect to using a function pointer to control how an array of strings is sorted.
Passing Function Pointers
Passing a function pointer is easy enough to do. Simply use a function pointer declaration as a parameter of a function. We will demonstrate passing a function pointer using add, sub, and compute functions as declared below:
int add(int num1, int num2) {
return num1 + num2;
}
int subtract(int num1, int num2) {
return num1 - num2;
}
typedef int (*fptrOperation)(int,int);
int compute(fptrOperation operation, int num1, int num2) {
return operation(num1, num2);
}
The following sequence demonstrates these functions:
printf("%d\n",compute(add,5,6));
printf("%d\n",compute(sub,5,6));
The output will be an 11 and a –1. The add and sub function’s addresses were passed to the compute function. These addresses were then used to invoke the corresponding operation. This example also shows how code can be made more flexible through the use of function pointers.
Returning Function Pointers
Returning a function pointer requires declaring the function’s return type as a function pointer. To demonstrate how this is done, we will reuse the add and sub function along with the type definition we developed in the section Passing Function Pointers.
We will use the following select function to return a function pointer to an operation based in a character input. It will return a pointer to either the add function or the subtract function, depending on the opcode passed:
fptrOperation select(char opcode) {
switch(opcode) {
case '+': return add;
case '-': return subtract;
}
}
The evaluate function ties these functions together. The function is passed two integers and a character representing the operation to be performed. It passes the opcode to the select function, which returns a pointer to the function to execute. In the return statement, it executes this function and returns the result:
int evaluate(char opcode, int num1, int num2) {
fptrOperation operation = select(opcode);
return operation(num1, num2);
}
This function is demonstrated with the following printf statements:
printf("%d\n",evaluate('+', 5, 6));
printf("%d\n",evaluate('-', 5, 6));
The output will be an 11 and a –1.
Using an Array of Function Pointers
Arrays of function pointers can be used to select the function to evaluate on the basis of some criteria. Declaring such an array is straightforward. We simply use the function pointer declaration as the array’s type, as shown below. The array is also initialized to all NULLs. When a block of initialization values are used with an array, its values will be assigned to consecutive elements of the array. If the number of values is less than the size of the array, as in this example, the value is used to initialize every element of the array:
typedef int (*operation)(int, int);
operation operations[128] = {NULL};
Alternatively, we can declare this array without using a typedef as shown below:
int (*operations[128])(int, int) = {NULL};
The intent of this array is to allow a character index to select a corresponding function to execute. For example, the '*' character will identify the multiplication function if it exists. We can use character indexes because a character literal is an integer. The 128 elements corresponds to the first 128 ASCII characters. We will use this definition in conjunction with the add and subtract functions developed in the section Returning Function Pointers.
Having initialized the array to all NULLs, we then assign the add and subtract functions to the elements corresponding to the plus and minus signs:
void initializeOperationsArray() {
operations['+'] = add;
operations['-'] = subtract;
}
The previous evaluate function is rewritten as evaluateArray. Instead of calling the select function to obtain a function pointer, we used the operations with the operation character as an index:
int evaluateArray(char opcode, int num1, int num2) {
fptrOperation operation;
operation = operations[opcode];
return operation(num1, num2);
}
Test the functions using the following sequence:
initializeOperationsArray();
printf("%d\n",evaluateArray('+', 5, 6));
printf("%d\n",evaluateArray('-', 5, 6));
The results of executing this sequence are 11 and –1. A more robust version of the evaluateArray function would check for null function pointers before trying to execute the function.
Comparing Function Pointers
Function pointers can be compared to one another using the equality and inequality operators. In the following example, we use the fptrOperator type definition and the add function from the section Passing Function Pointers. The add function is assigned to the fptr1 function pointer and then compared against the add function’s address:
fptrOperation fptr1 = add;
if(fptr1 == add) {
printf("fptr1 points to add function\n");
} else {
printf("fptr1 does not point to add function\n");
}
When this is executed, the output will verify that the pointer does point to the add function.
A more realistic example of where the comparison of function pointers would be useful involves an array of function pointers that represent the steps of a task. For example, we may have a series of functions that manipulate an array of inventory parts. One set of operations may be to sort the parts, calculate a cumulative sum of their quantities, and then display the array and sum. A second set of operations may be to display the array, find the most expensive and the least expensive, and then display their difference. Each operation could be defined by an array of pointers to the individual functions. A log operation may be present in both lists. The ability to compare two function pointers would permit the dynamic modification of an operation by deleting the operation, such as logging, by finding and then removing the function from the list.
Casting Function Pointers
A pointer to one function can be cast to another type. This should be done with care since the runtime system does not verify that parameters used by a function pointer are correct. It is also possible to cast a function pointer to a different function pointer and then back. The resulting pointer will be equal to the original pointer. The size of function pointers used are not necessarily the same. The following sequence illustrates this operation:
typedef int (*fptrToSingleInt)(int);
typedef int (*fptrToTwoInts)(int,int);
int add(int, int);
fptrToTwoInts fptrFirst = add;
fptrToSingleInt fptrSecond = (fptrToSingleInt)fptrFirst;
fptrFirst = (fptrToTwoInts)fptrSecond;
printf("%d\n",fptrFirst(5,6));
This sequence, when executed, will display 11 as its output.
WARNING
Conversion between function pointers and pointers to data is not guaranteed to work.
The use of void* is not guaranteed to work with function pointers. That is, we should not assign a function pointer to void* as shown below:
void* pv = add;
However, when interchanging function pointers, it is common to see a “base” function pointer type as declared below. This declares fptrBase as a function pointer to a function, which is passed void and returns void:
typedef void (*fptrBase)();
The following sequence demonstrate the use of this base pointer, which duplicates the previous example:
fptrBase basePointer;
fptrFirst = add;
basePointer = (fptrToSingleInt)fptrFirst;
fptrFirst = (fptrToTwoInts)basePointer;
printf("%d\n",fptrFirst(5,6));
A base pointer is used as a placeholder to exchange function pointer values.
WARNING
Always make sure you use the correct argument list for function pointers. Failure to do so will result in indeterminate behavior.
Summary
Understanding the program stack and heap structures contributes to a more detailed and thorough understanding of how a program works and how pointers behave. In this chapter, we examined the stack, the heap, and the stack frame. These concepts help explain the mechanics of passing and returning pointers to and from a function.
For example, returning a pointer to a local variable is bad because the memory allocated to the local variable will be overwritten by subsequent function calls. Passing a pointer to constant data is efficient and prevents the function from modifying the data passed. Passing a pointer to a pointer allows the argument pointer to be reassigned to a different location in memory. The stack and heap helped detail and illustrate this functionality.
Function pointers were also introduced and explained. This type of pointer is useful for controlling the execution sequence within an application by allowing alternate functions to be executed based on the application’s needs.