Pointers Part 3: void*, Pointer-to-Pointer, Pointer Arrays
This chapter covers three pointer forms that build on the basics: void * as a generic object pointer, pointers to pointers, and arrays of pointers. These are the mechanisms behind generic containers, out-parameters for pointers, and the classic argv.
Why This Matters
void * is how C writes generic code (sorting, copying, callbacks). Pointer-to-pointer is how a function can give the caller a new pointer (for example, allocating and returning a buffer). Arrays of pointers are how argv, string tables, and many data structures are represented.
Prerequisites
c.core.20— pointer arithmetic and comparison.
Core Concept
void* — generic object pointer
void * can hold the address of any object, and any object pointer converts to and from void * without a cast (in C). It cannot be dereferenced or used in arithmetic, because void has no size.
int x = 5;
void *p = &x; /* ok */
int *q = p; /* ok: implicit conversion back */
/* *p is illegal */
Pointer-to-pointer
A pointer to a pointer (T **) holds the address of a T * object. It is needed when a function must change *which* pointer the caller has (e.g., allocate a buffer and hand it back).
void allocate(int **out) {
*out = malloc(sizeof(int));
}
Arrays of pointers
An array of pointers (T *arr[N]) holds N pointers. It is a natural representation for a table of strings or a list of objects.
char *names[] = { "alice", "bob", "carol" };
Syntax
void *vp;
int **pp;
int *arr[10]; /* array of 10 int pointers */
Examples
void* for generic data
#include <stdlib.h>
/* qsort comparator: receives pointers to elements as void* */
int compare_int(const void *a, const void *b)
{
const int *ia = a;
const int *ib = b;
return (*ia > *ib) - (*ia < *ib);
}
Pointer-to-pointer out-parameter
#include <stdlib.h>
#include <stdio.h>
int make_int(int **out, int value)
{
*out = malloc(sizeof **out);
if (*out == NULL)
return -1;
**out = value;
return 0;
}
int main(void)
{
int *p = NULL;
if (make_int(&p, 42) == 0) {
printf("%d\n", *p);
free(p);
}
return 0;
}
Expected output: 42.
Array of pointers (argv pattern)
#include <stdio.h>
int main(int argc, char **argv)
{
for (int i = 0; i < argc; i++)
printf("%s\n", argv[i]);
return 0;
}
argv is char **, but as a parameter it is equivalent to char *argv[] — an array of pointers to the argument strings.
How It Works
void * is a pointer type with no target size, so the compiler permits conversions but forbids dereference and arithmetic. A pointer-to-pointer is an ordinary pointer whose target is itself a pointer; dereferencing once yields a pointer, twice yields the ultimate object. An array of pointers is a contiguous array where each element is a pointer (not the pointed-to data).
Variations
Multiple levels of indirection
int ***ppp; /* pointer to pointer to pointer */
Deeper indirection is occasionally needed but should be used sparingly.
Arrays of void *
void *table[8]; /* an array of generic pointers */
Common Mistakes
- Dereferencing or doing arithmetic on
void *(illegal in ISO C). - Confusing
int **pwithint (*p)[N]. - Confusing an array of pointers with a pointer to an array.
- Forgetting that
argv[0]is the program name (by convention), not the first
user argument.
Undefined Behavior
- Dereferencing
void *is a constraint violation (not UB; the compiler must
diagnose it).
- Using a pointer-to-pointer that points to an invalid pointer object (e.g.,
a dangling or wild pointer) is UB.
- Out-of-bounds access of an array of pointers is UB.
Portability
void *↔ object-pointer conversions are standard and portable.- Function pointers do not convert to
void *portably (seec.func.fnptr).
Under the Hood
A void * is the same width as other object pointers on typical systems. Pointer-to-pointer is just a pointer stored in memory; accessing through it requires two loads. Arrays of pointers store pointer-sized elements.
Practical Usage
- Use
void *for generic container APIs and callback user data. - Use
T **for functions that allocate and return a pointer. - Use
char *argv[]/char **argvfor command-line arguments.
Exercises
1. Write a generic swap that swaps two objects of any type using void * and size_t (like qsort's approach). 2. Write a function that allocates a string and returns it through a char ** output parameter. 3. Explain the difference between int *a[10] and int (*a)[10]. 4. Iterate over argv and print each argument with its index.
Deep Challenge
Implement a dynamic array of void * pointers (a "vector of pointers") that can grow using realloc, and write functions to push/pop/get. Explain how void * makes it generic and where ownership and type-safety boundaries lie.
Related Concepts
c.core.22— pointer-to-array vs. array-of-pointers.c.func.fnptr— function pointers (which do not convert to void*).c.mem.malloc— allocation in out-parameters.
References
- ISO/IEC 9899:2018 §6.3.2.3 (pointer conversions), §6.2.5 (derived types).
Verification
void *cannot be dereferenced or used in arithmetic in ISO C.VERIFIED- Object pointers convert to/from
void *without a cast.VERIFIED char **argvandchar *argv[]are equivalent as parameters.VERIFIED- No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] void* as generic pointer
- [ ] Pointer-to-pointer
- [ ] Arrays of pointers
- [ ] Out-parameters for pointers
- [ ] argv pattern
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.ptr.void | 0 | 6 |
| c.ptr.ptr-to-ptr | 0 | 6 |
| c.ptr.array-of-ptr | 0 | 5 |