Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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

Azvya Education Pvt. Ltd.VLSI Mentor
in, and back out
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
*back       42
*dback      2.5
sizeof v    8

Both 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int i = 1;
    void *v = &i;
    int taken = *v;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    sizeof(void)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int a[3];
    void *v = a;
    v = v + 1;              /* how far is "one"? */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: arithmetic on a pointer to void is a GNU extension [-Wgnu-pointer-arith]

and with -pedantic-errors:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Operationint *void *
Hold an addressyesyes
Assign from another object pointer typeneeds a castimplicit, both ways
Dereferenceyesno
sizeof what it points atyesno
Arithmeticyesnot portable — a GNU extension
Compare with == / !=yesyes

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:

Azvya Education Pvt. Ltd.VLSI Mentor
a type-agnostic copy
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ints   11 22 33
double 2.5

One 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

  1. void * is an address with the type information deliberately removed — Chapter 7.3's definition, minus both halves.
  2. It converts to and from any object pointer type implicitly in C, both ways, no cast.
  3. Casts appear anyway because C++ requires them and some house styles ask for them — neither is a C rule.
  4. It cannot be dereferenced: *v is rejected, because nothing says how many bytes to read.
  5. sizeof(void) is invalid — void is not an object type.
  6. Arithmetic on it is a GNU extension, not portable C, and an error under -pedantic-errors.
  7. The standard shape: accept void *, convert to a concrete type immediately, work in that type.
  8. Generic interfaces must take the size separately, for Chapter 7.7's reason in its most general form.
  9. Use const void * for a parameter the function only reads.
  10. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.