Types Part 3: void, Pointer Types, and Type System Foundations
This chapter introduces void, the idea of a pointer type, and the fundamental type-system vocabulary (compatible types, incomplete types, derived types) that the rest of C depends on.
Why This Matters
Pointers are the single most important and most misunderstood feature of C. You cannot understand them until you understand that a pointer has a *type*, and that the type is what gives the pointer meaning. This chapter lays the type-system foundation before pointers are covered operationally in c.core.19.
Prerequisites
c.core.3— integer types.c.core.2— objects.
Core Concept
What void is
void is an incomplete type that cannot be completed. It has three distinct uses:
1. Function returns nothing: void f(void); 2. Function takes no arguments: int f(void); (the void parameter list). 3. Generic pointer target: void *p; — a pointer to an object of unknown type.
void itself has no values and no objects. You cannot declare an object of type void.
What a pointer type is
A pointer type is a derived type: for any object type T, there is a pointer type T * whose values are addresses of objects of type T (or one-past-the-end, or null).
The critical idea: int * and char * are different types. A pointer is not just "a number that holds an address"; it carries the type of the object it points to. That type controls:
- how many bytes
*preads or writes; - how
p + 1steps (bysizeof(T)bytes, not 1); - what type the dereference expression has.
Syntax
Declaring pointers
int *p; /* p is a pointer to int */
char *q; /* q is a pointer to char */
double *r; /* r is a pointer to double */
void *vp; /* vp is a pointer to an unknown object type */
The * binds to the declarator, not the type. This is a notorious C trap:
int *p, q; /* p is int*, but q is plain int, NOT int* */
Write one declaration per line to avoid this, or attach the * to the name (int *p;). To declare two pointers you must write:
int *p, *q;
Function void
void log_message(const char *msg); /* returns nothing */
int rand(void); /* takes no arguments */
int rand(); (empty parentheses) is not the same as int rand(void);. Empty parentheses mean "unspecified arguments" (an old-style declaration), while (void) means "no arguments." Always use (void).
Examples
Pointer types are distinct
int main(void)
{
int x = 5;
int *pi = &x; /* ok */
char *pc = &x; /* constraint violation: incompatible pointer types */
(void)pi;
(void)pc;
return 0;
}
This does not compile portably. Assigning int * to char * requires a cast, and even then dereferencing it may be undefined behavior (see c.ptr.void).
void as a generic pointer
#include <stdlib.h>
int main(void)
{
int x = 5;
void *vp = &x; /* ok: any object pointer converts to void* */
/* printf("%d\n", *vp); ERROR: cannot dereference void* */
(void)vp;
return 0;
}
void * is the universal *object* pointer: any object pointer converts to void * and back without a cast (in C). But you cannot dereference void * because the compiler does not know the target type.
How It Works
A pointer value, at the hardware level, is an address. But at the type-system level, a pointer has a type that the compiler uses for:
- Dereferencing:
*preads/writessizeof(T)bytes interpreted as aT. - Arithmetic:
p + naddsn * sizeof(T)bytes. - Compatibility:
T *andU *are compatible only ifTandUare
compatible (with a special exception for void * and for qualified variants).
The distinction between "address" (hardware) and "pointer" (typed value with provenance) is developed fully in c.obj.provenance. For now: a pointer is *typed*; an address is the raw machine concept.
Variations
Types and type categories
The C type system classifies types into:
- Object types: types that describe objects (e.g.,
int,struct S,
arrays).
- Function types: types that describe functions (e.g.,
int (double)). - Incomplete types: types that describe objects but lack size information
(e.g., void, struct S; before the full definition, arrays of unknown size). You cannot declare an object of an incomplete type, but you can have a pointer to one.
Derived types
A type can be derived from another by adding:
*— pointer to[]— array of()— function returning
These combine to form complex declarators (c.typedef.fnptr covers reading them).
Compatible and composite types
Two types are compatible if they are the same type (possibly with some allowed differences like missing array size or extern qualification). When several declarations of the same identifier are visible, they must be compatible; a composite type may be formed. This is the mechanism behind forward declarations and header declarations.
Common Mistakes
- Writing
int* p, q;and thinking both are pointers. - Confusing
int f()withint f(void). - Dereferencing
void *without a cast. - Assuming a pointer is just an integer (it is not; see
c.obj.provenance). - Declaring an object of type
void.
Undefined Behavior
- Dereferencing a
void *is a constraint violation (not UB; the program is
not conforming and the compiler must diagnose it).
- Converting a pointer to an integer type too small to hold it is
implementation-defined, and round-tripping may lose information.
- Using a pointer to an object of the wrong type to access it generally
violates strict aliasing (c.obj.aliasing).
Portability
void *↔ object-pointer conversions are guaranteed by ISO C. Function
pointers do *not* convert to/from void * portably.
- The representation of a pointer is implementation-defined; do not assume it
equals a long or int.
Under the Hood
The compiler represents a typed pointer internally as a value plus type metadata for its own analysis. In generated code, most pointers are just addresses in a register, but the *size* of the access and the *offset* in pointer arithmetic are baked in at compile time from the type.
Practical Usage
void *is the standard way to write generic containers and callbacks that
operate on "some object" (e.g., qsort, memcpy, callback user data).
- Prefer
int *p;overint* p;to avoid the multi-declarator trap. - Use
(void)in empty parameter lists always.
Exercises
1. Write a program that declares int *p, q; and attempts to assign an address to q; observe the compiler error, then fix it. 2. Explain the difference between int f() and int f(void) and demonstrate with a compiler warning. 3. Write a function void set_to_zero(void *p, size_t n) that zeroes n bytes (you may use memset); explain why the void * parameter is the right generic choice.
Deep Challenge
Using only the type-system rules in this chapter, explain the types of each of these declarations (you may defer to c.typedef.fnptr for the last):
int *a[10]; /* ? */
int (*b)[10]; /* ? */
int *f(void); /* ? */
int (*g)(void); /* ? */
Write down your answers, then check them by compiling code that uses each.
Related Concepts
c.ptr.basics— pointer operations.c.ptr.void— void pointers in depth.c.typedef.fnptr— reading complex declarators.c.obj.provenance— pointer provenance.
References
- ISO/IEC 9899:2018 §6.2.5 (types), §6.7.6.3 (function declarators),
§6.3.2.3 (pointer conversions).
Verification
int *p, q;declarespas pointer andqas int.VERIFIEDint f()vsint f(void)differ.VERIFIED- Any object pointer converts to
void *and back.VERIFIED - Function pointers do not portably convert to
void *.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 type
- [ ] Pointer type basics
- [ ] Pointer declarator binding
- [ ] void* as generic pointer
- [ ] Incomplete types
- [ ] Compatible and composite types
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.types.void | 0 | 4 |
| c.types.ptr-type | 0 | 5 |