C / STRUCTS, UNIONS AND ENUMS
typedef and naming types without hiding pointers
Name struct, union and enum types with typedef, read any alias back to its base declaration, and keep pointer indirection visible in your APIs.
What you will learn
- Declare tag and alias in one shot: typedef struct Task { ... } Task;
- Read any typedef by deleting the keyword and asking what name it would declare
- Predict that const on a pointer alias locks the pointer, not the pointed-to object
- Wrap an int in a struct when you need a type the compiler will keep distinct
Understanding typedef and naming types without hiding pointers
typedef does not create a type. It adds a second name for a type that already exists, and that name lives in the same namespace as variables and functions, which is why typedef struct Counter { int hits; } Counter; is legal: struct tags occupy their own separate namespace, so the tag Counter and the alias Counter never collide. Because no new type comes into existence, the compiler gains no extra checking from the alias, and two aliases for int stay interchangeable everywhere they appear.
The way to read a typedef is to cover the keyword and ask what the remaining declaration would have declared. char *name; declares a pointer, so typedef char *name; makes name a spelling for char *; int f(void); declares a function, so typedef int f(void); names a function type. typedef reuses declarator syntax instead of inventing its own, which is why the alias name sits wherever a variable name would sit, sometimes buried in the middle, as in typedef int (*Cmp)(const void *, const void *);.
Whatever the alias absorbs then disappears from every declaration that uses it, and a * is the thing readers most need to see. A parameter written Row *r warns the caller that the function can reach back into their object, RowRef r says nothing at all, and const RowRef r actively misleads, because const attaches to the alias as a whole and therefore qualifies the pointer instead of its target. Alias struct, union and enum types freely, since the payoff is that you stop writing struct everywhere, but leave stars and array brackets out in the open. The one deliberate exception is an opaque handle, where hiding the pointer is the goal: a caller cannot dereference what it cannot see.
<stdio.h>
/* One declaration gives both a struct tag and an alias for the same type. */
typedef struct Counter {
int hits;
} Counter;
/* An alias that swallows a '*'. */
typedef Counter *CounterRef;
/* const binds to the alias, so this parameter is Counter *const ref:
a fixed pointer to a still-writable Counter. */
static void touch_hidden(const CounterRef ref)
{
ref->hits++;
}
/* Star written out, so const lands on the pointed-to Counter. */
static void touch_visible(const Counter *p)
{
printf("visible view: hits=%d\n", p->hits);
/* p->hits++; would not compile here */
}
int main(void)
{
Counter c = { 0 };
struct Counter *tag = &c; /* tag name */
CounterRef ref = tag; /* alias name, same type */
touch_hidden(ref);
touch_hidden(ref);
printf("after two const-qualified calls: hits=%d\n", c.hits);
touch_visible(ref);
return 0;
}
typedef only adds another spelling for an existing type, so anything the alias absorbs, above all a *, vanishes from later declarations, including from wherever const attaches.
Worked examples
An alias is not a distinct type
Two aliases for int mix silently, while a one-member struct gives the compiler something to check.
<stdio.h>
typedef int Meters;
typedef int Seconds;
/* A struct is a genuinely new type, unlike an alias. */
typedef struct Kelvin { int v; } Kelvin;
int main(void)
{
Meters d = 100;
Seconds t = 9;
t = d; /* accepted: both aliases name plain int */
printf("t = %d\n", t);
Kelvin k = { 300 };
/* int bad = k; would be an error: Kelvin is not int */
printf("k.v = %d\n", k.v);
return 0;
}
Example explained
Line 1typedef int Meters; introduces a spelling, not a type, so Meters and Seconds are both just int.
Line 2t = d; therefore compiles with no diagnostic even under -Wall -Wextra, and a unit mix-up survives to runtime.
Line 3typedef struct Kelvin { int v; } Kelvin; does create a distinct type, so int bad = k; can be rejected.
Line 4The price of that safety is writing .v at every use, which is why the trick is saved for values worth protecting.
Aliasing the function type, not the pointer
Naming the function type keeps the star visible in the signature that takes a callback.
<stdio.h>
/* Alias the function type, not a pointer to it. */
typedef int Reducer(int, int);
static int add(int a, int b) { return a + b; }
static int mul(int a, int b) { return a * b; }
static int fold(Reducer *f, int seed, const int *v, int n)
{
int acc = seed;
for (int i = 0; i < n; i++)
acc = f(acc, v[i]);
return acc;
}
int main(void)
{
int v[] = { 1, 2, 3, 4 };
printf("sum = %d\n", fold(add, 0, v, 4));
printf("product = %d\n", fold(mul, 1, v, 4));
return 0;
}
Example explained
Line 1typedef int Reducer(int, int); names the function type, so the indirection stays visible as Reducer *f.
Line 2fold(add, 0, v, 4) works because a function name used as a value converts automatically to a pointer to that function.
Line 3f(acc, v[i]) needs no (*f)(...), since the call operator accepts a function pointer directly.
Line 4The common alternative, typedef int (*ReducerPtr)(int, int);, hides the star, which is tolerable here because a function pointer cannot be mistaken for owned data.
An array alias collapses in a parameter list
Vec3 looks like a by-value triple but decays to int * as soon as it is a parameter type.
<stdio.h>
typedef int Vec3[3];
/* Looks like three ints by value; the parameter is adjusted to int *. */
static void zero_x(Vec3 v)
{
v[0] = 0;
}
int main(void)
{
Vec3 a = { 7, 8, 9 };
zero_x(a);
printf("%d %d %d\n", a[0], a[1], a[2]);
return 0;
}
Example explained
Line 1Vec3 a = { 7, 8, 9 }; really is an array of three ints, because a local declaration keeps the array-ness of the alias.
Line 2As a parameter type, an array is adjusted to a pointer, so Vec3 v means exactly int *v and nothing is copied.
Line 3The write to v[0] therefore changes the caller's array, which the signature gives no hint of.
Line 4sizeof v inside zero_x is the size of a pointer, not of three ints.
Important notes
#define Str char * is not equivalent: after the macro, Str a, b; gives one pointer and one plain char, while typedef char *Str; makes both pointers, because typedef works on the type rather than on the text.
typedef const char *CStr; is the defensible flavour of pointer alias, since the qualifier is baked into the alias and no caller can be misled into thinking an extra const protects the characters.
Common mistakes
Using the alias inside the struct that defines it, as in typedef struct { Node *next; } Node;, which fails with an unknown type name error because the alias exists only after the declarator ends; forward-declare with typedef struct Node Node; or write struct Node *next;.
Writing const Handle h and assuming the object is protected: the const qualifies the hidden pointer, so the callee can still overwrite the caller's data and no warning is issued.
Treating typedef int UserId; typedef int OrderId; as separate types, so swapping the two arguments at a call site compiles cleanly and shows up only as wrong behaviour at runtime.
Try it yourself
Change, predict, then run
Define typedef struct Account { long cents; } Account; together with typedef Account *AccountRef;, then write void wipe(const AccountRef a) and void wipe2(const Account *a), each doing a->cents = 0;, and compile to see which one the compiler rejects.
Open the C workspaceCheck your understanding
Given typedef struct Row Row; and typedef Row *RowRef;, what does a parameter declared const RowRef r actually guarantee?
- That neither r nor the Row it points to can be modified.
- That r cannot be repointed, while the Row's members remain writable.
- That the Row is read-only, although r itself may be repointed.
- Nothing at all, because const on a parameter is only a note to the reader.
Show answer
RowRef already contains the star, so const qualifies that pointer type itself and the declaration means Row *const r: the pointer is fixed, the target is not. Option three is the reading most people expect, but it describes const Row *r, which you can only get by writing the star out in the signature.