Think of a typedef line as a small machine: input = an existing type, output = a new name. The behaviour changes depending on what kind of type you feed it and how many variables you declare with the alias. This table lists every distinct case class.
Cell
Case class
What makes it tricky
A
Simple scalar alias (int, unsigned)
The trivial base case
B
Pointer alias, multiple vars on one line
The * is baked in — surprises int* a,b
C
struct (tag) alias
Drops the struct keyword; tag vs alias namespace
D
Anonymous struct + alias
No tag exists — alias is the only name
E
Array alias
Size is baked in; sizing surprises
F
Function-pointer alias
Ugliest declarator; the payoff case
G
Degenerate / zero case: aliasing an alias, typedef void V;
Chaining and empty-ish types
H
The typedefvs#define twist (exam trap)
Text substitution breaks where alias holds
I
Real-world word problem
Portability across machines
J
Self-referential struct (Node)
Forward reference — order matters
Every example below is tagged with the cell(s) it covers. Together they hit A–J.
Forecast: Guess before reading — is uint a new type the compiler must invent, or just a label?
Remove typedef: the line reads unsigned int uint;.
Why this step? A typedef mirrors a variable declaration exactly, so pretending it is one shows where the new name lands.
Find the "variable name": it is uint. So uint is the alias for unsigned int.
Why? Whatever type that pretend-variable would have had is exactly the alias's meaning.
Substitute:uint count = 5; means unsigned int count = 5;.
Why? The compiler treats uint and unsigned int as identical — see the figure.
Verify:sizeof(uint) == sizeof(unsigned int) — they are the same type, so the sizes are equal by definition. On a typical 32-bit-int machine that is 4 bytes. No new type was created.
Forecast: In plain C, int* a, b; makes only a a pointer. Does the alias behave the same, or differently?
Decode the alias: remove typedef → int* IntPtr; → IntPtr = "pointer to int".
Why? The * binds to the name IntPtr inside the declarator, so the alias is the whole pointer type.
Expand the declaration mentally:IntPtr a, b; = "a is IntPtr, b is IntPtr", i.e. both are int*.
Why this step? The alias is one atomic type token. It is not text; the * cannot "fall off" the second variable.
Verify:sizeof(a) == sizeof(b) == sizeof(int*) (both are pointers, typically 8 bytes on a 64-bit machine). Whereas for int* a, b;, sizeof(b) == sizeof(int) (typically 4).
Forecast: Two things are named Point. Does the compiler complain?
Two namespaces: C keeps struct tags in a separate namespace from ordinary identifiers (which includes typedef names).
Why this matters? So the tag Point (used as struct Point) and the typedef Point (used bare) can share a spelling without conflict.
The alias:typedef struct Point Point; → remove typedef → struct Point Point; → the pretend variable named Point has type struct Point. Hence bare Point ≡ struct Point.
Use:Point p; compiles because the compiler resolves the ordinary-identifier Point to the alias.
Why? Now you drop the noise word struct everywhere.
Verify:p.x + p.y == 3 + 4 == 7 after assignment — the aliased type has the same members, so field access is unchanged. sizeof(Point) == sizeof(struct Point) (same type).
Forecast: There is no tag written. Is struct Complex valid?
Decode: remove typedef → struct { double re, im; } Complex; → the variable-name slot holds Complex, whose type is "the anonymous struct".
Why? The alias becomes the only handle to this type.
No tag exists:struct Complex w; is a compile error — there is no tag named Complex, only the typedef name.
Why this step? Anonymous means "no tag"; the typedef is your sole naming route.
Verify:z.re * z.re + z.im * z.im == 9.0 + 16.0 == 25.0 (this is ∣z∣2 for z=3+4i). The struct stores two doubles regardless of naming.
Forecast: The size 3 is baked into the alias. Does sizeof(v) know it?
Decode: remove typedef → int Vec3[3]; → pretend variable Vec3 is an array of 3 ints. So the alias means "array of 3 int".
Why? The [3] is part of the declarator attached to the name — like the * in Cell B, it is baked into the alias.
Function-parameter twist: if you write void f(Vec3 a), the array-in-parameter decays to int*, so inside f, sizeof(a) == sizeof(int*). The alias does not stop C's array-to-pointer decay.
Why show this? This is the degenerate/limiting case where the baked-in size silently disappears.
Verify: On a 4-byte-int machine sizeof(v) == 12, while inside the function sizeof(a) == 8 (pointer). Element sum 1 + 2 + 3 == 6.
Decode the alias: remove typedef → int (*Operation)(int, int); → the parenthesised (*Operation) names a pointer to a function taking two ints and returning int.
Why the parentheses? Without them, int *Operation(int,int) would mean "function returning int*". The (*name) forces "pointer to function". See Function Pointers.
Array of aliases:Operation table[2] is an array of 2 such pointers — instantly readable thanks to the alias.
Why this step? Without typedef, this would be int (*table[2])(int,int) — the readability win the parent note promised.
Call:table[0](2,3) == add(2,3) == 5; table[1](2,3) == mul(2,3) == 6. So r = 5 + 6.
Verify:r == 11. Units: pure integers, no overflow near INT_MAX.
Forecast: Does aliasing an alias create a two-step lookup, or does it resolve to int?
Chaining:typedef Integer Age; → decode → Age = Integer = int. Aliases resolve transitively to the underlying type; no chain remains at runtime.
Why? Type aliases are fully resolved by the compiler; Age and int are indistinguishable.
Void alias:typedef void V; is perfectly legal — you may then write V *generic; for void *generic; (a generic pointer). But V x; (a variable of void) is illegal, exactly as void x; is.
Why this step? The degenerate void case shows the alias inherits every rule of the base type, including its restrictions.
Verify:years == 12, and Age and int satisfy sizeof(Age) == sizeof(int). There is no runtime cost to the chain.
Preprocessor expands line (1):#define is blind text substitution ⇒ INTPTR a, b; becomes int* a, b;. The * attaches to a only ⇒ a is int*, b is int.
Why?#define never understands types; it just pastes text before the compiler sees it. See Pointers in C.
Compiler handles line (2):IntPtr is one atomic type ⇒ c and d are both int* (same as Cell B).
Why the difference? The alias is a type token; the macro is a string. That is the entire moral.
Verify:sizeof(a) == sizeof(c) == sizeof(d) == sizeof(int*) (pointers), but sizeof(b) == sizeof(int). So exactly 3 of the 4 are pointers.
Forecast: The alias Node is being defined by this very statement — is it usable inside itself?
Timing: the typedef name Node only becomes visible after the whole typedef completes. Inside the braces it does not exist yet.
Why? You cannot refer to a name before its declaration finishes.
Use the tag instead: the tagnode (from struct node) is visible as soon as it is written, so struct node *next legally refers to the same type mid-definition.
Why this step? Tag namespace availability is what makes self-reference possible — the classic Linked Lists pattern.
After definition: everywhere else you may write Node n; freely.
Verify: A three-node list 1 → 2 → 3 summed by following next gives 1 + 2 + 3 == 6, and traversal terminates when next == NULL. Each Node holds one int + one pointer.
Recall Which cell did each example hit?
Ex1 A ::: scalar alias
Ex2 B ::: pointer alias, multiple vars
Ex3 C ::: struct tag alias
Ex4 D ::: anonymous struct + alias
Ex5 E ::: array alias & decay
Ex6 F ::: function-pointer alias
Ex7 G ::: chained alias + void
Ex8 H ::: typedef vs #define
Ex9 I ::: portability word problem
Ex10 J ::: self-referential struct