|$ curl https://forge-ai.dev/api/markdown?path=docs/c/structures
$cat docs/c-—-structures,-unions-&-enums.md
updated Recently·45 min read·published

C — Structures, Unions & Enums

CStructuresUnionsEnumsIntermediateIntermediate🎯Free Tools
Introduction

Structures, unions, and enums are the primary tools for creating custom types in C. Together they let you model real-world data — records, variant types, and named constants — without leaving the language. This page covers declaration, initialization, memory layout, and practical patterns for each.

Struct Declaration & Initialization

A struct groups variables of different types under a single name. Each member occupies its own storage. You declare a struct type and then create instances of it:

point.c
C
1struct Point {
2 double x;
3 double y;
4};
5
6int main(void) {
7 struct Point p1 = { 3.0, 4.0 }; /* positional init */
8 struct Point p2 = { .x = 1.0, .y = 2.0 }; /* designated init (C99) */
9 struct Point p3 = { 5.0 }; /* y defaults to 0.0 */
10 struct Point p4 = { 0 }; /* both members zeroed */
11
12 printf("p1 = (%.1f, %.1f)\n", p1.x, p1.y);
13 printf("p2 = (%.1f, %.1f)\n", p2.x, p2.y);
14 return 0;
15}

info

Designated initializers (C99) let you initialize members in any order and skip trailing members. Unspecified members are zero-initialized.
Accessing Members: The Dot Operator

The dot operator (.) accesses individual members of a struct. Members can be read and written like any other variable:

employee.c
C
1struct Employee {
2 char name[64];
3 int id;
4 double salary;
5};
6
7void print_employee(const struct Employee *e) {
8 printf("ID: %d | Name: %s | Salary: $%.2f\n",
9 e->id, e->name, e->salary);
10}
11
12int main(void) {
13 struct Employee emp;
14
15 strcpy(emp.name, "Alice Chen");
16 emp.id = 1042;
17 emp.salary = 95000.00;
18
19 print_employee(&emp);
20 return 0;
21}
sizeof, Padding & Alignment

sizeof returns the total size of a struct including padding inserted by the compiler for alignment. Understanding padding is critical for binary I/O, networking, and embedded systems:

alignment.c
C
1#include <stdio.h>
2
3struct Bad {
4 char a; /* 1 byte + 3 padding */
5 int b; /* 4 bytes */
6 char c; /* 1 byte + 3 padding */
7};
8
9struct Good {
10 int b; /* 4 bytes */
11 char a; /* 1 byte */
12 char c; /* 1 byte + 2 padding */
13};
14
15struct Packed {
16 char a;
17 int b;
18 char c;
19} __attribute__((packed));
20
21int main(void) {
22 printf("sizeof(Bad) = %zu\n", sizeof(struct Bad)); /* 12 */
23 printf("sizeof(Good) = %zu\n", sizeof(struct Good)); /* 8 */
24 printf("sizeof(Packed)= %zu\n", sizeof(struct Packed)); /* 6 */
25 return 0;
26}

warning

Padding wastes memory. Reorder members from largest to smallest to minimize gaps. Never assume a struct has no padding unless you verify with sizeof.
Struct Assignment & Copies

Unlike arrays, structs can be assigned with the= operator. This performs a shallow member-wise copy. Two structs with pointer members will share the pointed-to memory after assignment:

shallow-copy.c
C
1struct Buffer {
2 size_t len;
3 char *data;
4};
5
6int main(void) {
7 struct Buffer a = { 5, malloc(5) };
8 memcpy(a.data, "hello", 5);
9
10 struct Buffer b = a; /* shallow copy */
11 b.len = 10; /* independent */
12 b.data[0] = 'H'; /* shared — modifies a too! */
13
14 printf("a.data = %.5s\n", a.data); /* "Hello" */
15 printf("b.data = %.5s\n", b.data); /* "Hello" */
16
17 free(a.data);
18 /* free(b.data); — double free! Both point to same memory */
19
20 return 0;
21}

best practice

For structs containing pointers, implement an explicitdeep_copy function that allocates and copies pointed-to data.
Passing Structs to Functions

Structs can be passed by value or by pointer. Passing by value copies the entire struct onto the stack — expensive for large structs. Passing by pointer is faster and allows the function to modify the original:

vec3.c
C
1struct Vec3 { float x, y, z; };
2
3/* Pass by value — entire struct copied */
4float length_by_value(struct Vec3 v) {
5 return sqrtf(v.x * v.x + v.y * v.y + v.z * v.z);
6}
7
8/* Pass by pointer — only 8 bytes copied (the pointer) */
9float length_by_ptr(const struct Vec3 *v) {
10 return sqrtf(v->x * v->x + v->y * v->y + v->z * v->z);
11}
12
13/* Pass by pointer — modifies in place */
14void normalize(struct Vec3 *v) {
15 float len = length_by_ptr(v);
16 if (len > 0.0f) {
17 v->x /= len;
18 v->y /= len;
19 v->z /= len;
20 }
21}
22
23int main(void) {
24 struct Vec3 v = { 3.0f, 4.0f, 0.0f };
25 printf("length = %.2f\n", length_by_value(v)); /* 5.00 */
26 printf("length = %.2f\n", length_by_ptr(&v)); /* 5.00 */
27 normalize(&v);
28 printf("normalized = (%.2f, %.2f, %.2f)\n", v.x, v.y, v.z);
29 return 0;
30}
Returning Structs from Functions

Functions can return structs by value. The compiler typically uses a hidden pointer to the caller's storage (the "hidden return" convention) to avoid copying on return. Large structs should still be returned via pointer for clarity:

rectangle.c
C
1struct Rect {
2 double x, y, width, height;
3};
4
5struct Rect make_rect(double x, double y, double w, double h) {
6 struct Rect r = { .x = x, .y = y, .width = w, .height = h };
7 return r;
8}
9
10double area(struct Rect r) {
11 return r.width * r.height;
12}
13
14int main(void) {
15 struct Rect box = make_rect(10.0, 20.0, 5.0, 3.0);
16 printf("area = %.1f\n", area(box)); /* 15.0 */
17 return 0;
18}
Array of Structs

Arrays of structs store elements contiguously in memory, making them cache-friendly. This is the typical pattern for collections of records:

students.c
C
1struct Student {
2 char name[32];
3 float gpa;
4};
5
6int main(void) {
7 struct Student class[] = {
8 { "Alice", 3.9f },
9 { "Bob", 3.5f },
10 { "Carol", 3.8f },
11 { "Dave", 3.2f },
12 };
13 int n = sizeof(class) / sizeof(class[0]);
14
15 /* find highest GPA */
16 float max_gpa = 0.0f;
17 int best = 0;
18 for (int i = 0; i < n; i++) {
19 if (class[i].gpa > max_gpa) {
20 max_gpa = class[i].gpa;
21 best = i;
22 }
23 }
24
25 printf("Top student: %s (%.1f)\n", class[best].name, max_gpa);
26 return 0;
27}
Nested Structs

Structs can contain other structs as members. This enables hierarchical data modeling — a person has an address, an address has a city, etc.

nested.c
C
1struct Date {
2 int year, month, day;
3};
4
5struct Person {
6 char name[64];
7 struct Date birth;
8 struct {
9 char street[128];
10 char city[64];
11 int zip;
12 } address; /* anonymous nested struct */
13};
14
15int main(void) {
16 struct Person p = {
17 .name = "Diana Prince",
18 .birth = { .year = 1975, .month = 3, .day = 1 },
19 .address = {
20 .street = "123 Justice Lane",
21 .city = "Gateway City",
22 .zip = 90210
23 }
24 };
25
26 printf("%s was born on %d-%02d-%02d\n",
27 p.name, p.birth.year, p.birth.month, p.birth.day);
28 printf("Lives in %s, %d\n", p.address.city, p.address.zip);
29 return 0;
30}
Pointers to Structs & the Arrow Operator

When you have a pointer to a struct, the arrow operator (->) dereferences and accesses in one step. It is syntactic sugar for(*ptr).member:

config.c
C
1struct Config {
2 int width;
3 int height;
4 int fullscreen;
5};
6
7void apply_config(struct Config *cfg) {
8 /* These are equivalent: */
9 int w1 = (*cfg).width; /* explicit dereference */
10 int w2 = cfg->width; /* arrow operator (preferred) */
11
12 cfg->width = 1920;
13 cfg->height = 1080;
14 cfg->fullscreen = 1;
15}
16
17int main(void) {
18 struct Config cfg = { 0 };
19 apply_config(&cfg);
20 printf("%dx%d fullscreen=%d\n", cfg.width, cfg.height, cfg.fullscreen);
21 return 0;
22}
typedef with Structs

typedef eliminates the need to repeat struct everywhere. It is the idiomatic way to define custom types in C:

vec2.c
C
1typedef struct {
2 double x, y;
3} Vec2;
4
5typedef struct Vec3 {
6 double x, y, z;
7} Vec3;
8
9Vec2 vec2_add(Vec2 a, Vec2 b) {
10 return (Vec2){ a.x + b.x, a.y + b.y };
11}
12
13Vec3 vec3_add(Vec3 a, Vec3 b) {
14 return (Vec3){ a.x + b.x, a.y + b.y, a.z + b.z };
15}
16
17int main(void) {
18 Vec2 p = { 1.0, 2.0 };
19 Vec2 q = { 3.0, 4.0 };
20 Vec2 r = vec2_add(p, q);
21 printf("(%.1f, %.1f)\n", r.x, r.y); /* (4.0, 6.0) */
22 return 0;
23}
Self-Referential Structs

A struct can contain a pointer to itself. This is the foundation of linked data structures — lists, trees, graphs. The pointer is finite in size, unlike embedding the struct directly (which would be infinitely recursive):

linked-list.c
C
1typedef struct Node {
2 int data;
3 struct Node *next; /* self-referential pointer */
4} Node;
5
6Node *push_front(Node *head, int value) {
7 Node *new_node = malloc(sizeof(Node));
8 new_node->data = value;
9 new_node->next = head;
10 return new_node;
11}
12
13void print_list(const Node *head) {
14 for (const Node *cur = head; cur != NULL; cur = cur->next) {
15 printf("%d -> ", cur->data);
16 }
17 printf("NULL\n");
18}
19
20void free_list(Node *head) {
21 while (head) {
22 Node *tmp = head;
23 head = head->next;
24 free(tmp);
25 }
26}
27
28int main(void) {
29 Node *list = NULL;
30 list = push_front(list, 30);
31 list = push_front(list, 20);
32 list = push_front(list, 10);
33 print_list(list); /* 10 -> 20 -> 30 -> NULL */
34 free_list(list);
35 return 0;
36}
Memory Layout, Padding & Packing

The C standard allows the compiler to insert padding bytes between and after struct members to satisfy alignment requirements. You can inspect layout withoffsetof and control it with compiler extensions:

offsetof.c
C
1#include <stdio.h>
2#include <stddef.h>
3
4struct Packed {
5 char a;
6 int b;
7 char c;
8} __attribute__((packed));
9
10int main(void) {
11 printf("sizeof = %zu\n", sizeof(struct Packed));
12 printf("offset a = %zu\n", offsetof(struct Packed, a));
13 printf("offset b = %zu\n", offsetof(struct Packed, b));
14 printf("offset c = %zu\n", offsetof(struct Packed, c));
15 /* sizeof = 6, offset b = 1 (no padding) */
16 return 0;
17}

MSVC uses #pragma pack instead of__attribute__((packed)):

packed-msvc.c
C
1#pragma pack(push, 1)
2struct PackedMSVC {
3 char a;
4 int b;
5 char c;
6};
7#pragma pack(pop)

warning

Packed structs may cause misaligned accesses on architectures that require aligned reads. Use them only when layout control is essential (wire protocols, file formats).
Unions

A union overlaps all members at the same memory address. Only one member is valid at any time. The size of a union equals the size of its largest member:

union.c
C
1union Value {
2 int i;
3 float f;
4 char s[16];
5};
6
7int main(void) {
8 union Value v;
9 printf("sizeof(union Value) = %zu\n", sizeof(union Value)); /* 16 */
10
11 v.i = 42;
12 printf("i = %d\n", v.i);
13
14 v.f = 3.14f;
15 printf("f = %.2f\n", v.f); /* valid */
16 /* printf("i = %d\n", v.i); */ /* undefined — last write was .f */
17
18 strcpy(v.s, "hello");
19 printf("s = %s\n", v.s);
20 return 0;
21}

best practice

Unions are useful for variant types. Pair a union with a tag (a struct containing aenum and the union) to track which member is currently valid.
Union vs Struct: Shared Memory
Featurestructunion
MemorySum of all members + paddingSize of largest member
AccessAll members simultaneouslyOne member at a time
Use caseAggregating related dataVariant types, type punning
AssignmentCopies all membersCopies storage (largest member)
Union Use Cases

Unions are indispensable for type punning, implementing variant types, and saving memory in embedded systems:

variant.c
C
1#include <stdint.h>
2
3/* Type punning: reinterpret bytes as float */
4float bytes_to_float(uint8_t bytes[4]) {
5 union {
6 float f;
7 uint8_t b[4];
8 } u;
9 u.b[0] = bytes[0];
10 u.b[1] = bytes[1];
11 u.b[2] = bytes[2];
12 u.b[3] = bytes[3];
13 return u.f;
14}
15
16/* Variant type with tag */
17typedef enum { TAG_INT, TAG_FLOAT, TAG_STR } TypeTag;
18
19typedef struct {
20 TypeTag tag;
21 union {
22 int i;
23 float f;
24 char s[32];
25 } value;
26} Variant;
27
28void print_variant(const Variant *v) {
29 switch (v->tag) {
30 case TAG_INT: printf("int: %d\n", v->value.i); break;
31 case TAG_FLOAT: printf("float: %f\n", v->value.f); break;
32 case TAG_STR: printf("str: %s\n", v->value.s); break;
33 }
34}
35
36int main(void) {
37 Variant v1 = { .tag = TAG_INT, .value.i = 42 };
38 Variant v2 = { .tag = TAG_FLOAT, .value.f = 3.14f };
39 Variant v3 = { .tag = TAG_STR };
40 strcpy(v3.value.s, "hello");
41
42 print_variant(&v1);
43 print_variant(&v2);
44 print_variant(&v3);
45 return 0;
46}
Enum Declaration & Usage

Enums create named integer constants. They improve readability and enable compiler warnings for unhandled cases in switch statements:

enums.c
C
1enum Color {
2 RED, /* 0 */
3 GREEN, /* 1 */
4 BLUE, /* 2 */
5 YELLOW = 10,
6 CYAN, /* 11 */
7 MAGENTA /* 12 */
8};
9
10enum Status {
11 OK = 0,
12 ERR_IO = -1,
13 ERR_MEM = -2,
14 ERR_TIMEOUT = -3
15};
16
17int main(void) {
18 enum Color c = GREEN;
19 switch (c) {
20 case RED: printf("Red\n"); break;
21 case GREEN: printf("Green\n"); break;
22 case BLUE: printf("Blue\n"); break;
23 case YELLOW: printf("Yellow\n"); break;
24 case CYAN: printf("Cyan\n"); break;
25 case MAGENTA: printf("Magenta\n"); break;
26 }
27
28 printf("GREEN = %d\n", GREEN); /* 1 */
29 return 0;
30}
Enums for State Machines & Flags

Enums are perfect for representing states, error codes, and flag sets. Combined with bitwise operations, they enable compact flag systems:

flags.c
C
1/* Bit flags — values are powers of 2 */
2enum FilePerm {
3 PERM_READ = 1 << 0, /* 1 */
4 PERM_WRITE = 1 << 1, /* 2 */
5 PERM_EXECUTE = 1 << 2, /* 4 */
6 PERM_APPEND = 1 << 3, /* 8 */
7};
8
9int main(void) {
10 int perm = PERM_READ | PERM_WRITE; /* combined flags */
11
12 if (perm & PERM_READ) printf("Readable\n");
13 if (perm & PERM_WRITE) printf("Writable\n");
14 if (perm & PERM_EXECUTE) printf("Executable\n"); /* no */
15
16 perm |= PERM_EXECUTE; /* add flag */
17 perm &= ~PERM_WRITE; /* remove flag */
18 printf("New perm: %d\n", perm); /* 5 (READ | EXECUTE) */
19 return 0;
20}

info

Always use power-of-2 values for flag enums. The1 << n pattern ensures each flag occupies exactly one bit.
typedef: Type Aliases

typedef creates an alias for any type. It is heavily used with structs (covered above) but works with all types — pointers, function pointers, and platform-specific sizes:

typedef.c
C
1#include <stdint.h>
2
3typedef unsigned char byte;
4typedef int32_t i32;
5typedef uint64_t u64;
6typedef const char* cstring;
7
8typedef int (*Comparator)(const void*, const void*);
9
10int cmp_int(const void *a, const void *b) {
11 return (*(const int*)a) - (*(const int*)b);
12}
13
14int main(void) {
15 byte buffer[256] = { 0 };
16 i32 value = -42;
17 u64 big = 18446744073709551615ULL;
18
19 int arr[] = { 5, 2, 8, 1, 9 };
20 int n = sizeof(arr) / sizeof(arr[0]);
21
22 qsort(arr, n, sizeof(int), cmp_int);
23
24 for (i32 i = 0; i < n; i++) {
25 printf("%d ", arr[i]);
26 }
27 printf("\n");
28 return 0;
29}
Anonymous Structs & Unions (C11)

C11 allows anonymous structs and unions. Members of the anonymous member are accessed directly as if they belonged to the outer struct, reducing verbosity:

anonymous.c
C
1struct Record {
2 int type;
3 union { /* anonymous union */
4 int i;
5 double d;
6 char s[32];
7 };
8};
9
10int main(void) {
11 struct Record r1 = { .type = 0, .i = 42 };
12 struct Record r2 = { .type = 1, .d = 3.14 };
13
14 /* Access union members directly — no .value. prefix */
15 printf("type=%d val=%d\n", r1.type, r1.i);
16 printf("type=%d val=%.2f\n", r2.type, r2.d);
17 return 0;
18}
Bit Fields in Structs

Bit fields pack integer members into specific numbers of bits. They are useful for hardware registers, protocol headers, and compact flag storage. The layout is implementation-defined:

bitfields.c
C
1struct Flags {
2 unsigned int bold : 1;
3 unsigned int italic : 1;
4 unsigned int underline : 1;
5 unsigned int fg_color : 4; /* 0-15 */
6 unsigned int bg_color : 4; /* 0-15 */
7};
8
9struct IPv4Header {
10 unsigned int version : 4;
11 unsigned int ihl : 4;
12 unsigned int dscp : 6;
13 unsigned int ecn : 2;
14 unsigned int length : 16;
15};
16
17int main(void) {
18 struct Flags f = { 0 };
19 f.bold = 1;
20 f.fg_color = 12; /* 0-15 */
21 f.bg_color = 0;
22
23 printf("sizeof Flags = %zu\n", sizeof(struct Flags));
24 printf("bold=%d italic=%d fg=%d bg=%d\n",
25 f.bold, f.italic, f.fg_color, f.bg_color);
26
27 struct IPv4Header hdr = { .version = 4, .ihl = 5, .length = 60 };
28 printf("version=%u ihl=%u length=%u\n",
29 hdr.version, hdr.ihl, hdr.length);
30 return 0;
31}

warning

Bit field behavior is implementation-defined. Do not rely on padding, ordering across byte boundaries, or specific layout when porting code between compilers.
Complete Examples

Binary Tree with Self-Referential Structs

bst.c
C
1#include <stdio.h>
2#include <stdlib.h>
3
4typedef struct TreeNode {
5 int data;
6 struct TreeNode *left;
7 struct TreeNode *right;
8} TreeNode;
9
10TreeNode *new_node(int data) {
11 TreeNode *n = malloc(sizeof(TreeNode));
12 *n = (TreeNode){ .data = data, .left = NULL, .right = NULL };
13 return n;
14}
15
16TreeNode *insert(TreeNode *root, int data) {
17 if (!root) return new_node(data);
18 if (data < root->data)
19 root->left = insert(root->left, data);
20 else
21 root->right = insert(root->right, data);
22 return root;
23}
24
25void inorder(const TreeNode *root) {
26 if (!root) return;
27 inorder(root->left);
28 printf("%d ", root->data);
29 inorder(root->right);
30}
31
32void free_tree(TreeNode *root) {
33 if (!root) return;
34 free_tree(root->left);
35 free_tree(root->right);
36 free(root);
37}
38
39int main(void) {
40 TreeNode *tree = NULL;
41 int values[] = { 50, 30, 70, 20, 40, 60, 80 };
42 int n = sizeof(values) / sizeof(values[0]);
43
44 for (int i = 0; i < n; i++)
45 tree = insert(tree, values[i]);
46
47 printf("Inorder: ");
48 inorder(tree);
49 printf("\n");
50
51 free_tree(tree);
52 return 0;
53}

Generic Container with Union

cell-row.c
C
1#include <stdio.h>
2#include <string.h>
3
4typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } DataType;
5
6typedef struct {
7 DataType type;
8 union {
9 int i;
10 float f;
11 char s[64];
12 };
13} Cell;
14
15typedef struct {
16 Cell cells[16];
17 int count;
18} Row;
19
20void row_add_int(Row *r, int val) {
21 Cell *c = &r->cells[r->count++];
22 c->type = TYPE_INT;
23 c->i = val;
24}
25
26void row_add_float(Row *r, float val) {
27 Cell *c = &r->cells[r->count++];
28 c->type = TYPE_FLOAT;
29 c->f = val;
30}
31
32void row_add_string(Row *r, const char *val) {
33 Cell *c = &r->cells[r->count++];
34 c->type = TYPE_STRING;
35 strncpy(c->s, val, 63);
36 c->s[63] = '\0';
37}
38
39void print_row(const Row *r) {
40 for (int i = 0; i < r->count; i++) {
41 const Cell *c = &r->cells[i];
42 switch (c->type) {
43 case TYPE_INT: printf("%d\t", c->i); break;
44 case TYPE_FLOAT: printf("%.2f\t", c->f); break;
45 case TYPE_STRING: printf("%s\t", c->s); break;
46 }
47 }
48 printf("\n");
49}
50
51int main(void) {
52 Row row = { .count = 0 };
53 row_add_int(&row, 42);
54 row_add_float(&row, 3.14f);
55 row_add_string(&row, "hello");
56 print_row(&row); /* 42 3.14 hello */
57 return 0;
58}
$Blueprint — Engineering Documentation·Section ID: C-STRUCTURES·Revision: 1.0