ccompiler.inOnline C Compiler & Docs
Engineering Blog2026-08-147 min read

Compare structures and unions in C programming. Explore memory alignment, member sharing, padding, and practical real-world embedded use cases.

In C programming, both struct (Structure) and union are composite user-defined data types that allow you to group multiple variables under a single name.

However, they treat system memory in fundamentally opposite ways. While a struct allocates separate, dedicated memory for every single member, a union forces all of its members to share the exact same memory location.

Understanding the difference between structs and unions is essential for low-level systems programming, embedded firmware development, network protocol parsing, and high-performance memory optimization.

In this guide, we will analyze the hardware memory layouts of structs and unions, explore compiler padding and byte alignment, and examine real-world applications including Tagged Unions, Register Bitfields, and Endianness Detection.


Quick Comparison: Struct vs. Union

Featurestruct (Structure)union (Union)
Keywordstructunion
Memory AllocationEach member receives its own unique memory addressAll members share the exact same starting memory address
Total SizeSum of all member sizes + compiler padding bytesSize of the largest member (rounded to alignment)
Member AccessAll members can be accessed and stored simultaneouslyOnly one member can hold a meaningful value at a time
Memory OffsetMembers reside at increasing memory offsets (+0, +4, +8)All members reside at offset +0
Primary Use CaseRepresenting complex entities (e.g., Person, Point3D)Memory conservation, type punning, variant types, registers

1. Deep Dive: Memory Layout of a struct

A struct creates a composite record where every field has its own independent storage.

c (ISO Standard)
#include <stdio.h>

struct DataStruct {
    char flag;    // 1 byte
    int id;       // 4 bytes
    double score; // 8 bytes
};

You might expect sizeof(struct DataStruct) to be $1 + 4 + 8 = 13 bytes$. However, on a 64-bit system, sizeof(struct DataStruct) actually evaluates to 16 bytes!

Why? Structure Padding and Byte Alignment

CPUs read and write memory most efficiently when data is aligned to natural memory boundaries (e.g., 4-byte integers must sit at addresses divisible by 4, and 8-byte doubles at addresses divisible by 8). To satisfy alignment requirements, the compiler inserts hidden padding bytes:

text (ISO Standard)
+-------------------+-------------------+-------------------+-------------------+
| char flag (1B)    | Padding (3 bytes) | int id (4 bytes)  | double score (8B) |
| Offset: +0        | Offset: +1..+3    | Offset: +4..+7    | Offset: +8..+15   |
+-------------------+-------------------+-------------------+-------------------+
<---------------------------------- Total: 16 Bytes ---------------------------->

Optimizing Struct Size by Reordering Members

By simply rearranging struct members from largest to smallest, you can reduce compiler padding and shrink the struct size:

c (ISO Standard)
// Optimized Struct: Only 16 bytes with zero internal gap padding
struct OptimizedStruct {
    double score; // 8 bytes (Offset +0)
    int id;       // 4 bytes (Offset +8)
    char flag;    // 1 byte  (Offset +12)
    char pad[3];  // 3 bytes tail padding (Total: 16 bytes)
};

2. Deep Dive: Memory Layout of a union

A union creates a shared memory container where all members overlap in the exact same memory space.

c (ISO Standard)
#include <stdio.h>

union DataUnion {
    char flag;    // 1 byte
    int id;       // 4 bytes
    double score; // 8 bytes
};

Here, sizeof(union DataUnion) is exactly 8 bytes (the size of its largest member, double score).

text (ISO Standard)
Memory Offset: +0                  +4                  +8
+-----------------------------------+-------------------+
| char flag (1B)                    |                   |
+-----------------------------------+                   |
| int id (4 bytes)                  |                   |
+-----------------------------------+-------------------+
| double score (8 bytes)                                |
+-------------------------------------------------------+
<--------------------- Total: 8 Bytes ------------------>

When you write to id = 100, it overwrites the first 4 bytes of score. When you write to score = 3.14, it overwrites id and flag.


3. Side-by-Side Code Demonstration

Let us execute a complete program demonstrating how structs and unions behave when their members are modified:

c (ISO Standard)
#include <stdio.h>

struct ExampleStruct {
    int integerVal;
    float floatVal;
    char charVal;
};

union ExampleUnion {
    int integerVal;
    float floatVal;
    char charVal;
};

int main(void) {
    struct ExampleStruct s;
    union ExampleUnion u;

    printf("=== Size Comparison ===\n");
    printf("sizeof(struct ExampleStruct): %zu bytes\n", sizeof(s));
    printf("sizeof(union ExampleUnion):   %zu bytes\n\n", sizeof(u));

    printf("=== Setting Members in Struct ===\n");
    s.integerVal = 42;
    s.floatVal = 3.14f;
    s.charVal = 'A';
    printf("Struct: integer=%d, float=%.2f, char='%c'\n\n", 
           s.integerVal, s.floatVal, s.charVal);

    printf("=== Setting Members in Union ===\n");
    u.integerVal = 42;
    printf("After setting integerVal=42:  u.integerVal = %d\n", u.integerVal);

    u.floatVal = 3.14f; // Overwrites integerVal
    printf("After setting floatVal=3.14:  u.floatVal   = %.2f\n", u.floatVal);
    printf("Notice u.integerVal is now corrupted: %d\n", u.integerVal);

    return 0;
}

Expected Output:

text (ISO Standard)
=== Size Comparison ===
sizeof(struct ExampleStruct): 12 bytes
sizeof(union ExampleUnion):   4 bytes

=== Setting Members in Struct ===
Struct: integer=42, float=3.14, char='A'

=== Setting Members in Union ===
After setting integerVal=42:  u.integerVal = 42
After setting floatVal=3.14:  u.floatVal   = 3.14
Notice u.integerVal is now corrupted: 1078523331

4. 3 Real-World Engineering Use Cases for Unions


Use Case 1: Tagged / Discriminated Unions (Variant Types)

Because a union does not track which member is currently active, production C code pairs a union with an enum inside a wrapper struct:

c (ISO Standard)
#include <stdio.h>

typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } ValueType;

typedef struct {
    ValueType type;
    union {
        int iValue;
        float fValue;
        char *strValue;
    } data;
} DynamicVariant;

void printVariant(const DynamicVariant *v) {
    switch (v->type) {
        case TYPE_INT:
            printf("Integer: %d\n", v->data.iValue);
            break;
        case TYPE_FLOAT:
            printf("Float: %.2f\n", v->data.fValue);
            break;
        case TYPE_STRING:
            printf("String: %s\n", v->data.strValue);
            break;
    }
}

Use Case 2: Hardware Register & Byte Inspection in Embedded Systems

Embedded engineers frequently use unions to access a 32-bit hardware register either as a single 32-bit word or as 4 individual 8-bit bytes:

c (ISO Standard)
#include <stdio.h>
#include <stdint.h>

typedef union {
    uint32_t fullRegister;
    struct {
        uint8_t byte0; // Lowest byte
        uint8_t byte1;
        uint8_t byte2;
        uint8_t byte3; // Highest byte
    } bytes;
} HardwareRegister;

int main(void) {
    HardwareRegister reg;
    reg.fullRegister = 0xAABBCCDD;

    printf("Full 32-bit Register: 0x%08X\n", reg.fullRegister);
    printf("Byte 0: 0x%02X\n", reg.bytes.byte0);
    printf("Byte 1: 0x%02X\n", reg.bytes.byte1);
    printf("Byte 2: 0x%02X\n", reg.bytes.byte2);
    printf("Byte 3: 0x%02X\n", reg.bytes.byte3);

    return 0;
}

Use Case 3: Endianness Detection

You can detect whether your host CPU is Little-Endian (x86, ARM) or Big-Endian using a 2-byte union:

c (ISO Standard)
#include <stdio.h>
#include <stdint.h>

int isLittleEndian(void) {
    union {
        uint16_t word;
        uint8_t byte;
    } test = { .word = 0x0001 };

    return (test.byte == 0x01); // 1 = Little Endian, 0 = Big Endian
}

int main(void) {
    if (isLittleEndian()) {
        printf("Host Architecture is: Little Endian\n");
    } else {
        printf("Host Architecture is: Big Endian\n");
    }
    return 0;
}

Frequently Asked Questions (FAQ)

1. What is an Anonymous Union in C11?

Starting in C11, you can declare an unnamed union inside a struct without needing an extra variable name, allowing direct member access:

c (ISO Standard)
struct Vector {
    int type;
    union { // Anonymous union
        float x;
        float y;
    };
};
// Access directly: vec.x instead of vec.data.x

2. Can you pass a struct or union to a function by value?

Yes. Unlike arrays (which decay into pointers), passing a struct or union by value creates a full bitwise copy of its memory. For large structs, pass a pointer (const struct Data *ptr) to avoid copy overhead.

3. What is #pragma pack(1) used for?

#pragma pack(1) instructs GCC to disable structure padding and pack members with 1-byte alignment. This is critical when serializing C structs directly into network packets (TCP/IP headers) or binary file formats.


Conclusion

Structs and unions serve complementary roles in C. Use struct when you need to bundle independent properties together into a coherent entity. Use union when you need to conserve memory, implement dynamic variant types, or manipulate hardware registers at the byte level.


Related Articles & References