How Unions Share Memory in C Programming

In the C programming language, a union is a specialized composite data type designed for memory efficiency, allowing multiple members of different data types to occupy the exact same memory space. Unlike a structure, where each declared member is allocated its own distinct address and memory offset, a union allocates a single memory buffer sized to accommodate only its largest member. This article explains the underlying memory mechanics of C unions, including base address alignment, member overlaying, and the practical implications of sharing memory across different data representations.

The Mechanics of Union Memory Allocation

When the C compiler encounters a struct, it calculates the total required memory by summing the sizes of all members along with any necessary padding bytes for CPU architecture alignment. In contrast, when the compiler evaluates a union, it applies a fundamentally different allocation strategy:

  1. Size Calculation: The compiler identifies the member requiring the greatest number of bytes. The total size of the union is set to at least the size of this largest member. If necessary, trailing padding is added so the union's total size is an even multiple of the strictest alignment requirement among its members.
  2. Offset Assignment: Every member inside a union is assigned an offset of zero (offset = 0) relative to the union's starting address.

Because every member begins at the exact same base address, modifying the value of one member directly modifies the underlying bits shared by all other members.

Demonstrating Shared Memory Addresses

The shared nature of union memory can be verified by inspecting the memory addresses of its individual members. Consider the following definition:

#include <stdio.h>

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

int main(void) {
    union Data data;

    printf("Memory address of data:     %p\n", (void*)&data);
    printf("Memory address of data.i:   %p\n", (void*)&data.i);
    printf("Memory address of data.f:   %p\n", (void*)&data.f);
    printf("Memory address of data.str: %p\n", (void*)&data.str);
    printf("Total size of union:        %zu bytes\n", sizeof(data));

    return 0;
}

In this example, &data, &data.i, &data.f, and &data.str all resolve to the exact same pointer address. The total size of the union will typically be 20 bytes (the size of char str[20]), assuming standard 4-byte alignment for the int and float types.

Bit Overwriting and State Mutation

Because all members map to the same binary storage, storing data into one member overwrites the bytes occupied by the others:

union Data data;

data.i = 42;
// The first 4 bytes of the union now represent the integer 42.

data.f = 3.14159f;
// The first 4 bytes are overwritten with the IEEE 754 floating-point representation of 3.14159.
// Accessing data.i now will yield an unpredictable, garbage integer value.

At any point during execution, a union is only intended to hold a valid value for one of its members. The programmer is responsible for tracking which member was written to most recently.

Common Use Cases for Shared Union Memory

  1. Memory Conservation: Embedded systems and resource-constrained environments use unions when multiple attributes are mutually exclusive, preventing the allocation of unused memory buffers.
  2. Tagged (Discriminated) Unions: Developers frequently pair a union with an enum inside an enclosing struct. The enum serves as a tag indicating which union member currently holds valid data.
  3. Type Punning (Hardware/Protocol Level): Low-level systems programming uses unions to inspect the binary byte representation of a composite type (such as reading a 32-bit integer as four distinct 8-bit bytes for network serialization), bypassing pointer casting while respecting specific compiler semantics.