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.
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.
Local variables are also called automatic variables. They are always allocated to a stack frame.
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
"
);
}
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.
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.
A stack frame consists of several elements, including:
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
"
,
&
arr
);
printf
(
"size: %p
"
,
&
size
);
printf
(
"sum: %p
"
,
&
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.
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.
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.
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.
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.
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 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:
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
"
,
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
);
We must eventually free it once we are through using it. If we don’t, then we will have a memory leak.
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
"
,
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.
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 section Returning Strings, an example of using global and static values is demonstrated.
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.
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.
To easily identify problems such as memory leaks, draw a diagram of memory allocation.
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
"
,
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.
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
"
,
pi
);
safeFree
(
pi
);
printf
(
"After: %p
"
,
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.
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.
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.
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
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.
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
,
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
=
&
square
;
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.
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
,
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 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
"
,
compute
(
add
,
5
,
6
));
printf
(
"%d
"
,
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 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
"
,
evaluate
(
'+'
,
5
,
6
));
printf
(
"%d
"
,
evaluate
(
'-'
,
5
,
6
));
The output will be an 11 and a –1.
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
NULL
s. 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 NULL
s, 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
"
,
evaluateArray
(
'+'
,
5
,
6
));
printf
(
"%d
"
,
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.
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
"
);
}
else
{
printf
(
"fptr1 does not point to add function
"
);
}
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.
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
"
,
fptrFirst
(
5
,
6
));
This sequence, when executed, will display 11 as its output.
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
"
,
fptrFirst
(
5
,
6
));
A base pointer is used as a placeholder to exchange function pointer values.
Always make sure you use the correct argument list for function pointers. Failure to do so will result in indeterminate behavior.
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.
3.145.33.235