Unions
Master C unions - special data structures where members share memory. Learn memory-efficient storage, type punning, tag techniques, and when unions save space versus structures.
What Are Unions?
Unions are like structures but all members share the same memory location. Only one member can hold a value at a time. Union size equals the size of its largest member. They're useful for memory-constrained systems or when you need type reinterpretation.
#include <stdio.h>
/* Union definition */
union Data {
int i;
float f;
char str[20];
};
int main(void) {
union Data data;
/* Store integer */
data.i = 10;
printf("data.i: %d\n", data.i);
/* Store float (overwrites integer!) */
data.f = 3.14;
printf("data.f: %.2f\n", data.f);
printf("data.i: %d (garbage)\n", data.i); /* Previous value lost */
/* Store string (overwrites float!) */
strcpy(data.str, "Hello");
printf("data.str: %s\n", data.str);
printf("data.f: %.2f (garbage)\n", data.f); /* Previous value lost */
/* Size of union */
printf("Size: %zu bytes\n", sizeof(union Data)); /* ~20 bytes (largest member) */
return 0;
}
/* Memory comparison:
Structure (all members exist):
┌────────────────â”
│ int i (4) │
├────────────────┤
│ float f (4) │
├────────────────┤
│ char str[20] │
└────────────────┘
Total: ~28 bytes
Union (members share space):
┌────────────────â”
│ │
│ All members │
│ share this │
│ memory │
│ │
└────────────────┘
Total: ~20 bytes (largest)
*/Union vs Structure
The key difference: structures allocate space for all members, unions allocate space for only the largest. This makes unions memory-efficient but requires careful tracking of which member is currently valid.
/* Structure: All members have separate storage */
struct StructExample {
int i; /* 4 bytes */
float f; /* 4 bytes */
char c; /* 1 byte */
}; /* Total: ~12 bytes (with padding) */
/* Union: All members share storage */
union UnionExample {
int i; /* 4 bytes */
float f; /* 4 bytes */
char c; /* 1 byte */
}; /* Total: 4 bytes (largest member) */
void compare_struct_union(void) {
struct StructExample s;
union UnionExample u;
/* Structure: All members accessible */
s.i = 10;
s.f = 3.14;
s.c = 'A';
printf("Struct - i: %d, f: %.2f, c: %c\n", s.i, s.f, s.c);
printf("Struct size: %zu\n", sizeof(s));
/* Union: Only one member valid at a time */
u.i = 10;
printf("Union i: %d\n", u.i);
u.f = 3.14; /* Overwrites i */
printf("Union f: %.2f\n", u.f);
printf("Union i: %d (invalid)\n", u.i); /* Garbage */
u.c = 'A'; /* Overwrites f */
printf("Union c: %c\n", u.c);
printf("Union f: %.2f (invalid)\n", u.f); /* Garbage */
printf("Union size: %zu\n", sizeof(u));
}
/* When to use unions */
/*
Use structures when:
- Need all data simultaneously
- Memory is not constrained
- Clarity is priority
Use unions when:
- Only one field needed at a time
- Memory is limited
- Type reinterpretation needed
- Implementing variant types
*/Tagged Unions
Tagged unions (discriminated unions) combine a union with a tag field indicating which member is valid. This pattern is essential for safe union usage, preventing access to invalid members.
/* Enum for tag */
typedef enum {
TYPE_INT,
TYPE_FLOAT,
TYPE_STRING
} DataType;
/* Tagged union */
typedef struct {
DataType type; /* Tag indicating which member is valid */
union {
int i;
float f;
char str[50];
} value;
} TaggedData;
/* Create tagged data */
TaggedData make_int(int value) {
TaggedData data;
data.type = TYPE_INT;
data.value.i = value;
return data;
}
TaggedData make_float(float value) {
TaggedData data;
data.type = TYPE_FLOAT;
data.value.f = value;
return data;
}
TaggedData make_string(const char *value) {
TaggedData data;
data.type = TYPE_STRING;
strncpy(data.value.str, value, sizeof(data.value.str) - 1);
data.value.str[sizeof(data.value.str) - 1] = '\0';
return data;
}
/* Print based on type */
void print_tagged_data(const TaggedData *data) {
switch (data->type) {
case TYPE_INT:
printf("Integer: %d\n", data->value.i);
break;
case TYPE_FLOAT:
printf("Float: %.2f\n", data->value.f);
break;
case TYPE_STRING:
printf("String: %s\n", data->value.str);
break;
default:
printf("Unknown type\n");
}
}
/* Usage */
void tagged_union_example(void) {
TaggedData data1 = make_int(42);
TaggedData data2 = make_float(3.14);
TaggedData data3 = make_string("Hello");
print_tagged_data(&data1);
print_tagged_data(&data2);
print_tagged_data(&data3);
}
/* Complex tagged union example */
typedef enum {
SHAPE_CIRCLE,
SHAPE_RECTANGLE,
SHAPE_TRIANGLE
} ShapeType;
typedef struct {
ShapeType type;
union {
struct {
float radius;
} circle;
struct {
float width;
float height;
} rectangle;
struct {
float base;
float height;
} triangle;
} data;
} Shape;
float calculate_area(const Shape *shape) {
switch (shape->type) {
case SHAPE_CIRCLE:
return 3.14159 * shape->data.circle.radius * shape->data.circle.radius;
case SHAPE_RECTANGLE:
return shape->data.rectangle.width * shape->data.rectangle.height;
case SHAPE_TRIANGLE:
return 0.5 * shape->data.triangle.base * shape->data.triangle.height;
default:
return 0.0;
}
}Type Punning and Bit Manipulation
Unions allow type punning - reinterpreting data as a different type. Useful for low-level operations like accessing individual bytes of a value or converting between types without casts. Use carefully to avoid undefined behavior.
/* Access bytes of an integer */
union IntBytes {
int value;
unsigned char bytes[sizeof(int)];
};
void print_int_bytes(int value) {
union IntBytes ib;
ib.value = value;
printf("Value: %d (0x%X)\n", value, value);
printf("Bytes: ");
for (size_t i = 0; i < sizeof(int); i++) {
printf("%02X ", ib.bytes[i]);
}
printf("\n");
}
/* Check endianness */
int is_little_endian(void) {
union {
int i;
unsigned char bytes[sizeof(int)];
} test = {1};
return test.bytes[0] == 1;
}
/* Float to int conversion (bit pattern) */
union FloatInt {
float f;
unsigned int i;
};
void print_float_bits(float value) {
union FloatInt fi;
fi.f = value;
printf("Float: %f\n", value);
printf("Bits: 0x%08X\n", fi.i);
/* Extract sign, exponent, mantissa */
unsigned int sign = (fi.i >> 31) & 1;
unsigned int exponent = (fi.i >> 23) & 0xFF;
unsigned int mantissa = fi.i & 0x7FFFFF;
printf("Sign: %u, Exponent: %u, Mantissa: %X\n", sign, exponent, mantissa);
}
/* Swap bytes of 32-bit integer */
unsigned int swap_bytes(unsigned int value) {
union {
unsigned int i;
unsigned char bytes[4];
} input, output;
input.i = value;
output.bytes[0] = input.bytes[3];
output.bytes[1] = input.bytes[2];
output.bytes[2] = input.bytes[1];
output.bytes[3] = input.bytes[0];
return output.i;
}
/* Color representation */
union Color {
unsigned int rgba; /* 32-bit color */
struct {
unsigned char r;
unsigned char g;
unsigned char b;
unsigned char a;
} channels;
};
void manipulate_color(void) {
union Color color;
/* Set color as 32-bit value */
color.rgba = 0xFF00FF80; /* Purple with 50% alpha */
/* Access individual channels */
printf("R: %u, G: %u, B: %u, A: %u\n",
color.channels.r, color.channels.g,
color.channels.b, color.channels.a);
/* Modify channel */
color.channels.a = 255; /* Full opacity */
printf("New RGBA: 0x%08X\n", color.rgba);
}
/* IP address representation */
union IPAddress {
unsigned int addr; /* 32-bit address */
unsigned char octets[4];
};
void print_ip(unsigned int ip) {
union IPAddress addr;
addr.addr = ip;
printf("%u.%u.%u.%u\n",
addr.octets[0], addr.octets[1],
addr.octets[2], addr.octets[3]);
}Practical Union Applications
Unions excel in specific scenarios: variant types, memory-mapped I/O, protocol parsing, and embedded systems. Understanding these patterns helps you recognize when unions are the right tool.
/* Variant type for scripting language */
typedef enum {
VAL_NIL,
VAL_BOOL,
VAL_NUMBER,
VAL_STRING
} ValueType;
typedef struct {
ValueType type;
union {
int boolean;
double number;
char *string;
} as;
} Value;
void print_value(Value val) {
switch (val.type) {
case VAL_NIL:
printf("nil\n");
break;
case VAL_BOOL:
printf("%s\n", val.as.boolean ? "true" : "false");
break;
case VAL_NUMBER:
printf("%.2f\n", val.as.number);
break;
case VAL_STRING:
printf("\"%s\"\n", val.as.string);
break;
}
}
/* Network packet parsing */
union Packet {
unsigned char raw[1024];
struct {
unsigned char type;
unsigned char flags;
unsigned short length;
unsigned char data[1020];
} header;
};
void parse_packet(const unsigned char *buffer) {
union Packet packet;
memcpy(packet.raw, buffer, sizeof(packet.raw));
printf("Type: %u\n", packet.header.type);
printf("Flags: 0x%02X\n", packet.header.flags);
printf("Length: %u\n", packet.header.length);
}
/* Memory-mapped I/O register */
union IORegister {
unsigned int full;
struct {
unsigned int bit0 : 1;
unsigned int bit1 : 1;
unsigned int bit2 : 1;
unsigned int bit3 : 1;
unsigned int reserved : 28;
} bits;
};
void configure_register(volatile union IORegister *reg) {
/* Set individual bits */
reg->bits.bit0 = 1;
reg->bits.bit1 = 0;
reg->bits.bit2 = 1;
/* Or write entire register */
reg->full = 0x00000005;
}
/* JSON value representation */
typedef enum {
JSON_NULL,
JSON_BOOL,
JSON_NUMBER,
JSON_STRING,
JSON_ARRAY,
JSON_OBJECT
} JsonType;
typedef struct JsonValue JsonValue;
struct JsonValue {
JsonType type;
union {
int boolean;
double number;
char *string;
struct {
JsonValue *items;
int count;
} array;
struct {
char **keys;
JsonValue *values;
int count;
} object;
} as;
};
/* Event system */
typedef enum {
EVENT_MOUSE_CLICK,
EVENT_KEY_PRESS,
EVENT_WINDOW_RESIZE
} EventType;
typedef struct {
EventType type;
union {
struct {
int x;
int y;
int button;
} mouse_click;
struct {
int key_code;
int modifiers;
} key_press;
struct {
int width;
int height;
} window_resize;
} data;
} Event;
void handle_event(const Event *event) {
switch (event->type) {
case EVENT_MOUSE_CLICK:
printf("Mouse click at (%d, %d), button %d\n",
event->data.mouse_click.x,
event->data.mouse_click.y,
event->data.mouse_click.button);
break;
case EVENT_KEY_PRESS:
printf("Key pressed: %d, modifiers: 0x%X\n",
event->data.key_press.key_code,
event->data.key_press.modifiers);
break;
case EVENT_WINDOW_RESIZE:
printf("Window resized to %dx%d\n",
event->data.window_resize.width,
event->data.window_resize.height);
break;
}
}Union Best Practices
Using unions safely requires discipline. Always track which member is valid, use tagged unions for clarity, and document assumptions. Understand undefined behavior risks with type punning.
/* DON'T: Access invalid member */
union Bad {
int i;
float f;
} bad;
bad.i = 42;
printf("%f\n", bad.f); /* WRONG: f is invalid */
/* DO: Use tagged union */
typedef struct {
enum { IS_INT, IS_FLOAT } type;
union {
int i;
float f;
} value;
} Good;
Good good;
good.type = IS_INT;
good.value.i = 42;
if (good.type == IS_INT) {
printf("%d\n", good.value.i); /* Correct */
}
/* DON'T: Forget which member is active */
union Unclear {
int i;
float f;
} unclear;
unclear.i = 10;
/* ... lots of code ... */
printf("%f\n", unclear.f); /* Which member is valid? */
/* DO: Make intent clear */
typedef struct {
enum Type { TYPE_A, TYPE_B } tag;
union {
int a;
float b;
} data;
} Clear;
/* DON'T: Assume byte order */
union Endian {
int i;
char bytes[4];
} endian;
endian.i = 0x12345678;
/* Don't assume bytes[0] is 0x12 or 0x78 */
/* DO: Check endianness if needed */
int is_big_endian(void) {
union {
int i;
char bytes[sizeof(int)];
} test = {1};
return test.bytes[0] == 0;
}
/* DON'T: Type pun through pointer (undefined in C) */
int i = 42;
float f = *(float*)&i; /* Undefined behavior */
/* DO: Use union for type punning (defined behavior) */
union TypePun {
int i;
float f;
} pun;
pun.i = 42;
float f2 = pun.f; /* Defined behavior in C */
/* Guidelines */
/*
1. Always use tagged unions in production code
2. Document which member is active
3. Initialize unions completely
4. Be aware of padding and alignment
5. Test on target platform (endianness matters)
6. Use unions for memory savings only when needed
7. Prefer structures for clarity when memory isn't constrained
8. Understand strict aliasing rules
*/Summary & What's Next
Key Takeaways:
- ✅ Unions share memory among all members
- ✅ Only one member valid at a time
- ✅ Union size = largest member size
- ✅ Use tagged unions for safe access
- ✅ Unions enable type punning and bit manipulation
- ✅ Useful for variant types and memory-constrained systems
- ✅ Always track which member is active
- ✅ Prefer structures when memory isn't constrained