Structs & Unions

Course 2 · Ch 1
Structs & Unions
Composite data — and the one place C quietly stops tracking what's actually there

Course 1 covered scalars, arrays, and the pointers connecting them. Course 2 opens with C's way of grouping several values into one — and, in the same chapter, its way of letting several values share one.

Declaring & Using Structs

struct Point { int x; int y; }; struct Point p = {3, 4}; printf("%d, %d\n", p.x, p.y);

typedef

Referring to struct Point everywhere gets verbose fast. typedef gives the type a shorter name — an idiom so common in real C that it's effectively the default style.

typedef struct { int x; int y; } Point; Point p = {3, 4}; // no "struct" keyword needed anymore

Rust never needed this ceremony — a struct's own name is always the type, directly, with no separate aliasing step required.

Structs Are Pure Data

A C struct has no methods attached to it — no impl block equivalent from Rust. Any function operating on a struct is written separately, typically taking a pointer to it as a parameter. Course 2's own Function Pointers chapter (Ch.7) later shows one way C approximates method-like dispatch without a language feature for it at all.

Unions

A union looks like a struct syntactically, but its members share the same memory — only one is genuinely "active" at a time. A union's sizeof equals its largest member's size, not the sum of all members, unlike a struct.

union Value { int as_int; float as_float; }; union Value v; v.as_int = 42; // v.as_float now reads the SAME bytes, reinterpreted as a float — not 42.0

The Danger of Type Punning via Unions

Reading a union member that wasn't the one last written is, in most cases, undefined behavior — the union itself carries no record of which member is currently valid. Compare this directly to Rust's own data-carrying enums (rust1-6): a Rust enum stores a hidden discriminant tracking which variant is actually active, so accessing the wrong one is caught as a compile-time type error through match, not silently allowed at runtime. A C union tracks nothing at all — the programmer is expected to remember.

ConceptRust enumC union
Tracks which variant/member is activeyes — a hidden discriminant, checked by matchno — nothing tracks it at all
Reading the "wrong" onenot possible — match forces handling every caseundefined behavior in most cases
Sizelargest variant + discriminant tagexactly the largest member — no tag
typedef struct is the idiomatic default
Combining an anonymous struct definition with a typedef in one statement is extremely common in real C code — expect to see it constantly when reading existing codebases.
Reading the wrong union member isn't "reading garbage" — it's UB
This is genuinely stronger than "you'll get a nonsense value." Because it's undefined behavior, the compiler is free to assume it never happens at all, and can optimize the surrounding code in ways that produce surprising results well beyond just a wrong number — exactly the same class of danger as Course 1's signed-overflow warning.

Coding Challenges

Challenge 1

Define a Point struct with x and y int fields using typedef struct, create an instance with values 5 and 10, and print both fields.

📄 View solution
Challenge 2

Define a union with an int member and a float member. Write 100 into the int member, then print both the int member and the float member. Explain why they don't show a related value.

📄 View solution
Challenge 3

Explain how Rust's enum discriminant makes reading the "wrong" variant impossible at compile time, while a C union has no equivalent safeguard at all — and why this makes the union version genuinely undefined behavior rather than just an inconvenience.

📄 View solution

Chapter 1 Quick Reference

  • struct groups multiple values; access members with .
  • typedef struct { ... } Name; — the idiomatic default, avoiding repeated struct keywords
  • C structs carry no methods — functions operating on them are always separate, taking a pointer
  • union members share memorysizeof is the largest member's size, not the sum
  • Reading the wrong union member is undefined behavior — no discriminant tracks which one is active, unlike Rust's enums
  • Next chapter: dynamic memory — malloc/calloc/realloc/free, and C's own version of Rust's ownership model