C Mastery / Types Part 3: void, Pointer Types, and Type System Foundations
Part 1 — The Core Language

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

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:

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:

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:

arrays).

(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:

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

Undefined Behavior

not conforming and the compiler must diagnose it).

implementation-defined, and round-tripping may lose information.

violates strict aliasing (c.obj.aliasing).

Portability

pointers do *not* convert to/from void * portably.

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

operate on "some object" (e.g., qsort, memcpy, callback user data).

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.

References

§6.3.2.3 (pointer conversions).

Verification

verified.`

Progress

Concept checkboxes

Mastery levels

ConceptCurrent level (0–8)Target level
c.types.void04
c.types.ptr-type05