Structures and Unions in C: Complete Guide with Implementation Examples

Structures and Unions in C

When I first started writing C, I leaned on arrays and separate variables for everything, until the code became a mess of parallel arrays that were easy to get out of sync. Structures fixed that instantly — they let you group related data into one coherent unit. Unions, on the other hand, took me a bit longer to really “get,” because they solve a different problem: sharing memory between different interpretations of the same data. Once both clicked, a huge amount of C code — from simple record-keeping programs to protocol parsers — suddenly made a lot more sense.

This guide covers both concepts from the ground up, with full working code, memory layout explanations, and the practical patterns you’ll actually use.

Table of Contents

  1. Why Structures Exist
  2. Declaring and Initializing Structures
  3. Accessing and Modifying Structure Members
  4. Nested Structures
  5. Arrays of Structures
  6. Pointers to Structures
  7. Passing Structures to Functions
  8. Structure Padding and Memory Alignment
  9. Unions: Sharing Memory
  10. Structures vs. Unions: Key Differences
  11. typedef with Structures and Unions
  12. Best Practices
  13. Performance Considerations
  14. Debugging Structures and Unions
  15. Common Mistakes
  16. Real-World Applications
  17. Interview Questions
  18. FAQs
  19. Summary and Key Takeaways
  20. References

1. Why Structures Exist

Imagine modeling a student record: name, roll number, and GPA. Without structures, you’d need three separate arrays — names[], rollNumbers[], gpas[] — and you’d have to trust yourself to keep index i consistent across all three everywhere in your code. One mistake, and student data silently gets mixed up. A struct bundles these related fields into a single named type, so the compiler enforces the grouping and your code reads far more naturally.

2. Declaring and Initializing Structures

#include <stdio.h>
#include <string.h>

struct Student {
    char name[50];
    int rollNumber;
    float gpa;
};

int main(void) {
    struct Student s1 = {"Ayesha Khan", 101, 3.8f};

    // Designated initializers (C99+) - clearer and order-independent
    struct Student s2 = {.rollNumber = 102, .gpa = 3.5f, .name = "Bilal Ahmed"};

    printf("s1: %s, Roll: %d, GPA: %.2f\n", s1.name, s1.rollNumber, s1.gpa);
    printf("s2: %s, Roll: %d, GPA: %.2f\n", s2.name, s2.rollNumber, s2.gpa);

    return 0;
}

Output:

s1: Ayesha Khan, Roll: 101, GPA: 3.80
s2: Bilal Ahmed, Roll: 102, GPA: 3.50

Designated initializers (.fieldName = value) are especially useful in larger structures — you don’t have to remember or match the exact declared field order, and the code documents itself.

3. Accessing and Modifying Structure Members

You use the dot operator (.) for a structure variable and the arrow operator (->) for a pointer to a structure.

#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main(void) {
    struct Point p1 = {3, 4};
    p1.x = 10; // modify directly

    struct Point *ptr = &p1;
    ptr->y = 20; // equivalent to (*ptr).y = 20

    printf("Point: (%d, %d)\n", p1.x, p1.y);
    return 0;
}

Output:

Point: (10, 20)

4. Nested Structures

Structures can contain other structures as members, which is how you model hierarchical, real-world data.

#include <stdio.h>

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

struct Employee {
    char name[50];
    struct Date joiningDate;
};

int main(void) {
    struct Employee e1 = {"Sana Malik", {15, 6, 2022}};

    printf("Employee: %s\n", e1.name);
    printf("Joining Date: %02d-%02d-%04d\n",
           e1.joiningDate.day, e1.joiningDate.month, e1.joiningDate.year);

    return 0;
}

Output:

Employee: Sana Malik
Joining Date: 15-06-2022

5. Arrays of Structures

This is one of the most common real-world patterns — a collection of records, like a small in-memory database table.

#include <stdio.h>

struct Product {
    char name[30];
    float price;
};

int main(void) {
    struct Product inventory[3] = {
        {"Notebook", 2.50f},
        {"Pen", 0.75f},
        {"Backpack", 25.00f}
    };

    float total = 0;
    for (int i = 0; i < 3; i++) {
        printf("%-10s: $%.2f\n", inventory[i].name, inventory[i].price);
        total += inventory[i].price;
    }
    printf("Total: $%.2f\n", total);

    return 0;
}

Output:

Notebook  : $2.50
Pen       : $0.75
Backpack  : $25.00
Total: $28.25

6. Pointers to Structures

Working with structures through pointers avoids copying large amounts of data and is essential for building dynamic data structures like linked lists and trees.

#include <stdio.h>
#include <stdlib.h>

struct Node {
    int data;
    struct Node *next;
};

int main(void) {
    struct Node *head = malloc(sizeof(struct Node));
    head->data = 1;
    head->next = malloc(sizeof(struct Node));
    head->next->data = 2;
    head->next->next = NULL;

    struct Node *current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");

    // Clean up
    free(head->next);
    free(head);

    return 0;
}

Output:

1 -> 2 -> NULL

Notice struct Node contains a pointer to itself (struct Node *next) — this self-referential structure is exactly how linked lists, trees, and graphs are built in C.

7. Passing Structures to Functions

You can pass structures by value (a full copy) or by pointer (a reference, no copy).

#include <stdio.h>

struct Rectangle {
    float width, height;
};

// Pass by value: original is untouched, a full copy is made
float calculateArea(struct Rectangle r) {
    return r.width * r.height;
}

// Pass by pointer: can modify the original, avoids copying
void scaleRectangle(struct Rectangle *r, float factor) {
    r->width *= factor;
    r->height *= factor;
}

int main(void) {
    struct Rectangle rect = {4.0f, 5.0f};

    printf("Original area: %.2f\n", calculateArea(rect));

    scaleRectangle(&rect, 2.0f);
    printf("After scaling, area: %.2f\n", calculateArea(rect));

    return 0;
}

Output:

Original area: 20.00
After scaling, area: 80.00

For large structures, always prefer passing a pointer (often const struct Rectangle * if you don’t need to modify it) — passing by value copies every byte of the structure onto the stack, which is wasteful for anything beyond a few small fields.

8. Structure Padding and Memory Alignment

This is where structures get genuinely interesting at the internals level. Compilers align structure members in memory according to the target architecture’s alignment requirements, which means the compiler may insert invisible padding bytes between members.

#include <stdio.h>

struct Example1 {
    char a;   // 1 byte
    int b;    // 4 bytes
    char c;   // 1 byte
};

struct Example2 {
    int b;    // 4 bytes
    char a;   // 1 byte
    char c;   // 1 byte
};

int main(void) {
    printf("sizeof(Example1) = %zu\n", sizeof(struct Example1));
    printf("sizeof(Example2) = %zu\n", sizeof(struct Example2));
    return 0;
}

Output (typical on a 64-bit system):

sizeof(Example1) = 12
sizeof(Example2) = 8

Even though both structures contain the exact same members, Example1 is larger because the compiler inserts padding to keep int b aligned on a 4-byte boundary, and adds trailing padding so arrays of the structure stay aligned too. Example2 avoids most of that waste simply by ordering members from largest to smallest. This is a real, practical technique: order structure members from largest to smallest type to minimize padding in memory-sensitive applications.

You can eliminate padding almost entirely with a compiler-specific directive, though it comes at a performance cost on some architectures due to unaligned memory access:

#pragma pack(push, 1)
struct Packed {
    char a;
    int b;
    char c;
};
#pragma pack(pop)

9. Unions: Sharing Memory

A union looks syntactically like a structure, but all its members share the same memory location. The union’s total size is the size of its largest member, and writing to one member overwrites the bytes of all the others.

#include <stdio.h>

union Data {
    int i;
    float f;
    char str[20];
};

int main(void) {
    union Data data;

    data.i = 10;
    printf("data.i = %d\n", data.i);

    data.f = 3.14f;
    printf("data.f = %.2f\n", data.f);
    printf("data.i after setting f = %d (garbage, overwritten memory)\n", data.i);

    printf("sizeof(union Data) = %zu\n", sizeof(union Data));

    return 0;
}

Output (typical):

data.i = 10
data.f = 3.14
data.i after setting f = 1078523331 (garbage, overwritten memory)

Notice sizeof(union Data) equals 20 bytes (the size of the largest member, str[20]), not the sum of all members. And once you write to data.f, reading data.i no longer gives you meaningful data — you’re reading the same memory bytes reinterpreted as a different type. This is exactly why unions are useful for type-punning and tagged unions, but dangerous if you forget which member you last wrote.

A Practical Tagged Union Pattern

#include <stdio.h>

enum ValueType { TYPE_INT, TYPE_FLOAT, TYPE_STRING };

struct TaggedValue {
    enum ValueType type;
    union {
        int i;
        float f;
        char str[20];
    } value;
};

void printValue(struct TaggedValue v) {
    switch (v.type) {
        case TYPE_INT:    printf("Int: %d\n", v.value.i); break;
        case TYPE_FLOAT:  printf("Float: %.2f\n", v.value.f); break;
        case TYPE_STRING: printf("String: %s\n", v.value.str); break;
    }
}

int main(void) {
    struct TaggedValue a = {.type = TYPE_INT, .value.i = 42};
    struct TaggedValue b = {.type = TYPE_STRING, .value.str = "hello"};

    printValue(a);
    printValue(b);

    return 0;
}

Output:

Int: 42
String: hello

This “tagged union” pattern — a struct pairing a type indicator (the enum) with a union of possible values — is how many real interpreters and parsers represent variant data types safely, since the tag tells you which union member is currently valid.

10. Structures vs. Unions: Key Differences

AspectStructureUnion
MemoryEach member has its own spaceAll members share the same space
SizeSum of all members (plus padding)Size of the largest member
Simultaneous accessAll members hold valid data at onceOnly the last-written member holds valid data
Use caseGrouping unrelated-but-related fields (a record)Representing one value as different types (space-saving, type-punning)

11. typedef with Structures and Unions

typedef lets you avoid writing struct or union every time you declare a variable, which makes code noticeably cleaner.

#include <stdio.h>

typedef struct {
    int x, y;
} Point;

typedef union {
    int i;
    float f;
} Number;

int main(void) {
    Point p = {5, 10};       // no "struct" keyword needed
    Number n;
    n.f = 9.5f;

    printf("Point: (%d, %d)\n", p.x, p.y);
    printf("Number as float: %.2f\n", n.f);

    return 0;
}

Output:

Point: (5, 10)
Number as float: 9.50

12. Best Practices

  • Order structure members from largest to smallest to reduce padding waste.
  • Use typedef for cleaner, more readable type names in larger codebases.
  • Pass large structures by pointer (ideally const pointer if read-only) instead of by value.
  • Always track which member of a union is currently valid — a tag field (enum) alongside the union is the standard, safe pattern.
  • Use designated initializers for clarity, especially with structures that have many fields.
  • Zero-initialize structures you plan to fill incrementally (struct Foo f = {0};) to avoid leftover garbage in unused fields.

13. Performance Considerations

  • Structure padding can meaningfully bloat memory usage in large arrays of structures — reordering members is a free, zero-risk optimization worth doing habitually.
  • Passing structures by value in function calls copies every byte, which adds up in hot loops; pass by pointer for anything beyond a couple of small fields.
  • Unions are a deliberate memory-saving technique — when you know only one “view” of data is needed at a time, a union uses far less memory than a structure holding every possible type separately.
  • Be cautious with #pragma pack — tightly packed structures can cause slower memory access on architectures that penalize unaligned reads, even though they save space.

14. Debugging Structures and Unions

  • Use sizeof() liberally during development to confirm your assumptions about memory layout, especially before writing structures to binary files.
  • In GDB, print on a structure variable shows all fields at once, which is much faster for inspection than printing members one by one.
  • For unions, always double-check which member you last wrote before reading — a debugger will happily show you a “value” that’s actually garbage from a different type’s bit pattern.
  • When debugging binary file formats built from structs, watch for padding differences between the machine that wrote the file and the machine reading it back.

15. Common Mistakes

  1. Assuming sizeof(struct) equals the sum of member sizes — padding usually makes it larger.
  2. Reading a union member you didn’t most recently write, and getting garbage or misinterpreted data.
  3. Comparing structures directly with == — C doesn’t support this; you must compare member by member (or use memcmp carefully, mindful of padding bytes).
  4. Forgetting to allocate memory for pointer members inside a structure before using them.
  5. Copying structures containing pointers by value (“shallow copy”) and ending up with two structures pointing to the same dynamically allocated memory, leading to double frees.
  6. Not using a tag/discriminator with a union, causing bugs when the “wrong” member is read.

16. Real-World Applications

Structures and unions are everywhere: modeling database records, representing network packet headers (a struct with bit-fields mapped to a protocol’s exact byte layout), game entity data, tagged unions in interpreters and compilers for representing different token or AST node types, and hardware register mapping in embedded systems, where a union lets you view the same memory as either a whole register or its individual bit-fields.

17. Interview Questions

  • What’s the difference between a structure and a union in terms of memory allocation?
  • Why might sizeof(struct) not equal the sum of its members’ sizes?
  • How would you implement a linked list using structures?
  • What is a tagged union, and why is it useful?
  • Explain shallow copy vs. deep copy in the context of structures containing pointers.
  • How does #pragma pack affect structure size and performance?
  • Can a structure contain a pointer to itself? Why is this necessary for certain data structures?

18. FAQs

Can a union contain a structure? Yes. Structures and unions can be nested inside each other in either direction, which is exactly the basis of the tagged-union pattern shown earlier.

Why does my structure take more memory than expected? Compiler-inserted padding for alignment. Reordering members from largest to smallest type typically reduces or eliminates this.

Is it safe to compare two structures with ==? No — C does not allow direct equality comparison of structures. Compare each member individually, or use memcmp only if you’re certain there’s no uninitialized padding involved.

19. Summary and Key Takeaways

Structures let you group related data into a single, well-defined type, which is the foundation for records, linked data structures, and organized program design in C. Unions let multiple types share the same memory, which is powerful for space efficiency and type-punning, but only safe when paired with a way to track which member is currently valid. Understanding padding and alignment isn’t just trivia — it directly affects memory usage and, in some cases, performance, especially in large arrays of structures or binary file formats.

20. References

  • ISO/IEC 9899 — Programming languages C (Section 6.7.2.1, “Structure and union specifiers”)
  • GCC Online Documentation — https://gcc.gnu.org/onlinedocs/
  • GCC documentation on #pragma pack and structure layout
  • The C Standard Library reference for <stddef.h> (offsetof macro, useful for inspecting padding)
Total
1
Shares

Leave a Reply

Previous Post
Pointers and Memory Management in C

Pointers and Memory Management in C: Dynamic Allocation and Optimization

Next Post
Working with Files in C

Working with Files in C: File Handling, Reading, Writing, and Management

Related Posts