🧬 Types & Qualifiers, In Depth
typedef: giving types better names
▶ Open the interactive lesson — free, no signupSome of C's most familiar type names — size_t, or the FILE behind every file you'll ever open — aren't built into the language at all: they're nicknames minted with typedef, and this lesson shows you the trick. You'll also use it to turn a declaration full of stars and parentheses into a name a human can read at a glance.
typedef creates an alias for an existing type — a nickname, not a new type. typedef int Celsius; makes Celsius and int fully interchangeable (the compiler won't stop you mixing them — it's documentation, not a wall between types). Where typedef shines is taming C's gnarlier type syntax.
The one insight that makes typedef easy
A typedef is written exactly like a variable declaration, with typedef stapled on front. Wherever the variable name would be, that becomes the type name:
/* variable declarations... ...become type aliases */
unsigned char byte_v; typedef unsigned char byte;
int pair_v[2]; typedef int pair[2];
int (*cmp_v)(const void *, const void *);
typedef int (*cmp_fn)(const void *, const void *);
/* now these are easy to read: */
byte b = 0xFF; /* one unsigned char */
pair xy = { 3, 4 }; /* an array of two ints! */
cmp_fn f; /* pointer to qsort-style fn */This mirror rule works for arbitrarily hairy types. Can you declare it as a variable? Then you can typedef it.
🧠 Checkpoint: After typedef int Meters;, what does Meters m = 5; int x = m; do?
- Compile error: incompatible types
- Compiles: Meters IS int, just under another name
- Runtime conversion from Meters to int
- Undefined behavior
Show answer
Compiles: Meters IS int, just under another name — typedef creates an alias, not a distinct type. Great for readability and portability, useless as a type-safety wall — the compiler sees int everywhere.
Taming structs and function pointers
The two heavyweight champions of "please give this a name":
#include <stdio.h>
#include <stdlib.h>
typedef struct Node Node; /* name first: usable below */
struct Node {
int value;
Node *next; /* thanks to the line above */
};
typedef int (*cmp_fn)(const void *, const void *);
int by_int(const void *a, const void *b) {
return *(const int *)a - *(const int *)b;
}
int main(void) {
cmp_fn cmp = by_int; /* vs: int (*cmp)(const void*, const void*) */
int v[] = { 31, 4, 15 };
qsort(v, 3, sizeof v[0], cmp);
printf("%d %d %d\n", v[0], v[1], v[2]);
Node n2 = { 20, NULL }, n1 = { 10, &n2 };
printf("%d -> %d\n", n1.value, n1.next->value);
return 0;
}$ gcc tamed.c -o tamed && ./tamed 4 15 31 10 -> 20
Note the self-referencing struct: the struct Node tag is still needed inside, because the typedef name isn't usable until the declaration ends. The idiom typedef struct Node Node; before the struct body sidesteps this entirely.
🧠 Checkpoint: Inside struct Node { ... };, why can't the member be declared with a typedef name defined after the struct?
- Members can never be pointers to the own struct
- A typedef name only exists once its declaration is complete — inside the body you must use the struct tag (or pre-declare the typedef)
- typedef and struct cannot mix
- It can — order never matters in C
Show answer
A typedef name only exists once its declaration is complete — inside the body you must use the struct tag (or pre-declare the typedef) — C is processed top-to-bottom. Either use the tag (struct Node *next;) or write typedef struct Node Node; BEFORE the body — then Node *next; works inside it.
Opaque types: typedef as API armor
Here's a professional trick. Declare a typedef of an incomplete struct in the header, and define the struct only in the .c file. Users can hold pointers to a Timer, but can't see or touch its fields — the layout can change without breaking anyone:
/* ---- timer.h (what users see) ---- */
typedef struct Timer Timer; /* incomplete: fields hidden */
Timer *timer_start(const char *name);
double timer_elapsed(const Timer *t);
void timer_free(Timer *t);
/* ---- timer.c (private) ---- */
struct Timer { /* real definition lives here */
const char *name;
long start_ns;
};
/* users CANNOT write t->start_ns — incomplete type! */This is exactly how FILE works in <stdio.h>: you juggle FILE * pointers everywhere but have (portably) no idea what's inside a FILE. That's an opaque type in the wild — and why the pattern is also called the "FILE idiom".
When NOT to typedef: hiding pointers
🤔 Given typedef char *string; — what exactly does const string s declare?
Think first
It declares char *const s — a const pointer to modifiable chars. Not const char *s! The qualifier applies to the typedef'd type as a sealed unit; const cannot reach "inside" the alias to qualify the pointee. This trap alone is a good reason not to hide pointers behind typedefs.
Rule of thumb: don't typedef away a * unless the type is truly opaque (a handle). typedef char *string; looks cute, but readers can no longer see that assignment copies a pointer (not the text!), and const stops meaning what they think. The Linux kernel style guide bans pointer typedefs for exactly this reason.
🧠 Checkpoint: When is typedef-ing a pointer type considered good style?
- Always — it saves keystrokes
- For opaque handles where callers should never dereference it anyway
- Whenever the pointer is const
- Never, it is a syntax error
Show answer
For opaque handles where callers should never dereference it anyway — If users are meant to treat the value as a black-box handle, hiding the * is honest. If they will dereference, index, or free it, hiding its pointer-ness just obscures the code and breaks const intuition.
You use typedefs constantly already
size_t, ptrdiff_t, uint32_t, sig_atomic_t, FILE, time_t — all typedefs. That's the portability trick of the standard library: uint32_t might alias unsigned int on your machine and unsigned long on another, but your code just says uint32_t and works everywhere.
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
typedef names existing types — next up is the keyword that mints whole families of named integer constants: enum.