C Unions — Shared Memory, Type Punning, and Union vs Struct
In this tutorial, you will learn about C Unions. We cover key concepts, practical examples, and best practices to help you master this topic.
C unions are user-defined types where all members share the same memory location, allowing you to store different data types in the same space, with the size determined by the largest member.
Why It Matters
Unions provide memory efficiency and enable type punning -- interpreting the same bytes as different types. They are essential in systems programming for protocol Parsing, hardware register access, and variant data structures. Understanding unions deepens your knowledge of how C maps types to memory.
Real-World Use
Network protocol parsers use unions to interpret packet headers in different ways. Hardware drivers map registers to union members for byte and word access. JSON parsers use unions for variant values (string, number, boolean). The X11 protocol uses unions for event types.
What You Will Learn
- Declaring and using union types
- How unions share memory between members
- Type punning with unions
- Union vs struct differences
- Anonymous unions (C11+)
Learning Path
flowchart LR A[Structs] --> B[Unions
You are here] B --> C[Bit Fields] C --> D[Typedef] D --> E[Void Pointers] style B fill:#f90,color:#fff
What Is a Union?
A union stores all its members at the same memory address. The size is the size of the largest member. Writing to one member overwrites the others:
#include <stdio.h>
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
printf("Size of union: %zu bytes\n", sizeof(data));
// Size is 20 (largest member: str[20])
data.i = 42;
printf("As int: %d\n", data.i);
data.f = 3.14159f; // Overwrites data.i
printf("As float: %f\n", data.f);
printf("As int (corrupted): %d\n", data.i); // Garbage
return 0;
}
Expected output:
Size of union: 20 bytes
As int: 42
As float: 3.141590
As int (corrupted): 1078530011
The key insight: all members share the same bytes. When you write to data.f, you overwrite what was stored in data.i. Reading data.i after writing data.f gives you the raw bytes of the float interpreted as an integer.
Union vs Struct
#include <stdio.h>
struct StructExample {
int i;
float f;
char str[20];
};
union UnionExample {
int i;
float f;
char str[20];
};
int main() {
struct StructExample s = {42, 3.14f, "Hello"};
union UnionExample u;
printf("Struct size: %zu\n", sizeof(s));
printf("Union size: %zu\n", sizeof(u));
printf("Struct members are independent:\n");
printf(" i = %d\n", s.i);
printf(" f = %f\n", s.f);
printf(" str = %s\n", s.str);
printf("Union members share memory:\n");
u.i = 42;
printf(" int = %d\n", u.i);
u.f = 3.14f;
printf(" float = %f\n", u.f);
printf(" int (overwritten) = %d\n", u.i);
return 0;
}
Expected output:
Struct size: 28
Union size: 20
Struct members are independent:
i = 42
f = 3.140000
str = Hello
Union members share memory:
int = 42
float = 3.140000
int (overwritten) = 1078523331
| Aspect | Struct | Union |
|---|---|---|
| Memory layout | Members stored sequentially | All members at same address |
| Size | Sum of all members (plus padding) | Size of largest member |
| Member access | All members available simultaneously | Only last written member is valid |
| Use case | Grouping related data | Variant types, type punning |
Type Punning with Unions
Type punning means interpreting the same bytes as different types. Unions provide a standard-compliant way to do this:
#include <stdio.h>
union FloatInt {
float f;
unsigned int u;
};
int main() {
union FloatInt fi;
fi.f = 3.14159f;
printf("Float: %f\n", fi.f);
printf("As hex: 0x%08X\n", fi.u);
// Inspect individual bytes
unsigned char *bytes = (unsigned char*)&fi;
printf("Bytes: ");
for (int i = 0; i < sizeof(float); i++) {
printf("%02X ", bytes[i]);
}
printf("\n");
// Modify bits via int access
fi.u = 0x40490FDB; // Pi in IEEE 754
printf("Reinterpreted as float: %f\n", fi.f);
return 0;
}
Expected output:
Float: 3.141590
As hex: 0x40490FDB
Bytes: DB 0F 49 40
Reinterpreted as float: 3.141590
Why Type Punning Matters
- Debugging: inspect the binary representation of floating-point numbers
- Networking: interpret raw bytes as protocol fields
- Graphics: extract color components from packed integers
- Compression: manipulate header bits directly
Unions with Struct Members
Unions become powerful when combined with structs:
#include <stdio.h>
#include <string.h>
// Variant type: can hold different "types" of value
typedef struct {
int type; // 0 = int, 1 = float, 2 = string
union {
int i;
float f;
char str[50];
} value;
} Variant;
void print_variant(Variant v) {
switch (v.type) {
case 0:
printf("Int: %d\n", v.value.i);
break;
case 1:
printf("Float: %f\n", v.value.f);
break;
case 2:
printf("String: %s\n", v.value.str);
break;
}
}
int main() {
Variant v1 = {0, .value.i = 42};
Variant v2 = {1, .value.f = 3.14f};
Variant v3 = {2};
strcpy(v3.value.str, "Hello Unions");
print_variant(v1);
print_variant(v2);
print_variant(v3);
return 0;
}
Expected output:
Int: 42
Float: 3.140000
String: Hello Unions
This pattern is common in dynamic languages, configuration systems, and protocol parsers where the type of data varies at runtime.
Anonymous Unions (C11+)
C11 introduced anonymous unions that do not require a member name:
#include <stdio.h>
typedef struct {
int type;
union { // Anonymous union
int i;
float f;
char *s;
}; // No member name needed
} Value;
int main() {
Value v;
v.type = 0;
v.i = 42; // Direct access, no intermediate name
printf("Value: %d\n", v.i);
v.type = 1;
v.f = 2.718f;
printf("Value: %f\n", v.f);
return 0;
}
Anonymous unions eliminate the extra dot in member access. GCC and Clang support them; check your compiler for C11 Compliance.
Checking Endianness with Unions
Unions can determine the byte order (endianness) of your system:
#include <stdio.h>
union EndianCheck {
unsigned int word;
unsigned char bytes[4];
};
int main() {
union EndianCheck ec;
ec.word = 0x12345678;
printf("Word: 0x%08X\n", ec.word);
printf("Bytes: ");
for (int i = 0; i < 4; i++) {
printf("0x%02X ", ec.bytes[i]);
}
printf("\n");
if (ec.bytes[0] == 0x78) {
printf("System: Little-Endian\n");
} else if (ec.bytes[0] == 0x12) {
printf("System: Big-Endian\n");
}
return 0;
}
Expected output on x86 (Little-Endian):
Word: 0x12345678
Bytes: 0x78 0x56 0x34 0x12
System: Little-Endian
Common Mistakes
1. Reading Wrong Union Member
union { int i; float f; } u;
u.f = 3.14f;
printf("%d", u.i); // Reads float bits as int -- usually wrong
Only read the member that was last written. Use a discriminator (tag) to track the active member.
2. Assuming Union Clears Other Members
u.i = 42;
u.f = 3.14f; // u.i is now corrupted
Writing to one member overwrites all others. Initialize the union when you change the active member.
3. Using Union for Large Type Punning (Undefined Behavior)
float f = 3.14f;
int i = *(int*)&f; // Strict aliasing violation!
Unions provide the only standard-compliant way to type-pun in C.
4. Forgetting About Padding
union { char a; int b; } u;
// Size is sizeof(int) = 4, not 1
The union size is the largest member's size, including its padding.
5. Not Using Discriminator (Tag) Field
union Value { int i; float f; };
void process(union Value v) {
// How do we know if v holds int or float?
}
Always pair a union with a tag field that tracks the active member type.
Practice Questions
What is the size of a union? The size of its largest member, including any padding required.
How are union members stored in memory? All members start at the same memory address. They overlap in memory.
What is the difference between a struct and a union? Struct members are stored sequentially in memory. Union members overlap at the same address.
What is a tagged union? A union combined with a discriminator (tag) field that tracks which member is currently active.
Challenge: Write a program that uses a union to store different shape types (circle, rectangle, triangle) with appropriate data for each.
Mini Project: Shape Union
#include <stdio.h>
#include <math.h>
typedef enum { CIRCLE, RECTANGLE, TRIANGLE } ShapeType;
typedef struct {
ShapeType type;
union {
struct { double radius; } circle;
struct { double width, height; } rectangle;
struct { double a, b, c; } triangle;
} shape;
} Shape;
double area(Shape s) {
switch (s.type) {
case CIRCLE:
return M_PI * s.shape.circle.radius * s.shape.circle.radius;
case RECTANGLE:
return s.shape.rectangle.width * s.shape.rectangle.height;
case TRIANGLE: {
double p = (s.shape.triangle.a + s.shape.triangle.b
+ s.shape.triangle.c) / 2.0;
return sqrt(p * (p - s.shape.triangle.a)
* (p - s.shape.triangle.b)
* (p - s.shape.triangle.c));
}
}
return 0;
}
int main() {
Shape shapes[3];
shapes[0] = (Shape){CIRCLE, .shape.circle = {5.0}};
shapes[1] = (Shape){RECTANGLE, .shape.rectangle = {4.0, 6.0}};
shapes[2] = (Shape){TRIANGLE, .shape.triangle = {3.0, 4.0, 5.0}};
for (int i = 0; i < 3; i++) {
printf("Shape %d area: %.2f\n", i + 1, area(shapes[i]));
}
return 0;
}
FAQ
What is Next
Now that you understand unions, proceed to Bit Fields to learn how to pack data into individual bits within structs.