C Part I · Module 7 · Pointers
void * and Generic Pointers
Chapter 7.3 argued that a pointer type exists because an address on its own is not enough to do anything with — it tells you where an object begins and nothing about how many bytes it occupies or what those bytes mean. That argument was correct, and it has an awkward consequence. Some interfaces genuinely must work for every type: a routine that copies raw bytes from one place to another, or that takes an item it has been told nothing about and hands it to a function you supplied, cannot name the type it is handling. C provides a pointer type for exactly that situation, and it works by deliberately dropping the half of Chapter 7.3's definition that caused the problem. That makes both its usefulness and its restrictions entirely predictable.
Intermediate16 min readCPart IFundamentalsPointers
Part I · Module 7 · Chapter 7.12 · void * and Generic Pointers
1. The Address Without the Type
void *v;
void *holds the address of an object and says nothing about what is there.
Chapter 7.3 established that a pointer type supplies two things — how many bytes *p covers, and how to interpret them. void * supplies neither. It is the address, and only the address.
Everything in this chapter follows from that one sentence. What you gain is that any object's address fits; what you pay is that nothing which depends on knowing the type will work.
2. It Converts Without a Cast
#include <stdio.h>
int main(void)
{
int i = 42;
double d = 2.5;
void *v = &i; /* no cast needed */
int *back = v; /* and back again, no cast */
printf("*back %d\n", *back);
v = &d;
double *dback = v;
printf("*dback %.1f\n", *dback);
printf("sizeof v %zu\n", sizeof v);
return 0;
}Output:
*back 42
*dback 2.5
sizeof v 8Both directions, no casts, no diagnostics.
In C, a pointer to any object type converts to and from
void *implicitly. No cast is required in either direction.
That is a deliberate design decision and it is worth noticing, because Chapter 7.3 §4 established that mixing other pointer types without a cast is a constraint violation. void * is the exception, and it is the exception precisely so that generic interfaces are usable without casts cluttering every call.
3. What You Cannot Do With It
Three restrictions, all following from §1, all verified.
You cannot dereference it
int i = 1;
void *v = &i;
int taken = *v;warning: ISO C does not allow indirection on operand of type 'void *'
[-Wvoid-ptr-dereference]
error: initializing 'int' with an expression of incompatible type 'void'Obvious once you hold §1: dereferencing needs to know how many bytes to read, and void * does not carry that. Convert it to a real pointer type first, which is what §4's example does.
You cannot take sizeof of what it points at
sizeof(void)warning: invalid application of 'sizeof' to a void type [-Wpointer-arith]void is not an object type and has no size. This is the same gap from a different direction.
You cannot portably do arithmetic on it
int a[3];
void *v = a;
v = v + 1; /* how far is "one"? */warning: arithmetic on a pointer to void is a GNU extension [-Wgnu-pointer-arith]and with -pedantic-errors:
error: arithmetic on a pointer to void is a GNU extension
[-Werror,-Wgnu-pointer-arith]Chapter 7.8 established that p + 1 moves sizeof *p bytes. For a void * there is no sizeof *p, so the arithmetic has no defined meaning in ISO C.
GCC and Clang allow it anyway, treating void * as though it were char * so that + 1 moves one byte. That is a compiler extension, not portable C, and it is common enough in existing code that you should recognise it rather than assume it is standard. If you need to walk bytes, convert to unsigned char * and say so — which costs one line and works everywhere.
| Operation | int * | void * |
|---|---|---|
| Hold an address | yes | yes |
| Assign from another object pointer type | needs a cast | implicit, both ways |
| Dereference | yes | no |
sizeof what it points at | yes | no |
| Arithmetic | yes | not portable — a GNU extension |
Compare with == / != | yes | yes |
4. The Real Use
A function that copies raw bytes cannot name the type it is copying — that is the whole point of it. Here is the shape:
#include <stdio.h>
#include <stddef.h>
void copy_bytes(void *destination, const void *source, size_t byte_count)
{
unsigned char *d = destination;
const unsigned char *s = source;
for (size_t i = 0; i < byte_count; i++) {
d[i] = s[i];
}
}
int main(void)
{
int from[3] = {11, 22, 33};
int to[3] = {0};
double dfrom = 2.5;
double dto = 0.0;
copy_bytes(to, from, sizeof from);
printf("ints %d %d %d\n", to[0], to[1], to[2]);
copy_bytes(&dto, &dfrom, sizeof dfrom);
printf("double %.1f\n", dto);
return 0;
}Output:
ints 11 22 33
double 2.5One function, two completely different types, no changes.
Four parts of that signature and body are the standard pattern, and all four are worth copying.
The parameters are void * and const void *. Any object's address fits, with no cast at the call site — to and from are int[3] and &dto is a double *. The const on the source is Chapter 7.11's habit, applied: the function reads from one and writes to the other, and the signature says which is which.
The size is a separate size_t parameter. This is Chapter 7.7's rule in its most general form — the interface knows nothing about the data, so the caller must supply the extent. sizeof from is computed in main, where from is still an array.
The conversion to a real type happens immediately, inside. unsigned char *d = destination; is where the function decides what it is looking at. unsigned char is the right choice because it is the type C provides for examining raw bytes, as Chapter 7.3 §5 noted — and converting once at the top means the body is ordinary pointer code.
After the conversion, everything is normal. d[i] = s[i] is Chapter 7.8's indexing on an ordinary pointer. No part of the loop is generic.
The pattern is: accept
void *at the boundary, convert to a concrete type immediately, and do the work in that type.
5. Where You Will Meet It
You already have, indirectly. Three of the places this type appears are things this curriculum has used or named:
printf("%p", (void *)&x) — Chapter 7.1 described the cast as part of the printing recipe. Now it makes sense: %p is specified in terms of void *, because printing an address should not depend on what is at it.
Allocating storage. A function that obtains memory cannot know what you intend to put there, so it hands back a void *. That is Chapter 9.6's subject, and it is where most C programmers first meet this type.
Library routines that copy, compare or sort. Their parameters are void * for exactly §4's reason. Chapter 12.9 covers the ones worth knowing.
6. The Boundary of the Idea
7. What You Should Be Able to Say Now
void *is an address with the type information deliberately removed — Chapter 7.3's definition, minus both halves.- It converts to and from any object pointer type implicitly in C, both ways, no cast.
- Casts appear anyway because C++ requires them and some house styles ask for them — neither is a C rule.
- It cannot be dereferenced:
*vis rejected, because nothing says how many bytes to read. sizeof(void)is invalid —voidis not an object type.- Arithmetic on it is a GNU extension, not portable C, and an error under
-pedantic-errors. - The standard shape: accept
void *, convert to a concrete type immediately, work in that type. - Generic interfaces must take the size separately, for Chapter 7.7's reason in its most general form.
- Use
const void *for a parameter the function only reads. - It covers object pointers only — function pointers are different, and a wrong round trip cannot be caught.
8. Exercises
Exercise 1 — classify the operations. For int i; void *v = &i;, say which are valid C, which need a cast, and which are not portable.
v = &i; int *p = v; *v; v + 1; sizeof *v; v == &i;Exercise 2 — predict the diagnostics. For each, say what the compiler reports and why.
void *v = &some_double; double *d = v;
void *v = &some_double; int taken = *v;
void *v = arr; v = v + 1;
printf("%zu\n", sizeof(void));Exercise 3 — write the signature. Write the signature for a function that fills any object with a given byte value, and say why each parameter has the type you chose.
Exercise 4 — convert at the boundary. Rewrite this so it follows §4's pattern, and say what is wrong with the original.
void fill_bytes(void *target, size_t n, unsigned char value)
{
for (size_t i = 0; i < n; i++) {
((unsigned char *)target)[i] = value;
}
}Exercise 5 — explain the recipe. Chapter 7.1 told you to write (void *) before a %p argument without explaining it. Explain it now, in two sentences.
Exercise 6 — find the undetectable bug. Say what happens here, why no diagnostic appears, and what discipline from §6 would have prevented it.
int value = 7;
void *generic = &value;
double *wrong = generic;
printf("%f\n", *wrong);Exercise 7 — judge the cast. A project requires (int *) when converting from void *. Give the argument for the rule, the argument against adding casts by habit from Chapter 7.3 §5, and say what you would do in a C-only codebase.
Exercise 8 — size the interface. Explain why copy_bytes must take a byte count rather than working it out, relating your answer to Chapter 6.1's result about arrays and Chapter 7.7's about parameters.
9. Summary
void *is the address with the type removed — neither a width nor an interpretation.- It converts implicitly to and from any object pointer type, in both directions.
- Casts are common because of C++ compatibility and house style, not because C needs them.
- No dereference, no
sizeof(void), and no portable arithmetic — the last is a GNU extension. - The pattern is convert at the boundary and work in a concrete type, usually
unsigned char *. - Generic interfaces must take the extent separately.
- Use
const void *when the function only reads. - It is for object pointers; function pointers are a separate kind of thing.
- A wrong round trip is undetectable, so keep the two conversions close and give one place the responsibility.
Every pointer in this module has identified data — an int, a double, a run of bytes. Functions are not data, and a function is not an object in the sense Module 2 established, so none of this module's rules obviously apply to them. Yet a function does occupy a position in the running program, calls do have to find it, and §6 hinted that a pointer to one is a different kind of thing rather than an impossible one. The last chapter of this module covers what a pointer to a function is, how to declare one without the notation becoming unreadable, and the two patterns — callbacks and dispatch tables — that make them worth the trouble.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
