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
| Feature | struct (Structure) | union (Union) |
|---|---|---|
| Keyword | struct | union |
| Memory Allocation | Each member receives its own unique memory address | All members share the exact same starting memory address |
| Total Size | Sum of all member sizes + compiler padding bytes | Size of the largest member (rounded to alignment) |
| Member Access | All members can be accessed and stored simultaneously | Only one member can hold a meaningful value at a time |
| Memory Offset | Members reside at increasing memory offsets (+0, +4, +8) | All members reside at offset +0 |
| Primary Use Case | Representing 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 Technical Resources
- Understand memory segments in Stack vs Heap Memory in C — Key Differences & Guide.
- Master pointer navigation in Pointers in C Explained — Visual Memory & Code Guide.