1. The Core Idea: struct vs. union
A union is a user-defined data type just
like a struct, but it has one *massive* difference in how it stores
memory.
·
A struct is
an "AND" container. It holds *all* of its members at the same time. A struct Student holds a name *AND* a
roll number *AND* a GPA.
·
A union is
an "OR" container. It can only hold *one* of its members at any given
time. A union Value can
hold an int *OR*
a float *OR*
a char, but
never all at once.
Analogy: Toolbox vs. Single Box
struct (Toolbox): A struct is like a large toolbox with separate, labeled
compartments. There is a compartment for a hammer, a compartment for a
screwdriver, and a compartment for a wrench. All three can be in the toolbox at
the same time in their own space.
union (Single Box): A union is like a single, small box. The box is just big enough to
hold *either* the hammer, *or* the screwdriver, *or* the wrench. If you put the
hammer in, you must take the screwdriver out. All members share
the same space.
2. How union Manages Memory
This is the most
important part to understand. A struct allocates enough memory to hold *all* its members
added together. A union allocates only enough memory to hold its largest
member.
All members of a union start at the same memory
address.
Let's compare a struct and a union with the same members:
// STRUCT: Total size is AT LEAST 4 + 1 + 8 = 13 bytes
// (plus padding, so maybe 16 bytes)
struct DataStruct {
int i; // 4 bytes
char c; // 1 byte
double d; // 8 bytes
}; // UNION: Total size is sizeof(largest_member) = sizeof(double) = 8 bytes
union DataUnion {
int i; // Will use the first 4 bytes of the 8-byte block
char c; // Will use the first 1 byte of the 8-byte block
double d; // Will use all 8 bytes
}; int main() {
printf("Size of struct: %lu bytes\n", sizeof(struct DataStruct));
printf("Size of union: %lu bytes\n", sizeof(union DataUnion));
}Output (typical):
Size of struct: 16 bytes (13 bytes + 3 bytes of padding for
alignment)
Size of union: 8 bytes
3. Defining and Using a Union
The syntax for
defining and accessing a union is identical to a struct. You use the union keyword and access members with the dot
operator (.) or arrow operator (->) for
pointers.
The Golden Rule of Unions
You can only safely read from the member
that you most recently wrote to. If you write to i and then try to read from d, you will get garbage data. The union has no idea which member is
"active" — that is your job as the
programmer.
Example: The "Wrong" Way (Causing Data Corruption)
This example shows
what happens when you don't follow the golden rule. You are telling C,
"Take the bits that represent the integer 123456789 and *pretend* they are
a double."
#include <stdio.h>
union DataUnion {
int i;
char c;
double d;
}; int main() {
// Create a union variable
union DataUnion data;
// 1. Write to the integer 'i'
data.i = 123456789;printf("As integer: %d\n", data.i); // OK
// 2. Now read from 'c' and 'd' (DANGEROUS!)
// This just reads the first byte of 'i'
printf("As char: %c\n", data.c); // Garbage
// This re-interprets the bits of 'i' as a double
printf("As double: %f\n", data.d); // Garbage
printf("---\n");
// 3. Now write to the char 'c'
// This OVERWRITES the first byte of 'i'
data.c = 'A';
printf("As char: %c\n", data.c); // OK
// 4. Read from 'i' again. Its value has been corrupted!
printf("As integer: %d\n", data.i); // No longer 123456789
return 0;
}4. Why Use a Union? (Practical Use Case)
Unions are powerful for two main reasons: saving memory and "type punning."
Use Case 1: Saving Memory
Imagine you are
creating a data type Value that can hold *either* an integer, a float, or a string. If
you used a struct, you would waste a lot of space. A union is perfect.
But how do you
know which member is active? You must store that information separately. This
is usually done by wrapping the union inside a struct!
This is a very common and important C programming pattern:
#include <stdio.h>
#include <string.h>
// 1. Create 'tags' to identify the active type
typedef enum {
TYPE_INT, TYPE_FLOAT, TYPE_STRING} ValueType; // 2. Define the union to hold the data
typedef union {
int i;
float f;
char s[50];
} ValueData; // 3. Wrap them both in a STRUCT
typedef struct {
ValueType type; // The tag telling us what's active
ValueData data; // The union holding the actual value
} Value; // 4. Create a function to print the Value safely
void printValue(Value v) {
switch (v.type) {
case TYPE_INT:
printf("Value is an integer: %d\n", v.data.i);
break;
case TYPE_FLOAT:
printf("Value is a float: %.2f\n", v.data.f);
break;
case TYPE_STRING:
printf("Value is a string: %s\n", v.data.s);
break;
}} int main() {
Value v1, v2; // Create an integer Value
v1.type = TYPE_INT; v1.data.i = 100; // Create a string Value
v2.type = TYPE_STRING;strcpy(v2.data.s, "Hello World");
// Now we can safely print them
printValue(v1);
printValue(v2);
return 0;
}Use Case 2: Type Punning (Advanced)
Sometimes you
*want* to write as one type and read as another. This is an advanced (and often
dangerous) technique called "type punning," used to inspect the raw
binary representation of a value. For example, to see the bits of a float as an int.
union TypePun {
float f;
unsigned int u; // Must be same size as float (4 bytes)
}; union TypePun pun;
pun.f = -12.5; // This prints the raw IEEE 754 bit pattern for -12.5
printf("Bits of -12.5 are: 0x%X\n", pun.u);