C Part I · Module 10 · Structures, Unions, Enums and User-Defined Types
typedef and Type Names
There is a reason a great deal of real C never contains the word struct at all. C provides a facility for giving any type a one-word name of your choosing, and most codebases use it everywhere, which is why the two-word spellings this module has insisted on will look unfamiliar the moment you read someone else's code. Used well it makes code read like the problem it solves. Used badly it is responsible for some of the least readable C ever written, and the worst case is not a matter of taste: hiding a pointer behind a name makes const silently qualify the wrong thing, and makes the qualification you actually wanted impossible to express. This chapter measures that, and then gives the rule that separates the two outcomes.
Intermediate18 min readCPart IFundamentalsTypes
Part I · Module 10 · Chapter 10.9 · typedef and Type Names
1. What typedef Does
typedef unsigned long ticks;
ticks elapsed = 0;typedef introduces another name for an existing type. That is all it does.
The syntax is easier to remember than it looks: write the declaration you would write for a variable of the type you want, put the new type's name where the variable's name would go, and prefix the whole thing with typedef.
unsigned long elapsed; /* a variable */
typedef unsigned long ticks; /* a type name, same shape */
int *p; /* a variable */
typedef int *int_ptr; /* a type name, same shape */
int a[4]; /* a variable */
typedef int Row[4]; /* a type name, same shape */That rule handles every case, including the awkward ones in §7 where the name ends up buried in the middle of the declaration.
2. It Is an Alias, Not a New Type
This is the first thing to be exact about, because the syntax suggests otherwise.
#include <stdio.h>
typedef int metres;
typedef int seconds;
static metres distance(void) { return 100; }
static seconds duration(void) { return 9; }
int main(void)
{
metres m = duration(); /* seconds into metres */
seconds s = distance(); /* and back again */
int n = m + s; /* arithmetic across the "types" */
printf("m=%d s=%d n=%d\n", m, s, n);
printf("sizeof(metres)=%zu sizeof(seconds)=%zu sizeof(int)=%zu\n",
sizeof(metres), sizeof(seconds), sizeof(int));
return 0;
}m=9 s=100 n=109
sizeof(metres)=4 sizeof(seconds)=4 sizeof(int)=4Nothing was reported at -Wall -Wextra -Wpedantic. Seconds went into a metres variable, metres into seconds, and the two were added together.
metresandsecondsare not two types. They are two spellings ofint, and the compiler sees through both. A typedef creates no new type, performs no conversion, and enables no checking.
This is the same disappointment Chapter 10.8 §3 delivered about enum, for the same reason: C's type system does not have a way to say "an integer, but a different kind of integer". If you need units that cannot be mixed, the only tool C gives you is a structure with one member — which is a real type, and which = and + will refuse to apply across.
So the value of a typedef is entirely in what a reader gets from it. Which means a typedef that tells the reader less than the spelled-out type is a straight loss, and that is §5's subject.
3. With struct, union and enum
typedef struct reading { int id; int value; } Reading;That declares the structure type struct reading and the alias Reading. Both names work afterwards, and they name the same type.
Two forms are common and they differ in one important way:
#include <stdio.h>
/* keeps the tag: struct node AND node both name the type */
typedef struct node {
int v;
struct node *next; /* the tag is available here; the typedef is not */
} node;
/* no tag: fine for a type that never refers to itself */
typedef struct { int x; int y; } point;
int main(void)
{
node c = { 3, NULL };
node b = { 2, &c };
node a = { 1, &b };
for (node *p = &a; p != NULL; p = p->next) { printf("%d ", p->v); }
printf("\n");
point q = { .x = 4, .y = 5 };
printf("point %d,%d sizeof node=%zu sizeof point=%zu\n",
q.x, q.y, sizeof(node), sizeof(point));
struct node also = { 9, NULL }; /* the tag still works */
printf("via the tag: %d\n", also.v);
return 0;
}1 2 3
point 4,5 sizeof node=16 sizeof point=8
via the tag: 9The tag is required for a self-referring member. The typedef name does not exist until the declaration finishes, so it cannot be used inside its own braces. Drop the tag and:
typedef struct { int v; node *next; } node;error: unknown type name 'node'Keep the tag. It costs one word, it is mandatory for any self-referring type (Chapter 10.3 §7), it gives you a second way to name the type when a header needs to declare it incomplete, and it means a reader who sees
nodecan findstruct nodeby searching.
Note also that the tag and the typedef name may be identical — typedef struct node { ... } node; — because tags live in a separate namespace from ordinary identifiers (Chapter 10.2 §2). That is why the idiom looks redundant and is not.
4. When a Type Name Helps
A typedef earns its place when the new name says more than the old one.
typedef struct reading Reading; /* says the same thing, shorter */
typedef unsigned long ticks; /* says MORE: this is a time count */The second is the useful kind. unsigned long tells the reader the representation; ticks tells them the meaning. A function declared ticks elapsed_since(ticks start); documents itself in a way unsigned long elapsed_since(unsigned long start); does not.
The standard library leans on this heavily and gives you the model: size_t is a size, ptrdiff_t is a pointer difference, time_t is a time, FILE is a stream. In each case the underlying type is deliberately unspecified because you are not supposed to care — the name is the interface.
Name the concept, not the representation.
ticks,byte_count,nodeare useful.uint,ulong,pcharare not — they re-spell what C already said, and the reader now has to learn your spelling as well as C's.
One naming caution: POSIX reserves identifiers ending in _t for the implementation. widget_t is squatting in a namespace that is not yours, and a future header may collide with it. Use widget, or a project prefix.
5. The Habit That Obscures Pointers
Here is the measurement that makes this a correctness question rather than a style one.
typedef struct thing *thing_ptr; /* the pointer is now invisible */A reader seeing thing_ptr p; has no way to know p is an address. Every rule from Module 7 and Module 9 applies to it — it can be null, it can dangle, it must be checked, somebody owns what it points at — and none of that is visible at the declaration.
That is the readability cost. The correctness cost is worse:
static void a(const thing_ptr p) { p->n = 1; }$ cc -std=c17 -Wall -Wextra -Wpedantic hidden.c -o hidden
$It compiles, runs, and writes through p. The spelled-out version does not:
static void a(const struct thing *p) { p->n = 1; }error: cannot assign to variable 'p' with const-qualified type
'const struct thing *'The same word in the same position means two different things.
const struct thing *pis a pointer to a const thing — Chapter 7.11's promise not to modify what it points at.const thing_ptr pis a const pointer to a thing — the pointer cannot be reassigned, which nobody wanted and which protects nothing.
The reason is that const qualifies the typedef'd type as a whole, and that type is "pointer to struct thing". The qualifier lands on the pointer.
And there is no way out from inside the typedef:
void a(thing_ptr const p); /* also a const pointer */Both spellings qualify the pointer. Pointer-to-const is not expressible through thing_ptr at all. To get it you must abandon the typedef and write const struct thing *.
6. The Same Problem, With Arrays
A typedef can hide an array just as thoroughly, and the consequence is a function that looks like it takes a copy and does not.
#include <stdio.h>
typedef int Row[4];
static void takes_row(Row r) /* looks like it takes an array */
{
printf(" inside: sizeof r = %zu\n", sizeof r);
r[0] = 99; /* and it can MODIFY the caller's */
}
int main(void)
{
Row a = { 1, 2, 3, 4 };
printf("in main: sizeof a = %zu\n", sizeof a);
takes_row(a);
printf("after the call, a[0] = %d\n", a[0]);
return 0;
}in main: sizeof a = 16
inside: sizeof r = 8
after the call, a[0] = 99Inside the function sizeof r is 8 — a pointer — and the assignment changed the caller's array. The signature void takes_row(Row r) gives no sign of either. Chapter 7.6's array-to-pointer conversion applies to a typedef'd array exactly as it does to a spelled-out one, and Chapter 10.6 §5 measured the same decay on an array of structures.
The compiler does flag the sizeof:
warning: sizeof on array function parameter will return size of 'int *'
instead of 'Row' (aka 'int[4]')But it says nothing about the misleading signature, because the signature is legal.
An array typedef is safe for declaring objects and misleading in a parameter list.
Row a;is fine and says what it means.void f(Row r)should be writtenvoid f(int *r, size_t n)— Chapter 7.7's pairing, which the typedef was quietly removing.
7. Where typedef Is Unambiguously Right
Function pointers. This is the case where the raw declaration is genuinely hard to read, and the typedef is not a convenience but a legibility repair. Compare:
#include <stdio.h>
struct reading { int id; int value; };
static int by_id(const struct reading *a, const struct reading *b) { return a->id - b->id; }
static int by_value(const struct reading *a, const struct reading *b) { return a->value - b->value; }
/* without a typedef: a function returning a pointer to a comparison function */
static int (*pick_raw(int want_id))(const struct reading *, const struct reading *)
{
return want_id ? by_id : by_value;
}
/* with a typedef: the same thing */
typedef int compare_fn(const struct reading *, const struct reading *);
static compare_fn *pick(int want_id)
{
return want_id ? by_id : by_value;
}
int main(void)
{
struct reading a = { 7, 70 }, b = { 3, 30 };
int (*f)(const struct reading *, const struct reading *) = pick_raw(1);
compare_fn *g = pick(0);
printf("by id : %d\n", f(&a, &b));
printf("by value : %d\n", g(&a, &b));
printf("same function picked twice ? %d\n", pick(1) == pick_raw(1));
return 0;
}by id : 4
by value : 40
same function picked twice ? 1static compare_fn *pick(int want_id) against static int (*pick_raw(int want_id))(const struct reading *, const struct reading *). They declare the same thing. The second is the kind of declaration people copy without reading.
Note the form used: typedef int compare_fn(...) names the function type, not the pointer, so the * stays visible at every use — compare_fn *g. That follows §5's rule and still removes all the hard parentheses. Typedefing the pointer (typedef int (*compare_fn)(...)) is also common, and it hides the * again; prefer the function type.
The standard library's own types. size_t, ptrdiff_t, time_t, FILE. Use them; do not re-typedef them.
A named incomplete type used as a handle. The §5 callout's connection form.
8. The Rule
Add a typedef when the new name says something the old one does not. Keep the
*and the[]visible.
Applied:
| Typedef | Verdict |
|---|---|
typedef unsigned long ticks; | yes — names a concept |
typedef struct reading Reading; | yes — shorter, hides nothing |
typedef struct connection connection; | yes — a named incomplete type |
typedef int compare_fn(const struct reading *, const struct reading *); | yes — repairs a genuinely unreadable declaration |
typedef struct thing *thing_ptr; | no — hides a pointer; breaks const |
typedef int Row[4]; for parameters | no — hides the decay |
typedef unsigned int uint; | no — re-spells C with no gain |
typedef int metres; expecting type safety | no — §2; use a one-member struct |
And one habit that costs nothing: when you read unfamiliar code and meet a typedef'd name, find its definition before reasoning about it. A name that might be a pointer, might be an array, and might be a 200-byte structure changes what every line around it means — which is, in the end, the whole argument of this chapter.
9. What You Should Be Able to Say Now
typedefintroduces another name for an existing type. Write the variable declaration, put the type name where the variable's name would go, prefixtypedef.- It creates no new type and enables no checking — measured, seconds assigned to metres with no warning at all.
- For unit safety the only tool C offers is a one-member structure, which is a real type.
typedef struct tag { ... } Name;declares both. Keep the tag: it is required for a self-referring member — without it, "unknown type name".- The tag and the typedef name may be identical, because tags are a separate namespace.
- Name the concept, not the representation.
ticksyes,uintno. Avoid a_tsuffix — POSIX reserves it. - Never hide a pointer. Measured:
const thing_ptr pcompiled and wrote throughp, whileconst struct thing *pwas rejected. - Pointer-to-const is unreachable through such a typedef — both
const thing_ptrandthing_ptr constqualify the pointer. - For a handle, typedef the struct and leave the
*at every use:connection *. - Array typedefs hide the decay — measured,
sizeof16 outside and 8 inside, and the function changed the caller's array. - Function pointers are where typedef clearly wins; prefer naming the function type so the
*stays visible. - When reading unfamiliar code, look up every typedef before reasoning about the lines that use it.
10. Exercises
Exercise 1 — write the typedefs. Using §1's rule.
- a name for
unsigned shortmeaning a port number - a name for
struct packet - a name for a function taking two
ints and returningint - a name for an incomplete type
struct session, usable as a handle
Exercise 2 — predict the diagnostics. For each, say whether it compiles and what const qualifies.
typedef struct thing *tp;
void f(const tp p) { p->n = 1; }
void g(tp const p) { p->n = 1; }
void h(const struct thing *p) { p->n = 1; }
void i(struct thing *const p) { p->n = 1; }Exercise 3 — fix the units. typedef int metres; and typedef int seconds; give no protection. Write a version that does, say what you give up, and name the two operators that stop working.
Exercise 4 — restore the tag. This does not compile. Say why, in terms of when the typedef name comes into existence, and give the fix.
typedef struct { int v; node *next; } node;Exercise 5 — judge eight typedefs. For each, say keep or remove, and why.
typedef unsigned char byte;
typedef struct frame *frame_ref;
typedef int (*handler)(int);
typedef struct point Point;
typedef char string[64];
typedef unsigned long ulong;
typedef struct device device;
typedef double seconds;Exercise 6 — the misleading signature. Explain, using §6's measurements, why void f(Row r) is a worse signature than void f(int *r, size_t n) even though both compile and both work.
Exercise 7 — rewrite the declaration. Give the typedef that makes this readable, then declare a variable of the type and a function returning it.
void (*signal_handler(int, void (*)(int)))(int);Exercise 8 — the handle. Design a two-function interface over a connection handle such that a caller cannot see inside it, cannot create one directly, and can still write const connection * meaningfully. Say which chapter's rule your * placement follows.
11. Summary
typedefis an alias, nothing more — measured, no new type and no checking at all.- Unit safety needs a one-member struct; a typedef cannot give it.
- Keep the tag — mandatory for self-reference, and the tag namespace lets the two names match.
- Name the concept, not the representation; avoid the reserved
_tsuffix. - Never hide a pointer:
const thing_ptrcompiled and wrote through the pointer;const struct thing *was rejected, and pointer-to-const is unreachable through the alias. - Handles: typedef the struct, keep the
*. - Array typedefs hide the decay — 16 bytes outside, 8 inside, and the caller's array was modified.
- Function pointers are the clear win; name the function type so the
*stays visible. - Look up every typedef in code you did not write.
One construct in this module's title has still not appeared. C lets a structure member be declared with a width in bits rather than a type's natural size, so that several small fields share a single integer — a flag here, a three-bit counter there, a two-bit mode — packed together instead of each taking a whole byte or more. It looks like exactly the tool for describing something whose layout someone else specified, and that appearance is why the final chapter of this module exists: the standard leaves so much about these fields unspecified that two conforming compilers can place the same declaration differently, and the list of what is left open is longer than the list of what is fixed. The next chapter sets out that list precisely, shows what the fields do give you, and explains why nothing in Part II of this curriculum uses them to describe a hardware register.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
