Recall that pointer variables in C have types just like other variables. The main reason for this is so that when we dereference a pointer, the compiler knows the type of data being pointed to and can access the data accordingly. However, sometimes we are not concerned about the type of data a pointer references. In these cases we use generic pointers, which bypass C’s type system.
Normally C allows assignments only between pointers of
the same type. For example, given a character pointer
sptr
(a string) and an integer pointer
iptr
, we are not permitted to assign
sptr
to iptr
or iptr
to
sptr
. However, generic pointers can be
set to pointers of any type, and vice versa. Thus, given a generic
pointer gptr
, we are permitted to assign
sptr
to gptr
or gptr
to
sptr
. To make a pointer generic in C, we
declare it as a void pointer .
There are many situations in which void pointers are useful. For example, consider the standard C library function memcpy, which copies a block of data from one location in memory to another. Because memcpy may be used to copy data of any type, it makes sense that its pointer parameters are void pointers. Void pointers can be used to make other types of functions more generic as well. For example, we might have implemented the swap2 function presented earlier so that it swapped data of any type, as shown in the following code:
#include <stdlib.h> #include <string.h> int swap2(void *x, void *y, int size) { void *tmp; if ((tmp = malloc(size)) == NULL) return -1; memcpy(tmp, x, size); memcpy(x, y, size); memcpy(y, tmp, size); free(tmp); return 0; }
Void pointers are particularly useful when implementing data
structures because they allow us to store and retrieve data of any
type. Consider again the ListElmt
structure presented earlier for linked lists. Recall that this
structure contains two members, data
and
next
. Since
data
is declared as a void pointer, it
can point to data of any type. Thus, we can use
ListElmt
structures to build any type of
list.
In Chapter 5, one of the operations defined for linked lists is list_ins_next, which accepts a void pointer to the data to be inserted:
int list_ins_next(List *list, ListElmt *element, void *data);
To insert an integer referenced by
iptr
into a list of integers,
list
, after an element referenced by
element
, we use the following call. C
permits us to pass the integer pointer
iptr
for the parameter
data
because
data
is a void pointer.
retval = list_ins_next(&list, element, iptr);
Of course, when removing data from the list, it is important to use the correct type of pointer to retrieve the data removed. Doing so ensures that the data will be interpreted correctly if we try to do something with it. As discussed earlier, the operation for removing an element from a linked list is list_rem_next (see Chapter 5), which takes a pointer to a void pointer as its third parameter:
int list_rem_next(List *list, ListElmt *element, void **data);
To remove an integer from list
after an element referenced by element
,
we use the following call. Upon return,
iptr
points to the data removed. We pass
the address of the pointer iptr
since the
operation modifies the pointer itself to make it point to the data
removed.
retval = list_rem_next(&list, element, (void **)&iptr);
This call also includes a cast to make
iptr
temporarily appear as a pointer to a
void pointer, since this is what list_rem_next
requires. As we will see in the next section, casting is a mechanism
in C that lets us temporarily treat a variable of one type as a
variable of another type. A cast is necessary here because, although
a void pointer is compatible with any other type of pointer in C, a
pointer to a void pointer is not.
To cast a variable t
of
some type T
to another type
S
, we precede
t
with S
in
parentheses. For example, to assign an integer pointer
iptr
to a floating-point pointer
fptr
, we cast
iptr
to a floating-point pointer and then
carry out the assignment, as shown:
fptr = (float *)iptr;
(Although casting an integer pointer to a floating-point
pointer is a dangerous practice in general, it is presented here as
an illustration.) After the assignment,
iptr
and fptr
both contain the same address. However, the interpretation of the
data at this address depends on which pointer we use to access
it.
Casts are especially important with generic pointers because generic pointers cannot be dereferenced without casting them to some other type. This is because generic pointers give the compiler no information about what is being pointed to; thus, it is not clear how many bytes should be accessed, nor how the bytes should be interpreted. Casts are also a nice form of self-documentation when generic pointers are assigned to pointers of other types. Although the cast is not necessary in this case, it does improve a program’s readability.
When casting pointers, one issue we need to be particularly sensitive to is the way data is aligned in memory. Specifically, we need to be aware that applying casts to pointers can undermine the alignment a computer expects. Often computers have alignment requirements so that certain hardware optimizations can make accessing memory more efficient. For example, a system may insist that all integers be aligned on word boundaries. Thus, given a void pointer that is not word aligned, if we cast the void pointer to an integer pointer and dereference it, we can expect an exception to occur at runtime.