C Void and Double Pointers
A void * pointer is a generic object pointer: it can hold the address of many different object types without saying which type is stored there. A double pointer, such as int **, points to another pointer and is used when code needs one more level of indirection.
These two ideas show up often in real C programs. void * appears in generic library interfaces and dynamic memory, while double pointers let functions update a caller’s pointer, swap pointers, or build linked data structures.
Overview: How Void and Double Pointers Work
A normal pointer type includes the type of object it points to. An int * points to an int, so dereferencing it with *p tells C to read or write an int. A void * deliberately removes that object type. It stores an address, but void has no size and no representation as an ordinary object, so C cannot dereference a void * directly.
This makes void * useful for generic interfaces. A function can accept const void *data when it only knows that some object address is being passed. Inside the function, the code must use additional information, such as an enum, a size, or a callback, to convert the pointer back to the correct type before reading the object. The conversion is not a magic runtime check. It is the programmer telling the compiler how to interpret the bytes at that address.
Object pointers convert to and from void * without an explicit cast in C. For example, an int * can be assigned to a void *, and the same value can later be assigned back to an int *. That does not mean every conversion is safe to dereference. The final typed pointer must still match a real live object, be correctly aligned for that type, and obey C’s aliasing rules.
A double pointer adds another level. If int *p stores the address of an int, then int **pp = &p; stores the address of the pointer variable p. The expression *pp is the pointer p, and **pp is the int that p points to. This is not special syntax for two-dimensional arrays; it is simply pointer-to-pointer indirection.
Double pointers are especially important because C passes arguments by value. If a function receives int *p, it receives a copy of a pointer. It can modify *p, the pointed-to integer, but assigning p = something_else; changes only the local copy. If the function must make the caller’s pointer point somewhere else, the caller passes &p, and the function parameter is int **.
Syntax
#include <stdio.h>
int main(void)
{
int value = 42;
int *number = &value;
void *generic = number;
int **number_handle = &number;
printf("From void pointer: %d\n", *(int *)generic);
**number_handle = 50;
printf("After double pointer: %d\n", value);
return 0;
}
Output:
From void pointer: 42
After double pointer: 50
| Syntax | Meaning |
|---|---|
void *p |
A generic object pointer. It stores an address but cannot be dereferenced until converted to a complete object pointer type. |
const void *p |
A generic pointer used for read-only access to unknown object data. |
(int *)p |
Converts a generic pointer back to int * so an int can be accessed. |
int **pp |
A pointer to an int * variable. |
*pp |
The pointer variable being pointed to. |
**pp |
The object reached after following two pointer levels. |
Standard C does not allow pointer arithmetic on void *, because void has no element size. Use unsigned char * when you intentionally need byte-by-byte movement through storage.
Examples
Use a Void Pointer With a Type Tag
#include <stdio.h>
enum ValueType {
VALUE_INT,
VALUE_DOUBLE,
VALUE_CHAR
};
void print_value(const void *data, enum ValueType type)
{
if (data == NULL) {
printf("missing\n");
return;
}
switch (type) {
case VALUE_INT:
printf("int: %d\n", *(const int *)data);
break;
case VALUE_DOUBLE:
printf("double: %.2f\n", *(const double *)data);
break;
case VALUE_CHAR:
printf("char: %c\n", *(const char *)data);
break;
}
}
int main(void)
{
int count = 7;
double price = 19.95;
char grade = 'A';
print_value(&count, VALUE_INT);
print_value(&price, VALUE_DOUBLE);
print_value(&grade, VALUE_CHAR);
return 0;
}
Output:
int: 7
double: 19.95
char: A
The parameter data can receive the address of an int, double, or char. The enum tells the function which type is really present. Each case converts the const void * to the matching pointer type before dereferencing.
Swap Two Pointer Variables With a Double Pointer
#include <stdio.h>
void swap_int_pointers(int **left, int **right)
{
int *temporary = *left;
*left = *right;
*right = temporary;
}
int main(void)
{
int first = 10;
int second = 20;
int *a = &first;
int *b = &second;
printf("Before: *a=%d *b=%d\n", *a, *b);
swap_int_pointers(&a, &b);
printf("After: *a=%d *b=%d\n", *a, *b);
return 0;
}
Output:
Before: *a=10 *b=20
After: *a=20 *b=10
The function does not swap the integers themselves. It swaps the caller’s pointer variables. Passing &a and &b gives the function addresses of those pointer variables, so assigning through *left and *right changes the caller’s a and b.
Build a List by Updating the Head Pointer
#include <stdio.h>
struct Node {
int value;
struct Node *next;
};
void push_front(struct Node **head, struct Node *node)
{
node->next = *head;
*head = node;
}
void print_list(const struct Node *head)
{
const struct Node *current = head;
while (current != NULL) {
printf("%d", current->value);
if (current->next != NULL) {
printf(" -> ");
}
current = current->next;
}
printf("\n");
}
int main(void)
{
struct Node nodes[3] = {
{10, NULL},
{20, NULL},
{30, NULL}
};
struct Node *head = NULL;
push_front(&head, &nodes[0]);
push_front(&head, &nodes[1]);
push_front(&head, &nodes[2]);
print_list(head);
return 0;
}
Output:
30 -> 20 -> 10
The list’s head pointer lives in main. The push_front function must update that pointer when a new node becomes the first node. A parameter of type struct Node ** gives it controlled access to the caller’s head pointer.
How It Works Step by Step
- An object such as
int counthas storage, a value, a type, and an address. - The expression
&countcreates anint *, which can be stored in avoid *because it is an object pointer. - Before reading through the generic pointer, the program converts it back to the correct type, such as
const int *. - The dereference then uses that restored type to read the right number of bytes and interpret them correctly.
- For a double pointer,
&pgets the address of the pointer variable itself. - Inside the function, assigning to
*ppchanges the caller’s pointer variable, while assigning to**ppchanges the object reached by that pointer.
The extra indirection is powerful because it chooses what level of storage you are changing. With *p, you change the pointed-to object. With *pp, you change the pointer that points to the object. Clear naming matters because one misplaced asterisk can change the meaning of the program.
Common Mistakes
Dereferencing a void * Directly
A void * has no pointed-to object size, so *generic is not valid standard C. Convert to the correct complete object pointer first. The syntax example uses *(int *)generic because the original object is an int.
Forgetting That void * Does Not Remember the Type
If an int * is stored in a void * and later converted to double *, the compiler may accept the cast, but the program is wrong. The address still points to an int object. Keep type information nearby with a tag, a size, or a callback interface.
Passing a Pointer When a Function Needs a Pointer to Pointer
A function that updates the caller’s pointer needs the address of that pointer. Here is a corrected small example:
#include <stdio.h>
void choose_larger(int **target, int *left, int *right)
{
if (*left > *right) {
*target = left;
} else {
*target = right;
}
}
int main(void)
{
int a = 12;
int b = 30;
int *chosen = NULL;
choose_larger(&chosen, &a, &b);
printf("Chosen value: %d\n", *chosen);
return 0;
}
Output:
Chosen value: 30
The call passes &chosen, not chosen. That lets choose_larger assign a new value to the pointer variable in main.
Using the Wrong Level of Dereference
With int **pp, pp is the address of a pointer, *pp is a pointer, and **pp is an integer. If you want to redirect the pointer, assign to *pp. If you want to modify the integer, assign to **pp.
Best Practices
- Use
void *only when an interface truly needs generic object addresses. - Pair every
void *with reliable type information: an enum tag, an element size, or a function pointer that knows the real type. - Use
const void *for generic data that should only be read. - Do not do arithmetic on
void *in portable C; convert tounsigned char *for byte-wise traversal. - Avoid unnecessary casts from allocation functions in C; assigning a returned
void *to a typed object pointer is allowed. - Use double pointers when a function must update a caller’s pointer, not merely the object being pointed to.
- Name pointer-to-pointer parameters by role, such as
head,target, orhandle, so the extra level is easier to follow. - Check nullable pointer and pointer-to-pointer parameters before dereferencing when your interface allows
NULL.
Practice Exercises
- Write a function
void print_item(const void *item, int type)that can print either anintor afloat. Use a simple enum for the type values. - Write
void set_to_first_even(int **result, int values[], int count). It should make*resultpoint to the first even number, or set it toNULLif none exists. - Modify the linked-list example so
push_frontreturns nothing and rejects aNULLnode safely.
Summary
void *stores a generic object address but cannot be dereferenced directly.- Convert a
void *back to the correct object pointer type before accessing the object. void *does not store runtime type information; the program must keep that information elsewhere.- A double pointer points to another pointer variable.
- Use
*ppto change the pointer variable and**ppto change the final pointed-to object. - Double pointers are common in functions that update caller-owned pointers, such as list insertion helpers.
- Correct type, lifetime, alignment, and null checks still matter at every pointer level.
