C Unions
A union in C is a user-defined type whose members share the same storage. Unlike a structure, which stores all members at the same time, a union stores one meaningful member at a time. Unions matter when a value can have several possible shapes, but only one shape is active for any particular object.
Overview: How C Unions Work
A union looks similar to a struct, but its memory layout is very different. In a structure, each member has its own place in memory. In a union, all members begin at the same address and overlap. The size of the union is large enough to hold its largest member, plus any padding needed for alignment.
For example, a union with an int, a double, and a char array does not reserve space for all three values separately. It reserves one shared block big enough for the largest of them. If you store an int and then store a double, the bytes used for the int are overwritten by the bytes for the double.
This shared storage is powerful but dangerous if you do not track which member is currently valid. C does not automatically remember the active member for you. The programmer must know whether the union currently contains an integer, a floating-point value, a string, or some other member. In real programs, unions are often paired with an enum tag inside a struct. The tag says what kind of value is stored, and the union stores the actual data.
Unions are useful for compact data representations, interpreters, parsers, message formats, device protocols, and APIs where one object can contain one of several alternatives. They are not a general replacement for structures. Use a structure when values must exist together. Use a union when alternatives share one storage location because only one alternative is meaningful at a time.
Syntax
union Value {
int count;
double price;
char label[20];
};
| Part | Meaning |
|---|---|
union |
The keyword that declares a union type. |
Value |
The union tag. The full type name is union Value. |
| Members | Alternative views of the same storage. Only one should be treated as active at a time. |
| Semicolon | Required after the closing brace because this is a declaration. |
Declare a variable with union Value item;. Access members with the dot operator, such as item.count, or with the arrow operator through a pointer, such as ptr->count. You can initialize the first member with a simple initializer like { 10 }, or initialize a named member with a designated initializer like { .price = 19.99 }.
Examples
A Basic Union
#include <stdio.h>
union Value {
int count;
double price;
char grade;
};
int main(void) {
union Value value;
value.count = 7;
printf("count: %d
", value.count);
value.price = 12.50;
printf("price: %.2f
", value.price);
value.grade = 'A';
printf("grade: %c
", value.grade);
return 0;
}
Output:
count: 7
price: 12.50
grade: A
The same union variable is used three times. After value.count = 7, the count member is the meaningful member. After value.price = 12.50, the price bytes occupy the same storage and the old count should no longer be used. The program only prints the member most recently written.
A Tagged Union for Different Field Values
#include <stdio.h>
#define TEXT_SIZE 20
enum FieldKind {
FIELD_INT,
FIELD_DOUBLE,
FIELD_TEXT
};
union FieldValue {
int i;
double d;
char text[TEXT_SIZE];
};
struct Field {
char name[16];
enum FieldKind kind;
union FieldValue value;
};
void print_field(struct Field field) {
printf("%s = ", field.name);
switch (field.kind) {
case FIELD_INT:
printf("%d\n", field.value.i);
break;
case FIELD_DOUBLE:
printf("%.2f\n", field.value.d);
break;
case FIELD_TEXT:
printf("%s\n", field.value.text);
break;
}
}
int main(void) {
struct Field fields[] = {
{"retries", FIELD_INT, {.i = 3}},
{"timeout", FIELD_DOUBLE, {.d = 2.50}},
{"mode", FIELD_TEXT, {.text = "debug"}}
};
int count = (int)(sizeof fields / sizeof fields[0]);
for (int i = 0; i < count; i++) {
print_field(fields[i]);
}
return 0;
}
Output:
retries = 3
timeout = 2.50
mode = debug
This is the most important union pattern: a structure combines an enum tag with a union payload. The kind member tells the program which union member to read. The switch keeps the reads matched to the writes, so the integer field is printed as an integer, the double field as a double, and the text field as a string.
Commands With Different Payloads
#include <stdio.h>
enum CommandType {
CMD_MOVE,
CMD_RESIZE,
CMD_SET_VISIBLE
};
struct MoveData {
int dx;
int dy;
};
struct ResizeData {
int width;
int height;
};
union CommandData {
struct MoveData move;
struct ResizeData resize;
int visible;
};
struct Command {
enum CommandType type;
union CommandData data;
};
void run_command(struct Command command) {
switch (command.type) {
case CMD_MOVE:
printf("move by %d,%d\n", command.data.move.dx, command.data.move.dy);
break;
case CMD_RESIZE:
printf("resize to %dx%d\n", command.data.resize.width, command.data.resize.height);
break;
case CMD_SET_VISIBLE:
printf("visible: %s\n", command.data.visible ? "yes" : "no");
break;
}
}
int main(void) {
struct Command commands[] = {
{CMD_MOVE, {.move = {5, -2}}},
{CMD_RESIZE, {.resize = {800, 600}}},
{CMD_SET_VISIBLE, {.visible = 1}}
};
int count = (int)(sizeof commands / sizeof commands[0]);
for (int i = 0; i < count; i++) {
run_command(commands[i]);
}
return 0;
}
Output:
move by 5,-2
resize to 800x600
visible: yes
Each command has the same outer type, but its payload depends on the command kind. A move command needs dx and dy, a resize command needs width and height, and a visibility command needs one integer flag. The union avoids wasting space on payload fields that do not apply to the current command.
How It Works Step by Step
When the compiler sees a union definition, it calculates the size and alignment needed for each member. The union’s size becomes at least the size of its largest member. Its alignment must satisfy the strictest alignment requirement among its members. Every member has offset zero, meaning every member starts at the beginning of the union object.
When you write to a union member, C stores that value’s representation into the shared storage. If the member is an array, such as char text[20], the array bytes are part of the union storage. If the member is a structure, the complete structure object is stored in that same shared region.
Member access is still ordinary member access syntax. The expression field.value.d first accesses the value member of a structure, then accesses the d member of the union. The important rule is logical, not syntactic: read the same member you most recently wrote, unless you are using a carefully defined low-level technique that you fully understand.
Because the compiler does not store an automatic tag, a plain union variable can be ambiguous. If a function receives only union FieldValue, it cannot know whether to print i, d, or text. If the function receives struct Field, the tag and value travel together, which makes the code safer and easier to maintain.
Common Mistakes
Reading the Wrong Member
A common mistake is to store one member and read another, such as writing value.i and then printing value.d. The result is not a numeric conversion. It interprets the stored bytes as a different type, which can produce meaningless results and can even be undefined for some representations. Convert values with casts or assignment to the correct type instead of using a union as a conversion shortcut.
Forgetting the Tag
A union by itself does not know what it contains. If a value can be one of several types, store an enum tag next to the union and keep the tag updated every time you write a new member. Most safe union designs in application code are tagged unions.
Expecting All Members to Exist at Once
If you need a name, an id, and a score at the same time, use a struct, not a union. A union is for alternatives. Writing the id member after the name member overwrites the same storage, so the name is no longer the active value.
Assigning to an Array Member After Initialization
If a union contains char text[20], you cannot later write value.text = "hello"; because arrays are not assignable in C. Use initialization, strcpy, snprintf, or character-by-character copying with enough space for the null terminator.
Best Practices
- Use a
structplus anenumtag for most unions that represent different value kinds. - Keep the tag and active union member synchronized in the same function or constructor-style helper.
- Use designated initializers such as
{.d = 2.50}so it is clear which member starts active. - Prefer structures when all fields are needed at the same time; prefer unions only for alternatives.
- Do not use unions as a casual type-conversion trick. Use casts, parsing functions, or well-defined byte-copying techniques when conversion is the goal.
- Document ownership rules when a union member is a pointer. Switching active members does not automatically free memory.
- Keep union alternatives small and related. If the alternatives need very different behavior, separate types and functions may be clearer.
Practice Exercises
- Define a tagged union that can store either an integer temperature or a text status such as
"offline". Print the value based on its tag. - Create a
Messagestructure with a tag and a union payload forlogin,logout, anderror_codemessages. - Write a function that changes a tagged union from a double value to an integer value and updates the tag at the same time.
Summary
- A union stores all members in the same memory, so only one member should be treated as active at a time.
- The size of a union is large enough for its largest member, with alignment chosen for all members.
- Use
.for union objects and->for pointers to unions, just like structures. - C does not automatically track the active member; pair unions with an
enumtag when the type can vary. - Reading a different member from the one most recently written is not a normal conversion and is a common source of bugs.
- Unions are best for compact alternatives such as values, commands, tokens, and protocol payloads.
