Structs, Unions, and Enums in C

October 9, 2026 • 6 min read

Structs, Unions, and Enums in C
Table of contents

Part of the series:C Explained

Arrays hold many values of one type. This post covers the other three built-in structures the language gives you: structs1 group values of different types under one name, unions2 let different types share the same memory, and enums3 give readable names to integers.

Structs

A struct is a group of named fields, possibly of different types, stored back to back in memory. Define the shape once, then make variables of that shape:

struct Student {
    char name[50];
    int age;
    double grade;
};

That defines a type. Now declare variables of it, with struct Student spelling out the full type name:

struct Student s1;

s1.age = 20;
s1.grade = 8.5;
strncpy(s1.name, "Ada", 49);

Each field is set and read with a dot:

printf("%s is %d years old.\n", s1.name, s1.age);

You can initialize a struct at declaration with brace syntax, filling fields in order:

struct Student s2 = {"Grace", 22, 9.0};

typedef: giving the type a short name

Writing struct Student everywhere is verbose. typedef creates an alias, so a single word names the type:

typedef struct {
    char name[50];
    int age;
    double grade;
} Student;

Student s1 = {"Ada", 20, 8.5};

Now Student is a complete type name. Reading other people’s C, you will see both styles; typedef is the convention for avoiding repetition.

Arrays of structs

A struct is a type like any other, so Student students[3] is an array of students:

Student students[] = {
    {"Ada", 20, 8.5},
    {"Grace", 22, 9.0},
    {"Alan", 21, 7.5},
};

double total = 0;
for (int i = 0; i < 3; i++) {
    total += students[i].grade;
}
printf("average: %.2f\n", total / 3);

Access follows the rule you already know: students[i] is a Student, and .grade picks its field. Combining arrays and structs is how you model real collections of related data.

Nested structs

A struct can contain other structs:

typedef struct {
    int day;
    int month;
    int year;
} Date;

typedef struct {
    char name[50];
    Date birth;
} Person;

Person p = {"Ada", {10, 12, 1815}};
printf("%d\n", p.birth.year);   // 1815

Chains of dots peel off one level at a time.

Passing structs to functions

A struct passed to a function is passed by value, exactly like an int: the function gets a full copy, and changes stay inside. For a small struct that is cheap and fine; for a big one the copy costs time and memory:

void celebrate(Student s) {
    s.age++;            // only the copy changes
    printf("Happy birthday, %s!\n", s.name);
}

When the struct is large, or the function must modify the original, C uses pointers instead. The arrow operator -> is just a dot through a pointer, and passed dereferencing by hand is a shortcut. The full explanation is the next post’s job; here is the shape you will see all over real C:

void grantBonus(Student *s) {
    s->grade = 10.0;    // modifies the original
}

-> means “the field of the struct this pointer points to”. If s is a pointer, s->grade equals (*s).grade.

Unions

A union declares several fields that all share the same memory. Only one field is meaningful at a time, because writing any of them overwrites the others. Its size is the size of its largest member:

union Value {
    int    i;
    double d;
    char   c;
};
union Value v;
v.d = 3.14;   // writing the double
printf("%f\n", v.d);
v.i = 7;      // now the int; the double is gone
printf("%d\n", v.i);

Unions exist to squeeze memory when only one interpretation applies at a time, and to reinterpret bytes, which is what reading raw hardware or network data asks for. They are usually paired with a separate field or enum that records which member is active, a pattern sometimes called a “tagged union”. Use them deliberately; a union used by accident is a source of subtle bugs.

Padding and alignment

A struct’s size is not simply the sum of its fields. The compiler inserts padding after some fields so that each one lands on a memory address the CPU reads efficiently, a property called alignment4. In practice for this series: an int usually must sit on a 4-byte boundary, a double on an 8-byte one, so fields can be spaced out:

printf("%zu\n", sizeof(Student));   // usually bigger than 50 + 4 + 8

Getting the exact padding rules right matters only for binary file formats and networking, where a struct’s layout gets written to disk or the wire; for everything else, trust sizeof.

Enums

An enum names a set of integer constants. It exists so your code reads as words, not magic numbers:

enum Color { RED, GREEN, BLUE };

By default RED is 0, GREEN is 1, BLUE is 2. You assign explicit values if the numbers matter:

enum Color { RED = 1, GREEN = 2, BLUE = 3 };
enum Color c = GREEN;

Print it as the underlying integer:

printf("%d\n", c);   // 2

You have already used enums indirectly: switch cases prefer them, and they are the idiomatic way to represent a small set of options like states or error codes. A function that returns a status often typedefs an enum so callers can test readable names instead of bare numbers:

typedef enum { OK = 0, TOO_SMALL, TOO_BIG } Status;

That pattern leads directly into error handling later in the series.

The big picture

struct bundles different types into one meaningful record, union lets several types share one block of memory, and enum turns integers into readable names. Combined with arrays they model real data: a list of students, a grid of points, a packet of fields. Structs also mark the moment where “copy the value” and “point to the original” diverge, which is exactly where the next post lives: pointers, the subject C is most famous and most feared for.

Footnotes

  1. Struct (C programming language) - Wikipedia

  2. Union type - Wikipedia

  3. Enumerated type - Wikipedia

  4. Data structure alignment - Wikipedia