Structs & Data Structures · beginner · ~8 min
- Understand what `typedef` does (and, just as important, what it does **not** do) - Write `typedef` for structs so you can drop the `struct` keyword everywhere - Alias built-in and pointer types, and know when hiding a pointer is a mistake - Recognise the difference between a **value type** alias and an **opaque handle** - Apply consistent naming conventions (`_t` suffix, tag names) the way real projects do - Read and reason about the C standard library types (`size_t`, `FILE`, `time_t`) that are themselves typedefs
In the Structs lesson you learned to bundle related data into one type with struct point { double x, y; };. You may have noticed something annoying: every single time you used that type you had to write the word struct in front of it — struct point a;, struct point b;, and so on. typedef fixes that.
typedef is a small but everywhere-used feature. In plain language, it lets you give an existing type a second, more convenient name — an alias. It is a bit like a nickname: "Bob" and "Robert" refer to the same person, and an alias and the original type refer to the same type. Nothing new is created; you just get a shorter or clearer way to say the same thing.
The most common reason to reach for typedef is exactly the struct annoyance above: after one line of setup, you write point_t instead of struct point. But typedef works on any type — built-in types, pointers, arrays, function pointers — and it is the mechanism behind familiar library types such as size_t and FILE. Once you can read a typedef, a large amount of professional C code stops looking mysterious.
In technical terms: a typedef introduces a new identifier in the type namespace that is a synonym for a given type. The compiler treats the alias and the original as fully interchangeable — they are the same type to the type checker.
Every non-trivial C program defines its own vocabulary of types, and typedef is how that vocabulary is written down. Reading real code — the Linux kernel, SQLite, Git, libcurl — you will see typedef on almost every page. If you cannot read it, you cannot read the code.
Three concrete pay-offs:
Frac add(Frac a, Frac b) reads far better than struct fraction add(struct fraction a, struct fraction b). Less clutter means fewer places to make a mistake.sqlite3 or a Handle and keep the real layout private. They can change the internals in a later version without breaking your code, because you only ever named the alias.typedef so that types like size_t or time_t can be a different underlying integer on different machines while your code stays the same. This is why size_t "just works" whether you compile for a 32-bit microcontroller or a 64-bit laptop.Used well, typedef makes intent obvious. Used carelessly (especially on pointers), it hides exactly the details a reader needs. Learning the difference is the point of this lesson.
typedef actually is: an alias, not a new typeDefinition. typedef EXISTING_TYPE new_name; declares new_name as another name for EXISTING_TYPE. No new type is created — the alias and the original are the same type to the compiler.
Plain-language explanation. Think of a type as a label on a box that describes what fits inside. typedef does not build a new box; it prints a second sticker with a friendlier name and puts it on the existing box. Assigning between the alias and the original never needs a conversion, because they are the same box.
How it works internally. The syntax is deliberately weird: a typedef looks exactly like a variable declaration, except you prepend the keyword typedef. Where a normal declaration would create a variable of that type, typedef instead makes the declared name an alias of that type. So int x; declares a variable x; typedef int x; declares x as another name for int. The compiler resolves the alias at compile time — there is zero runtime cost and no extra memory.
Normal declaration: int counter;
^type ^new VARIABLE
typedef: typedef int Counter;
^keyword ^type ^new TYPE NAME (alias)
Both "Counter" and "int" now name the exact same type.
Counter c = 5; int n = c; // no conversion, they're identical
When to use it. Whenever a type name is long or repeated a lot — most often structs — or when you want a name that documents intent (typedef int Celsius;).
When NOT to use it. Do not typedef simple built-ins just to rename them cosmetically (typedef int myint; helps no one). And do not use it to hide how big or complex a type is when the reader genuinely needs to know.
Common pitfall. Beginners think typedef creates a distinct type that the compiler will keep separate for safety. It does not. typedef int Celsius; typedef int Fahrenheit; — you can freely add a Celsius to a Fahrenheit and the compiler will not complain, because both are just int.
Knowledge check. After
typedef double Money;, isMoneya brand-new type that the compiler treats differently fromdouble, or the same type under another name? (Answer: the same type;Moneyanddoubleare fully interchangeable.)
typedef with structs — the main eventDefinition. You can combine the struct definition and the typedef in one statement, so the struct's members and its alias are declared together.
Plain-language explanation. A struct type is normally spelled struct point. That two-word name is clumsy. typedef lets you attach a one-word alias so the rest of your code reads cleanly.
There are two spellings you will meet:
// Style A: separate the struct definition from the typedef
struct point { double x, y; };
typedef struct point point_t;
// Style B: do both at once (most common in practice)
typedef struct point { double x, y; } point_t;
// Style C: the tag name is optional if you only ever use the alias
typedef struct { double x, y; } point_t;
How it works internally. In Style B the compiler first sees the struct being defined (creating the type struct point), then binds the alias point_t to it. The tag (point) and the alias (point_t) live in different namespaces, which is why it is legal — and common — to give them the same name: typedef struct point { … } point;.
typedef struct point { double x, y; } point_t;
^^^^^^ ^^^^^ ^^^^^^^^^^^^ ^^^^^^^
| | | |
| | | alias (use as: point_t)
| | members
| tag (use as: struct point)
keyword introducing the struct type
When to use it. Almost always, for structs you pass around by value or reference frequently. It is the idiomatic way to define a data type in C.
When NOT to use it. If you want readers to see the word struct at every use — some style guides (notably the Linux kernel) prefer that for plain data structs — skip the alias. Consistency within a project matters more than the rule itself.
Common pitfall. Style C drops the tag, which means you cannot later write a self-referential pointer inside the struct. A linked-list node needs the tag:
typedef struct node { int val; struct node *next; } node_t;
// ^^^^ tag is REQUIRED so `struct node *next` can refer to itself
With Style C (no tag), there is no name to refer back to, and the node cannot point to another node.
Knowledge check (find the bug). Why does this fail to compile?
typedef struct { int val; Node *next; } Node;(Answer: at the point where
Node *next;appears, the aliasNodedoes not exist yet — it is only established at the end of the declaration — and there is no struct tag to refer to. Give the struct a tag:struct nodeand writestruct node *next;.)
There are two clean, professional uses of typedef. Knowing which you are doing keeps your code honest.
| Use | What the caller sees | Example | Why |
|---|---|---|---|
| Value type | The full struct layout | typedef struct { int num, den; } Frac; |
Small data you copy around; caller reads/writes members directly |
| Opaque handle | Only the alias name, layout hidden | typedef struct db db_t; (definition in a .c file) |
Library internals; caller must use the library's functions, not the fields |
Value type. The struct is small (a couple of numbers), you pass it by value, and callers touch its fields. Frac, Vec3, point_t are value types.
Opaque handle. You declare the alias in a header but define the struct's body only in the implementation file. Callers can hold a db_t * but cannot see or touch the fields — they must go through your API. This is how FILE works: you never poke at a FILE's internals; you call fopen, fread, fclose.
Knowledge check (explain in your own words). The standard library gives you
FILEbut never tells you what fields it contains. What benefit does that hiding buy the C library authors? (Answer: they can change the internal layout ofFILEbetween versions or platforms without breaking any program, because no program ever depended on those hidden fields.)
Definition. You can alias a pointer type: typedef struct node *node_ptr;. The alias then silently includes the *.
Why it is dangerous. A reader seeing node_ptr p; has no visual cue that p is a pointer. That matters because pointers carry hidden obligations: who allocated the memory, who frees it, and how long it stays valid. Hiding the * hides the ownership question.
typedef struct node *node_ptr;
node_ptr a = make_node();
node_ptr b = a; // Looks like a copy of a value.
// Actually both point to the SAME node — an aliasing bug waiting to happen.
When it is acceptable. For a genuine opaque handle whose pointer-ness is part of the abstraction (some libraries do typedef struct ctx *ctx_t;), or for function-pointer types where the raw syntax is unreadable. Even then, name it so the pointer is obvious (_handle, _ref).
Common pitfall. const interacts confusingly with pointer typedefs. const node_ptr p; makes the pointer const, not what it points to — the opposite of what most people expect from const struct node *. This subtlety is a strong reason to avoid pointer typedefs for ordinary code.
The general form mirrors a variable declaration with typedef bolted on the front:
typedef <existing type> <new alias>;
Key shapes you will use:
// 1. Alias a built-in type (documents intent)
typedef unsigned long u64;
// 2. Alias an existing struct (drop the `struct` keyword)
struct point { double x, y; };
typedef struct point point_t;
// 3. Define the struct AND alias it in one statement (idiomatic)
typedef struct point { double x, y; } point_t;
// 4. Self-referential struct — the TAG is mandatory here
typedef struct node { int val; struct node *next; } node_t;
// 5. Anonymous struct + alias (no tag; cannot self-reference)
typedef struct { int r, g, b; } color_t;
// 6. Function-pointer alias (one of typedef's most valuable uses)
typedef int (*compare_fn)(const void *a, const void *b);
Annotated: in shape 3, point is the tag (still usable as struct point), the members go inside the braces, and point_t is the alias the rest of your code will use. In shape 6, compare_fn becomes a name for "pointer to a function taking two const void * and returning int" — the alias makes qsort-style callbacks readable.
typedef doestypedef creates an alias (a second name) for a type that already exists. It does not create a new type. It just gives an existing one a more convenient name.
The most common use is to drop the struct keyword at every point where you use a struct.
struct point { double x, y; };
struct point a; // without typedef, you repeat "struct"
typedef struct point point_t;
point_t b; // with typedef, the name is shorter
Do not overuse typedef. Hiding a pointer type behind an alias is a common trap:
typedef foo_t *foo_handle;
This makes the type look like a plain value, so the reader can no longer tell that it is a pointer. Important details such as ownership (who frees the memory) and lifetime become invisible.
Reserve typedef for:
#include <stdio.h>
/* A value type: a fraction as an exact pair of integers, no floating point. */
typedef struct fraction {
int num; /* numerator */
int den; /* denominator (must never be 0) */
} Frac;
/* A self-referential type needs its struct TAG so `struct node *` can refer
back to the node being defined. */
typedef struct node {
Frac value;
struct node *next;
} Node;
/* Compare a/b vs c/d without dividing: cross-multiply.
Returns -1, 0, or 1. Assumes positive denominators. */
int frac_cmp(Frac a, Frac b) {
long left = (long)a.num * b.den; /* widen to long to avoid overflow */
long right = (long)b.num * a.den;
if (left < right) return -1;
if (left > right) return 1;
return 0;
}
int main(void) {
Frac half = {1, 2};
Frac third = {1, 3};
Frac two_q = {2, 4}; /* equals 1/2 */
printf("1/2 vs 1/3 -> %d\n", frac_cmp(half, third)); /* 1 (half is larger) */
printf("1/3 vs 1/2 -> %d\n", frac_cmp(third, half)); /* -1 */
printf("1/2 vs 2/4 -> %d\n", frac_cmp(half, two_q)); /* 0 (equal) */
/* Build a tiny two-node list on the stack to show the self-referential type. */
Node b = { {1, 3}, NULL };
Node a = { {1, 2}, &b };
for (Node *cur = &a; cur != NULL; cur = cur->next) {
printf("node holds %d/%d\n", cur->value.num, cur->value.den);
}
return 0;
}
What it does. It defines two aliases — Frac (a value type) and Node (a self-referential type) — then compares fractions exactly with cross-multiplication and walks a two-element linked list built on the stack. Notice how clean the function signatures read: int frac_cmp(Frac a, Frac b) instead of int frac_cmp(struct fraction a, struct fraction b).
Expected output:
1/2 vs 1/3 -> 1
1/3 vs 1/2 -> -1
1/2 vs 2/4 -> 0
node holds 1/2
node holds 1/3
Edge cases. frac_cmp assumes positive denominators; a negative denominator (e.g. {1,-2}) would flip the comparison and give a wrong result. Cross-multiplication can overflow for large numerators/denominators — the (long) casts push the risk further out but do not eliminate it for extreme values. A zero denominator is an invalid fraction and should be rejected before it ever reaches frac_cmp.
typedef struct fraction { int num; int den; } Frac; — defines a struct with tag fraction and immediately aliases it to Frac. From here Frac and struct fraction mean the same type.typedef struct node { Frac value; struct node *next; } Node; — a node holds a Frac and a pointer to another node. The tag node is essential: inside the braces we write struct node *next because the alias Node does not exist yet at that point.frac_cmp widens each numerator to long before multiplying: (long)a.num * b.den. If we multiplied two ints first and only then stored into a long, the overflow would already have happened. Cross-multiplying compares a.num/a.den against b.num/b.den without any division.main, Frac half = {1, 2}; uses positional initialisation: num gets 1, den gets 2. two_q = {2, 4} is numerically equal to half.printf calls pass Frac values by value — each call copies 8 bytes (two ints). The results are 1, -1, 0.Node b = { {1, 3}, NULL }; — the inner braces initialise the nested Frac; next is NULL (end of list). Node a = { {1, 2}, &b }; links a to b.for loop starts cur at &a, prints the fraction, then advances with cur = cur->next. After printing a it moves to b; after b, cur becomes NULL and the loop ends.Trace of the loop:
| Step | cur points to |
prints | next cur |
|---|---|---|---|
| 1 | a |
1/2 |
&b |
| 2 | b |
1/3 |
NULL |
| 3 | NULL |
— | loop ends |
Everything here lives on the stack, so there is nothing to free — no heap allocation was used.
Mistake 1 — Anonymous struct that tries to reference itself.
// WRONG
typedef struct { int val; Node *next; } Node; // 'Node' unknown inside the braces
Why it is wrong: the alias Node is only created at the semicolon; inside the braces there is no name (no tag either) for next to refer to. Corrected:
// RIGHT
typedef struct node { int val; struct node *next; } Node;
Prevent it: whenever a struct must point to its own type, give it a tag and use struct tag * for the self-reference.
Mistake 2 — Expecting the compiler to keep distinct aliases apart.
typedef int Celsius;
typedef int Fahrenheit;
Celsius c = 100;
Fahrenheit f = c; // compiles silently — both are just int!
Why it is wrong: typedef does not create a distinct type, so unit mix-ups are not caught. Recognise it: if you need real type safety, wrap the value in a one-field struct (typedef struct { int v; } Celsius;) so the types genuinely differ.
Mistake 3 — Hiding a pointer behind an alias.
// MISLEADING
typedef struct node *NodeP;
NodeP a = make_node();
NodeP b = a; // looks like a value copy; both actually share one node
Why it is wrong: the * is invisible, so ownership and aliasing become impossible to see at the call site. Corrected: keep the pointer visible.
// CLEARER
typedef struct node Node;
Node *a = make_node();
Node *b = a; // now it is obvious both are pointers to the same node
Mistake 4 — Putting a typedef in a header without an include guard. If two files include a header that does typedef struct { … } Foo;, and that header is pulled in twice into one translation unit, you get a redefinition error. Prevent it with an include guard (#ifndef FOO_H … #define FOO_H … #endif) around every header — standard practice regardless of typedefs.
Compiler errors you will actually see:
unknown type name 'Node' — you used an alias before its typedef line, or the self-referential struct lacks a tag. Fix: move the typedef above its first use, and add a struct tag for self-references.redefinition of typedef 'Foo' — the same typedef was seen twice, usually a header included without an include guard, or two typedefs of the same name with different bodies. Add include guards; make sure a type is defined in exactly one place.conflicting types for 'foo' — an alias resolves to a different underlying type than a later declaration expects. Check that both refer to the identical typedef.Runtime / logic errors:
frac_cmp usually mean a negative denominator slipped through, or an overflow from multiplying large numerators as int. Print left and right to see it.next chain was not terminated with NULL, or you followed a dangling pointer. Print each node address as you traverse.How to investigate:
ptype Frac and whatis half reveal that Frac is struct fraction { int num; int den; }.gcc -Wall -Wextra -std=c11 file.c. Uninitialised struct members and suspicious conversions surface here.typedef is defined in one file but used in another, confirm the header that declares it is actually included, and that the include path is correct.Questions to ask when it will not work: Is the typedef visible (declared before use, header included)? Does a self-referential struct have a tag? Am I confusing the tag namespace with the alias namespace? Is the alias hiding a pointer that I forgot to allocate or free?
typedef itself allocates nothing and has no runtime footprint — it is pure compile-time renaming — so it introduces no memory bug on its own. The safety concerns come from what you alias:
typedef struct node *NodeP; makes it hard to see that a value is heap-allocated and must be freed exactly once. Hidden pointers lead to double-frees, leaks, and use-after-free because the reviewer cannot see the pointer. Prefer keeping the * visible in ordinary code; reserve pointer typedefs for deliberate opaque handles.typedef char buffer[64]; gives you a fixed-size array type, but indexing past 63 is still undefined behaviour. The alias does not make the array safer — always bound your loops by the real size.Frac f; (no initialiser) leaves num and den as garbage. Reading them is undefined behaviour. Always initialise: Frac f = {0, 1}; or Frac f = {0};.typedef int Celsius; typedef int Fahrenheit; are the same type, the compiler will not stop you mixing them. If correctness depends on the distinction, use a wrapping struct instead so the types truly differ.const and pointer typedefs interact surprisingly. const NodeP p; (where NodeP is struct node *) makes the pointer const, not the pointee — the reverse of const struct node *. This trap is another reason to avoid pointer typedefs outside opaque-handle APIs.General rule: use typedef to make intent clearer, never to make a dangerous detail (pointer-ness, size, ownership) invisible.
Where it shows up. The C standard library is built on typedef: size_t (the type of every sizeof result and array index), FILE (the opaque type behind fopen/fread), time_t (a calendar time), ptrdiff_t, wchar_t, and the fixed-width integers in <stdint.h> (int32_t, uint64_t) are all typedefs. int32_t being a typedef is precisely why it can mean int on one platform and long on another while your code stays identical. Large projects lean on it heavily: SQLite exposes sqlite3 and sqlite3_stmt as opaque handles; graphics and OS APIs expose HANDLE, GLuint, pthread_t. Callback-heavy code (sorting, event loops, plugin systems) uses function-pointer typedefs like typedef int (*compare_fn)(const void *, const void *); to keep signatures readable.
Beginner best practices:
typedef struct { … } Name;._t suffix (point_t) or CamelCase (Point) across the whole project. (Note: POSIX reserves the _t suffix for the system, so many teams use CamelCase or a project prefix to stay clear of it.)Advanced best practices:
typedef struct impl Widget; in the header, define struct impl { … }; only in the .c file. Callers get stability; you get freedom to change the layout.* into a typedef for genuine opaque handles or unreadable function-pointer types.1. Beginner — Drop the struct keyword.
Take this code and add a typedef so you never write struct temperature again:
struct temperature { double celsius; };
struct temperature t = { 21.5 };
Objective: define an alias Temp and rewrite the declaration as Temp t = { 21.5 };. Print the value. Concepts: struct + typedef in one statement. Hint: typedef struct temperature { double celsius; } Temp;.
2. Beginner — Alias a built-in for intent.
Define typedef unsigned long Bytes; and write a function Bytes kib(Bytes n) that returns n * 1024. Call it with kib(4) and print the result (expect 4096). Objective: see that the alias behaves exactly like unsigned long. Concepts: aliasing a built-in. Hint: the alias adds no new behaviour — it is only documentation.
3. Intermediate — Vec3 value type.
Define typedef struct { int x, y, z; } Vec3; and implement long dot(Vec3 a, Vec3 b) returning a.x*b.x + a.y*b.y + a.z*b.z. Read two vectors, print the dot product.
Input example: 1 2 3 and 4 5 6 → output 32.
Requirements: pass Vec3 by value; widen to long in the sum to avoid overflow. Concepts: value-type struct, member access. Hint: (long)a.x * b.x + ….
4. Intermediate — Exact fraction compare.
Define typedef struct { int num, den; } Frac; and implement int frac_cmp(Frac a, Frac b) returning -1, 0, or 1 using cross-multiplication (no floating point). Handle positive denominators. Test with 1/2 vs 1/3 (→ 1) and 2/4 vs 1/2 (→ 0). Concepts: value type, avoiding division and overflow. Hint: compare (long)a.num*b.den with (long)b.num*a.den; reject a zero denominator before comparing.
5. Challenge — Opaque stack (information hiding).
Design a tiny integer-stack module with an opaque handle. In stack.h declare only typedef struct stack Stack; plus prototypes: Stack *stack_new(void); void stack_push(Stack *, int); int stack_pop(Stack *); void stack_free(Stack *);. In stack.c define struct stack { int *data; int len, cap; }; and implement the functions with malloc/realloc/free. In main.c, push three values, pop them, and print — without ever touching the struct's fields.
Requirements: the caller must not be able to see the struct layout; every function checks for NULL and allocation failure; stack_free releases all memory. Concepts: opaque handle, information hiding, ownership, resource cleanup. Hint: the whole point is that main.c compiles knowing only Stack exists, not what it contains.
typedef gives an existing type a second name — an alias. It never creates a new, distinct type and costs nothing at runtime; the compiler treats the alias and the original as identical.
Most important syntax: typedef struct tag { members } Alias; defines a struct and its alias together. Keep the tag whenever the struct must point to its own type (linked lists, trees), because the alias name is not yet available inside the braces. The alias and the tag live in separate namespaces, so they may share a name.
Two clean uses: value types (small structs you copy and whose fields the caller reads) and opaque handles (the alias is public, the layout is hidden in a .c file so the library can change internals freely). The standard library's size_t, FILE, time_t, and int32_t are all typedefs — that is how they stay portable.
Common mistakes: using an alias before it is declared, forgetting the tag on a self-referential struct, expecting the compiler to keep two aliases of int apart (it will not), and — the big one — hiding a pointer behind an alias, which conceals ownership and lifetime. What to remember: use typedef to make intent clearer, never to make a pointer, a size, or an ownership rule invisible.