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

  1. An object such as int count has storage, a value, a type, and an address.
  2. The expression &count creates an int *, which can be stored in a void * because it is an object pointer.
  3. Before reading through the generic pointer, the program converts it back to the correct type, such as const int *.
  4. The dereference then uses that restored type to read the right number of bytes and interpret them correctly.
  5. For a double pointer, &p gets the address of the pointer variable itself.
  6. Inside the function, assigning to *pp changes the caller’s pointer variable, while assigning to **pp changes 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 to unsigned 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, or handle, 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

  1. Write a function void print_item(const void *item, int type) that can print either an int or a float. Use a simple enum for the type values.
  2. Write void set_to_first_even(int **result, int values[], int count). It should make *result point to the first even number, or set it to NULL if none exists.
  3. Modify the linked-list example so push_front returns nothing and rejects a NULL node 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 *pp to change the pointer variable and **pp to 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.