C Unions
Introduction to Unions
Unions are special data types in C that allow storing different data types in the same memory location. Unlike structures where each member has its own memory, union members share the same memory space.
Key characteristics of unions:
- All members share the same memory location
- The size of a union is at least the size of its largest member (and may include extra bytes to satisfy alignment)
- Only one member should be treated as containing a valid value at any given time
- Useful for memory efficiency and certain low-level reinterpretations (with portability caveats)
Union Definition and Declaration
Example
union union_name {
data_type member1;
data_type member2;
// ... more members
};
ℹ️ Note: The syntax is similar to structures but uses the union keyword instead of struct. Use designated initializers to specify which member is initialized.
Union vs Structure Memory Layout
Example
#include <stdio.h>
struct ExampleStruct {
int a;
float b;
char c;
};
union ExampleUnion {
int a;
float b;
char c;
};
int main(void) {
printf("Size of struct: %zu bytes\n", sizeof(struct ExampleStruct));
printf("Size of union: %zu bytes\n", sizeof(union ExampleUnion));
return 0;
}
Output
Size of struct: 12 bytes Size of union: 4 bytes
ℹ️ Note: The union size is at least that of its largest member (here, int/float are 4). The structure's size is roughly the sum of members plus any padding required for alignment. Exact sizes are implementation-dependent.
Basic Union Usage
⚠️ Warning: Reading a union member other than the most recently written one is undefined behavior (except for specific standard allowances). Treat only the last written member as valid.
Example
#include <stdio.h>
#include <string.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 = 220.5f;
printf("data.f: %.2f\n", data.f);
// Previous value (data.i) is no longer active
printf("data.i after assigning float: %d (undefined)\n", data.i);
strcpy(data.str, "C Programming");
printf("data.str: %s\n", data.str);
return 0;
}
Output
data.i: 10 data.f: 220.50 data.i after assigning float: 1122360320 (undefined) data.str: C Programming
Practical Example: Tagged (Discriminated) Union
Example
#include <stdio.h>
enum Type { INT, FLOAT, CHAR };
struct Variant {
enum Type type;
union {
int i;
float f;
char c;
} value;
};
void printVariant(const struct Variant *v) {
switch (v->type) {
case INT: printf("Integer: %d\n", v->value.i); break;
case FLOAT: printf("Float: %.2f\n", v->value.f); break;
case CHAR: printf("Char: %c\n", v->value.c); break;
}
}
int main(void) {
struct Variant var1 = { INT, { .i = 10 } };
struct Variant var2 = { FLOAT, { .f = 3.14f } };
struct Variant var3 = { CHAR, { .c = 'A' } };
printVariant(&var1);
printVariant(&var2);
printVariant(&var3);
return 0;
}
Output
Integer: 10 Float: 3.14 Char: A
ℹ️ Note: This pattern tracks which union member is currently active, making usage safe and explicit.
Union with Structure Members
Example
#include <stdio.h>
struct Point2D { int x, y; };
struct Point3D { int x, y, z; };
union Point {
struct Point2D p2d;
struct Point3D p3d;
};
int main(void) {
union Point point;
// Use as 2D point
point.p2d.x = 10;
point.p2d.y = 20;
printf("2D Point: (%d, %d)\n", point.p2d.x, point.p2d.y);
// Now treat as 3D point (overwrites the same storage)
point.p3d.x = 5;
point.p3d.y = 15;
point.p3d.z = 25;
printf("3D Point: (%d, %d, %d)\n", point.p3d.x, point.p3d.y, point.p3d.z);
return 0;
}
Output
2D Point: (10, 20) 3D Point: (5, 15, 25)
Type Punning with Unions
⚠️ Warning: Reading a different union member than the one most recently written is not strictly portable. Many compilers support this as an extension, but the portable approach for reinterpreting bytes is to use memcpy.
Example
#include <stdio.h>
#include <string.h>
union Converter { float f; unsigned int u; };
void printFloatBits(float num) {
union Converter c; c.f = num;
printf("Float: %.2f\n", c.f);
printf("Binary representation: 0x%08X\n", c.u);
unsigned int sign = (c.u >> 31) & 1u;
unsigned int exponent = (c.u >> 23) & 0xFFu;
unsigned int mantissa = c.u & 0x7FFFFFu;
printf("Sign: %u\n", sign);
printf("Exponent: 0x%02X (%u)\n", exponent, exponent);
printf("Mantissa: 0x%06X\n", mantissa);
}
int main(void) {
printFloatBits(1.0f);
printf("\n");
printFloatBits(-2.5f);
// Portable alternative using memcpy:
float f = 1.0f; unsigned int u = 0;
memcpy(&u, &f, sizeof u);
printf("\nPortable memcpy bits: 0x%08X\n", u);
return 0;
}
Output
Float: 1.00 Binary representation: 0x3F800000 Sign: 0 Exponent: 0x7F (127) Mantissa: 0x000000 Float: -2.50 Binary representation: 0xC0200000 Sign: 1 Exponent: 0x80 (128) Mantissa: 0x200000 Portable memcpy bits: 0x3F800000
Best Practices
- Use a tagged union (add an enum tag) to track which member is active.
- Remember union size/alignment is based on the largest/strictest member; exact values are implementation-dependent.
- Do not read from a member that wasn't the last one written (undefined behavior).
- Prefer memcpy over union-based type punning for portability.
- Be careful when using unions in serialized data formats—padding/alignment can differ across platforms.
- Initialize unions with designated initializers to make the active member explicit.