Structs and unions are how C lets you group multiple values into a single unit. C has no classes, no objects in the OOP sense - structs (and, more rarely, unions) are the foundation almost everything else in "real-world" C data modeling is built on: linked lists, trees, network packets, hardware registers, even simple polymorphism.
This guide is split into two parts: Part 1 covers structs (sections 1.1–1.18), Part 2 covers unions (sections 2.1–2.6), and a short bridging comparison sits at the start of Part 2 since it needs both to make sense.
0. Why they're needed, why industry leans on them, and why your syllabus barely touches half of this
Why they're needed at all
A single int or char can only hold one value. The moment you need to
represent something with multiple related pieces of data - a point with
an x and y, a student with a name and a GPA, a network packet with a header
and a payload - you need a way to bundle those pieces together and treat
them as one thing. That's exactly what a struct gives you: a custom,
named grouping of variables (called members or fields) that travel
together as a single type.
A union solves a different problem: sometimes you want one piece of
memory to be interpreted as different types at different times - reading
raw bytes as either a float or an int, for example - without wasting
space storing both.
Why real projects lean on them so heavily
- Modeling real-world data. Every struct or record in any language -
a
Point, aEmployee, aPacket- starts life in C as astruct. Databases, file formats, and network protocols are usually defined in terms of C structs, because C struct layout maps directly onto raw memory. - Data structures. Linked lists, trees, graphs, hash tables - all of these are built from self-referential structs (a struct containing a pointer to another struct of the same type). You cannot build any non-trivial data structure in C without them.
- Hardware and protocol work. Device drivers, embedded firmware, and
network code use structs (often with
#pragma packor bit-fields) to overlay a C type directly onto a fixed byte layout - a hardware register, a TCP header, a file format - so that reading/writing the struct is reading/writing the real bytes. - Fast, tagless polymorphism. Unions plus an enum "tag" (section 2.3)
are how C fakes what other languages get natively from generics or sum
types - a value that can be one of several different types, common in
parsers, interpreters, and variant/
Any-style APIs. - Simulating objects. A struct holding data plus function pointers
(section 1.17) is how C code fakes methods and virtual dispatch - this
is literally how the Linux kernel implements things like filesystem
drivers (
struct file_operations).
Why your syllabus (probably) stopped early
Most BSc courses cover "declare a struct, access .member, maybe use
-> with a pointer" and stop there - for similar reasons to the macro
gap:
- Memory layout topics (padding, alignment,
#pragma pack) require understanding the memory model first, which many intro courses intentionally gloss over so students aren't fighting two hard concepts (structs and memory layout) at once. - Unions are taught minimally because they're inherently unsafe - write one member, read a different one, and you get implementation-defined or undefined behavior. It's an easy topic to skip when time is short and it's also easy to write a "wrong" exam question around.
- The advanced patterns (flexible array members, tagged unions,
function-pointer "vtables",
offsetof/container_of) come from systems programming experience, not from a first course - the same way the advanced macro tricks did. Nobody invents these in a vacuum; you see them in a real codebase and learn why they exist.
And yes - this is another one of those "quietly very cool" corners of C.
Once you see a tagged union acting like a mini type-safe variant, or a
struct overlaying raw bytes off the network exactly onto named fields, C
starts to feel a lot less limited than it's given credit for.
Part 1: Structs
1.1 Declaring and using a basic struct
A struct groups named variables (members) into one type.
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point p;
p.x = 3;
p.y = 4;
printf("(%d, %d)\n", p.x, p.y);
return 0;
}
Note that in plain C (unlike C++), you must write struct Point, not just
Point, when declaring a variable - unless you use typedef (section 1.3).
1.2 Initializing structs
You can initialize members at declaration time, either positionally or with designated initializers (which are clearer and don't depend on member order).
struct Point {
int x;
int y;
};
int main(void) {
struct Point a = {3, 4}; // positional: x=3, y=4
struct Point b = {.y = 10, .x = 5}; // designated: order doesn't matter
struct Point c = {0}; // zero-initializes ALL members
printf("%d %d\n", a.x, a.y);
printf("%d %d\n", b.x, b.y);
printf("%d %d\n", c.x, c.y); // 0 0
return 0;
}
{0} is the standard idiom for zero-initializing an entire struct,
regardless of how many members it has.
1.3 typedef with structs
typedef lets you drop the struct keyword when declaring variables -
almost universal in real-world C.
typedef struct {
int x;
int y;
} Point;
/* Equivalent, and needed if the struct must also refer to its own name
(see section 1.13, self-referential structs): */
typedef struct Point2 {
int x;
int y;
} Point2;
int main(void) {
Point p = {1, 2}; // no "struct" keyword needed
Point2 q = {3, 4}; // works the same way
printf("%d %d | %d %d\n", p.x, p.y, q.x, q.y);
return 0;
}
1.4 Nested structs
A struct can contain another struct as a member.
typedef struct {
int x;
int y;
} Point;
typedef struct {
Point topLeft;
Point bottomRight;
} Rectangle;
int main(void) {
Rectangle r = { .topLeft = {0, 0}, .bottomRight = {100, 50} };
printf("width = %d, height = %d\n",
r.bottomRight.x - r.topLeft.x,
r.bottomRight.y - r.topLeft.y);
return 0;
}
1.5 Arrays of structs
Structs work naturally as array elements - one of the most common patterns in real code (a table of records, a list of entities, etc.).
typedef struct {
char name[32];
int score;
} Student;
int main(void) {
Student class[3] = {
{"Alice", 90},
{"Bob", 75},
{"Cara", 88},
};
for (int i = 0; i < 3; i++) {
printf("%s: %d\n", class[i].name, class[i].score);
}
return 0;
}
1.6 Pointers to structs and the -> operator
Accessing members through a pointer uses -> instead of . - this is
just shorthand for (*ptr).member.
typedef struct {
int x;
int y;
} Point;
int main(void) {
Point p = {3, 4};
Point *ptr = &p;
printf("%d\n", (*ptr).x); // works, but clunky
printf("%d\n", ptr->x); // preferred: identical meaning
ptr->x = 100; // modifies the original p through the pointer
printf("%d\n", p.x); // prints 100
return 0;
}
1.7 Passing structs to functions: by value vs. by pointer
Passing a struct by value copies the entire thing - fine for small structs, wasteful (and unable to modify the original) for large ones. Passing by pointer avoids the copy and lets the function modify the caller's data.
typedef struct {
int x;
int y;
} Point;
/* By value: gets a COPY. Changes here do not affect the caller's struct. */
void tryToMove(Point p) {
p.x += 10;
}
/* By pointer: modifies the caller's actual struct. */
void move(Point *p, int dx, int dy) {
p->x += dx;
p->y += dy;
}
int main(void) {
Point a = {0, 0};
tryToMove(a);
printf("%d %d\n", a.x, a.y); // still 0 0 -- unaffected
move(&a, 10, 5);
printf("%d %d\n", a.x, a.y); // 10 5 -- actually changed
return 0;
}
Rule of thumb: pass small structs (a couple of ints) by value if you
don't need to modify them; pass anything larger, or anything you need to
modify, by pointer.
1.8 Struct padding and alignment
The compiler is free to insert unused padding bytes between struct members
so that each member sits at a memory address that's a multiple of its own
alignment requirement (usually its size, for basic types). This means
sizeof(struct) is often larger than the sum of its members' sizes.
#include <stdio.h>
struct Bad {
char a; // 1 byte
int b; // 4 bytes -- needs 4-byte alignment
char c; // 1 byte
}; // likely sizeof == 12 (1 + 3 padding + 4 + 1 + 3 padding)
struct Good {
int b; // 4 bytes
char a; // 1 byte
char c; // 1 byte
}; // likely sizeof == 8 (4 + 1 + 1 + 2 padding)
int main(void) {
printf("sizeof(struct Bad) = %zu\n", sizeof(struct Bad)); // e.g. 12
printf("sizeof(struct Good) = %zu\n", sizeof(struct Good)); // e.g. 8
return 0;
}
Ordering members from largest to smallest generally minimizes padding. This matters a lot when you have large arrays of structs, or when a struct's exact byte layout needs to match an external format (see section 1.14).
1.9 Bit-fields
You can tell the compiler to pack a member into a specific number of bits, useful for flags or matching a hardware/protocol layout that's specified in bits rather than bytes.
#include <stdio.h>
struct Flags {
unsigned int isReadable : 1;
unsigned int isWritable : 1;
unsigned int isExecutable : 1;
unsigned int permissionLevel : 4; // 0-15
};
int main(void) {
struct Flags f = {0};
f.isReadable = 1;
f.isWritable = 1;
f.permissionLevel = 7;
printf("r=%u w=%u x=%u level=%u\n",
f.isReadable, f.isWritable, f.isExecutable, f.permissionLevel);
printf("sizeof = %zu\n", sizeof(f)); // likely 4, not 4 separate ints' worth
return 0;
}
Caveats: the exact memory layout of bit-fields (bit order, whether they cross byte boundaries) is implementation-defined, so they're not portable for cross-platform binary formats - for that, use explicit masking and shifting instead.
1.10 Flexible array members
A struct's last member can be declared as an array with no size,
allowing a single malloc to allocate the struct together with a
variable-length trailing buffer, avoiding a second allocation and an
extra pointer indirection.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int length;
char data[]; // flexible array member -- must be last, no size given
} Buffer;
int main(void) {
const char *text = "hello";
size_t len = strlen(text);
/* Allocate the struct PLUS len+1 bytes for data, in one allocation. */
Buffer *buf = malloc(sizeof(Buffer) + len + 1);
buf->length = (int)len;
memcpy(buf->data, text, len + 1);
printf("length=%d data=%s\n", buf->length, buf->data);
free(buf);
return 0;
}
sizeof(Buffer) does not include the flexible array member's
contents - only the fixed part of the struct.
1.11 Comparing structs - why == doesn't work, and the padding trap
C has no built-in structural equality for structs; == on struct values
isn't even legal syntax. You either compare members manually, or use
memcmp - but memcmp has a subtle trap involving padding bytes.
#include <stdio.h>
#include <string.h>
struct Point {
int x;
int y;
};
int pointsEqual(struct Point a, struct Point b) {
return a.x == b.x && a.y == b.y; // safe: compares real values only
}
struct Padded {
char a;
int b; // padding bytes exist between a and b, with indeterminate content
};
int main(void) {
struct Point p1 = {1, 2}, p2 = {1, 2};
// if (p1 == p2) ... // COMPILE ERROR -- not legal C
printf("%d\n", pointsEqual(p1, p2)); // 1 (correct way)
struct Padded x = {0}, y = {0};
x.a = 'A'; x.b = 5;
y.a = 'A'; y.b = 5;
/* memcmp COULD report these as different, because the padding bytes
between 'a' and 'b' are not guaranteed to be zero or consistent --
only the member VALUES are guaranteed equal here, not every byte. */
printf("%d\n", memcmp(&x, &y, sizeof(x)) == 0); // not guaranteed to be 1!
return 0;
}
Prefer writing an explicit member-by-member comparison function; only use
memcmp on structs you've deliberately zero-initialized with {0} first
(which at least makes the padding consistent between two separately
zeroed instances) or on structs you've confirmed have no padding.
1.12 Copying structs
Plain assignment (=) performs a full member-by-member copy of a struct -
no special syntax needed, unlike arrays (which decay to pointers and can't
be assigned directly).
#include <stdio.h>
#include <string.h>
typedef struct {
int id;
char name[32];
} Record;
int main(void) {
Record a = {1, "Alice"};
Record b = a; // full copy -- a and b are now independent
strcpy(b.name, "Bob");
printf("%s %s\n", a.name, b.name); // Alice Bob -- a untouched
Record c;
memcpy(&c, &a, sizeof(Record)); // equivalent manual copy
printf("%s\n", c.name); // Alice
return 0;
}
Caution: if a struct contains a pointer (e.g. to heap-allocated memory),
= only copies the pointer value, not what it points to - this is a
"shallow copy" and both structs will point at the same memory.
1.13 Self-referential structs - linked lists and trees
A struct can contain a pointer to its own type (but not a full instance of its own type - that would be an infinitely-sized structure). This is the basis of every linked data structure in C.
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int value;
struct Node *next; // pointer to the SAME struct type -- this is fine,
// because a pointer has a fixed size regardless
// of what it points to
} Node;
int main(void) {
Node *head = malloc(sizeof(Node));
head->value = 1;
head->next = malloc(sizeof(Node));
head->next->value = 2;
head->next->next = NULL;
for (Node *cur = head; cur != NULL; cur = cur->next) {
printf("%d ", cur->value);
}
printf("\n");
free(head->next);
free(head);
return 0;
}
1.14 Controlling layout: #pragma pack and alignas
When a struct's byte layout must exactly match an external format (a file format, a network packet, a hardware register map), you need to override the compiler's default padding behavior.
#include <stdio.h>
#include <stdalign.h>
/* Force NO padding between members -- every byte matches the wire format
exactly. Non-standard but supported by GCC, Clang, and MSVC. */
#pragma pack(push, 1)
struct PacketHeader {
unsigned char version;
unsigned short length;
unsigned int checksum;
};
#pragma pack(pop)
/* Standard C11 way to force a specific alignment, e.g. for SIMD or
cache-line alignment. */
struct alignas(16) AlignedBuffer {
float data[4];
};
int main(void) {
printf("packed sizeof = %zu (would be larger with default padding)\n",
sizeof(struct PacketHeader));
printf("alignof(AlignedBuffer) = %zu\n", alignof(struct AlignedBuffer));
return 0;
}
#pragma pack is compiler-specific syntax (though very widely supported);
_Alignas/alignas (via <stdalign.h>) is the standard C11 way to
request a minimum alignment.
1.15 offsetof and container_of
offsetof (from <stddef.h>) computes a member's byte offset within a
struct. container_of (built on offsetof) recovers a pointer to the
whole struct from a pointer to one of its members - this is exactly how
intrusive linked lists (used throughout the Linux kernel) work, avoiding a
separate allocation for each list node.
#include <stdio.h>
#include <stddef.h>
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))
typedef struct {
int x, y;
} Position;
typedef struct {
char name[32];
Position pos; // embedded, not a pointer
} Entity;
int main(void) {
Entity e = { .name = "Player", .pos = {5, 10} };
Position *posPtr = &e.pos; // imagine this is all some other code has
/* Recover the owning Entity* from just the Position* */
Entity *owner = container_of(posPtr, Entity, pos);
printf("owner name = %s\n", owner->name);
printf("offsetof(Entity, pos) = %zu\n", offsetof(Entity, pos));
return 0;
}
1.16 const with structs and struct members
const can apply to an entire struct instance, or to individual members,
with different implications for what you can do with it afterward.
#include <stdio.h>
typedef struct {
int id;
const char *name; // the chars pointed to are read-only through
// this pointer; the pointer itself is not const
} Record;
int main(void) {
const Record r = {1, "Alice"}; // the whole struct is read-only
// r.id = 2; // COMPILE ERROR -- r is const
Record s = {2, "Bob"};
// s.name[0] = 'X'; // COMPILE ERROR -- name points to const char
s.name = "Charlie"; // OK -- reassigning the pointer itself is fine
printf("%d %s\n", s.id, s.name);
return 0;
}
Passing const struct T * to functions is the standard way to say "this
function reads the struct but won't modify it" without paying for a copy.
1.17 Function pointers in structs - simulating methods and vtables
A struct can hold function pointers, letting different instances carry different behavior - this is literally how C fakes object-oriented polymorphism (and is exactly what the Linux kernel's driver interfaces are built from). This example also uses a tagged union (introduced properly in section 2.3) to hold each shape's specific data.
#include <stdio.h>
typedef struct Shape {
float (*area)(const struct Shape *self);
union {
struct { float radius; } circle;
struct { float w, h; } rectangle;
};
} Shape;
float circleArea(const Shape *self) {
return 3.14159f * self->circle.radius * self->circle.radius;
}
float rectangleArea(const Shape *self) {
return self->rectangle.w * self->rectangle.h;
}
int main(void) {
Shape c = { .area = circleArea, .circle = {.radius = 2.0f} };
Shape r = { .area = rectangleArea, .rectangle = {.w = 3.0f, .h = 4.0f} };
Shape *shapes[2] = {&c, &r};
for (int i = 0; i < 2; i++) {
/* Calling through the function pointer -- this IS virtual dispatch */
printf("area = %f\n", shapes[i]->area(shapes[i]));
}
return 0;
}
1.18 Compound literals - anonymous struct values inline
C99 compound literals let you create a temporary, unnamed struct value inline - useful for passing a one-off struct to a function without a separate named variable.
#include <stdio.h>
typedef struct {
int x, y;
} Point;
void printPoint(Point p) {
printf("(%d, %d)\n", p.x, p.y);
}
int main(void) {
printPoint((Point){.x = 3, .y = 4}); // compound literal, no named variable needed
Point *p = &(Point){10, 20}; // even a pointer to a compound literal works
printf("%d %d\n", p->x, p->y);
return 0;
}
Part 2: Unions
2.0 Struct vs. union - side by side
Before diving into union-specific features, it helps to see the core difference laid out directly, since unions only make sense in contrast to the structs from Part 1.
#include <stdio.h>
struct S {
int i;
float f;
char c;
};
union U {
int i;
float f;
char c;
};
int main(void) {
printf("struct: each member has its own space -> sizeof = %zu\n", sizeof(struct S));
printf("union: all members share space -> sizeof = %zu\n", sizeof(union U));
struct S s = {1, 2.0f, 'a'};
printf("struct: s.i=%d s.f=%f s.c=%c (all valid at once)\n", s.i, s.f, s.c);
union U u;
u.i = 1;
printf("union: u.i=%d (only the most recently written member is valid)\n", u.i);
return 0;
}
struct | union | |
|---|---|---|
| Memory | each member gets its own space | all members share the same space |
| Size | sum of members (plus padding) | size of the largest member |
| Valid members | all members valid simultaneously | only the last-written member is valid |
| Use case | grouping unrelated data together | one value, multiple possible interpretations |
2.1 Declaring and using a basic union
A union looks just like a struct syntactically, but all members share
the same memory - the union's size is the size of its largest member,
not the sum of all of them.
#include <stdio.h>
union Value {
int i;
float f;
char c;
};
int main(void) {
union Value v;
v.i = 65;
printf("as int: %d\n", v.i); // 65
v.f = 3.14f;
printf("as float: %f\n", v.f); // 3.14
// v.i is now garbage -- writing v.f overwrote the same bytes v.i used
printf("sizeof(union Value) = %zu\n", sizeof(union Value)); // size of float (4), not int+float+char
return 0;
}
2.2 Union memory layout and type punning
Because all members overlap the same bytes, writing one member and reading a different one lets you reinterpret the same bits as a different type - called type punning. This is technically undefined behavior in strict ISO C (though GCC/Clang explicitly support it as a common extension), so treat it as "works in practice on mainstream compilers," not standard guaranteed behavior.
#include <stdio.h>
union FloatBits {
float f;
unsigned int bits;
};
int main(void) {
union FloatBits fb;
fb.f = 1.0f;
/* Reading .bits after writing .f reinterprets the same 4 bytes as
an unsigned int -- showing the raw IEEE-754 bit pattern of 1.0f. */
printf("1.0f as raw bits: 0x%08X\n", fb.bits);
return 0;
}
The portable, standard-compliant way to do the equivalent reinterpretation
is memcpy between same-sized types, which compilers optimize down to the
same instructions anyway.
2.3 Tagged unions - C's stand-in for variant/sum types
A union alone doesn't know which member was last written. The standard
pattern is to pair it with an enum "tag" so you always know how to
interpret the union - this is how C fakes the tagged unions/sum types
that languages like Rust or Haskell have natively.
#include <stdio.h>
typedef enum { SHAPE_CIRCLE, SHAPE_RECTANGLE } ShapeType;
typedef struct {
ShapeType type;
union {
struct { float radius; } circle;
struct { float width, height; } rectangle;
};
} Shape;
float area(const Shape *s) {
switch (s->type) {
case SHAPE_CIRCLE:
return 3.14159f * s->circle.radius * s->circle.radius;
case SHAPE_RECTANGLE:
return s->rectangle.width * s->rectangle.height;
}
return 0.0f;
}
int main(void) {
Shape shapes[2] = {
{ .type = SHAPE_CIRCLE, .circle = {.radius = 2.0f} },
{ .type = SHAPE_RECTANGLE, .rectangle = {.width = 3.0f, .height = 4.0f} },
};
for (int i = 0; i < 2; i++) {
printf("shape %d area = %f\n", i, area(&shapes[i]));
}
return 0;
}
This costs sizeof(largest variant) + sizeof(tag) instead of
sizeof(every variant added together) - the whole point of using a union
instead of just putting all the fields directly in the struct.
2.4 Anonymous unions (C11)
C11 allows a union member with no name, whose members are then accessed as
if they belonged directly to the containing struct - useful for things
like a vector type that can be accessed both as .x/.y/.z and as an
array, without an extra name in between.
#include <stdio.h>
typedef struct {
union {
struct { float x, y, z; }; // anonymous struct inside anonymous union
float coords[3];
};
} Vector3;
int main(void) {
Vector3 v;
v.x = 1.0f;
v.y = 2.0f;
v.z = 3.0f;
printf("%f %f %f\n", v.coords[0], v.coords[1], v.coords[2]);
return 0;
}
This is also what made the tagged union in section 2.3 readable as
s->circle.radius instead of s->data.circle.radius - the union member
itself had no name.
2.5 Unions for parsing raw bytes (protocols and file formats)
A very common real-world pattern: overlay a type directly onto a raw byte
buffer received from a network socket or file, letting you read fields by
name instead of manually indexing into a byte array. The safe way to do
this is a packed struct plus memcpy (rather than a union or a raw
pointer cast), since it avoids both alignment-fault risk on strict CPUs
and strict-aliasing undefined behavior.
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#pragma pack(push, 1)
typedef struct {
uint8_t version;
uint16_t payloadLength; // stored big-endian on the wire, for example
} PacketHeader;
#pragma pack(pop)
int main(void) {
/* Simulate 3 raw bytes received from a socket: version=1, length=256 */
unsigned char rawBytes[3] = {0x01, 0x01, 0x00};
PacketHeader header;
memcpy(&header, rawBytes, sizeof(header)); // safe way to reinterpret raw bytes
printf("version = %u\n", header.version);
printf("payloadLength (raw, endianness not yet handled) = %u\n",
header.payloadLength);
return 0;
}
If you do want to use an actual union for this (some codebases do, for
convenience, accepting the type-punning caveat from section 2.2), it looks
like this instead:
#include <stdio.h>
#include <stdint.h>
#pragma pack(push, 1)
typedef union {
uint8_t raw[3];
struct {
uint8_t version;
uint16_t payloadLength;
} fields;
} PacketUnion;
#pragma pack(pop)
int main(void) {
PacketUnion p;
p.raw[0] = 0x01;
p.raw[1] = 0x01;
p.raw[2] = 0x00;
/* Reading .fields after writing .raw is type punning -- works on
GCC/Clang in practice, but is the union-based (less portable)
alternative to the memcpy approach above. */
printf("version = %u\n", p.fields.version);
return 0;
}
2.6 Common pitfalls checklist
Forgetting
structbefore the tag name in plain C when not usingtypedef(section 1.3) -Point p;fails,struct Point p;works.Assuming
sizeof(struct)equals the sum of member sizes - padding (section 1.8) almost always makes it larger; always measure, don't assume.Comparing structs with
memcmpwithout zero-initializing first - padding bytes are indeterminate and can make two "equal" structs compare unequal (section 1.11).Shallow-copying a struct that contains pointers -
a = bcopies the pointer value, not the pointed-to data; both structs now alias the same memory (section 1.12).Forgetting a union only holds one valid member at a time - reading a member you didn't just write is either type punning (works in practice on GCC/Clang, but not strictly standard) or outright garbage (section 2.2).
Casting a raw buffer pointer directly to a struct/union pointer to parse network/file data, instead of
memcpy-ing into it - risks alignment faults and strict-aliasing undefined behavior (section 2.5).Assuming bit-field layout is portable across compilers/platforms - it's implementation-defined; don't use bit-fields for a format that must match byte-for-byte across different compilers (section 1.9).
Putting a flexible array member anywhere but last, or including one in a struct that's itself embedded (not pointed to) inside another struct - both are illegal (section 1.10).
Quick reference
Structs (Part 1)
| Feature | Syntax | Purpose | Section |
|---|---|---|---|
| Struct declaration | struct Name { ... }; | Group related variables into one type | 1.1 |
| typedef struct | typedef struct { ... } Name; | Drop the struct keyword at use sites | 1.3 |
| Member access | .member | Access a member of a struct value | 1.1 |
| Pointer member access | ->member | Access a member through a pointer | 1.6 |
| Designated initializer | {.field = value} | Initialize specific members by name | 1.2 |
| Zero-init | {0} | Zero every member of a struct | 1.2 |
| Nested struct | struct containing another struct | Compose larger records from smaller ones | 1.4 |
| Array of structs | Type arr[N] | A table/list of records | 1.5 |
| By-value parameter | void f(Type t) | Function gets a full copy | 1.7 |
| By-pointer parameter | void f(Type *t) | Function can modify caller's data, no copy | 1.7 |
| Padding | (implicit) | Compiler-inserted bytes for member alignment | 1.8 |
| Bit-field | unsigned int flag : 1; | Pack a member into a specific number of bits | 1.9 |
| Flexible array member | Type data[]; (last member) | Variable-length trailing data in one allocation | 1.10 |
#pragma pack | #pragma pack(push, 1) | Force a specific (often zero) byte alignment | 1.14 |
alignas | struct alignas(16) T { ... }; | Standard way to force a minimum alignment (C11) | 1.14 |
offsetof | offsetof(Type, member) | Byte offset of a member within a struct | 1.15 |
container_of | macro built on offsetof | Recover owning struct pointer from a member pointer | 1.15 |
| Compound literal | (Type){...} | Anonymous, inline struct value | 1.18 |
| Function pointer member | RetType (*fn)(Args) | Simulate methods / virtual dispatch | 1.17 |
Unions (Part 2)
| Feature | Syntax | Purpose | Section |
|---|---|---|---|
| Union declaration | union Name { ... }; | Overlap several types on the same memory | 2.1 |
| Type punning | write one member, read another | Reinterpret the same bytes as a different type | 2.2 |
| Tagged union | enum tag + union | C's stand-in for a type-safe variant/sum type | 2.3 |
| Anonymous union | unnamed nested union | Access nested members without an extra name (C11) | 2.4 |
| Raw byte parsing | packed struct/union + memcpy | Overlay a type onto a fixed external byte layout | 2.5 |
Coverage note
This guide covers struct and union declaration, initialization, memory layout, alignment and packing, bit-fields, self-referential and nested structures, type punning, tagged unions, and function-pointer-based polymorphism - the full range of what you'll encounter in real-world systems, embedded, and application C code. If you've read all of Part 1 and Part 2, you have everything needed to design, lay out, and debug any struct or union you'll run into.
Macros in C
Pointers in C