C / STRUCTS, UNIONS AND ENUMS
Unions and the one-member-at-a-time rule
Use C unions to give one block of storage several typed views, and track which member is live so you never read a value the bytes do not hold.
What you will learn
- Predict a union's size: the largest member, not the sum of the members
- Track the active member yourself; only the last one written reads back a real value
- Choose a member at initialisation with a designated initialiser like {.level = 0.25}
- Read raw bytes through an unsigned char[] member and explain why the order varies
Understanding Unions and the one-member-at-a-time rule
A struct lays its members out one after another; a union lays them all on top of each other. Every member of a union begins at the union's own address, so the compiler only reserves room for the largest member, rounded up to satisfy alignment. That is why a union of an int, a float and a char[8] has size 8 rather than 16. The mental model is a single fixed block of bytes with several typed windows onto it, not several fields sitting side by side.
Because the bytes are shared, writing a member overwrites whatever was there before, and the member written last is the only one whose value is defined. C keeps no hidden tag recording which member that is, so the bookkeeping is entirely yours. Switching members does not convert anything either: reading v.f after writing v.i re-decodes the same bits under float rules, so what you get depends on member sizes, byte order and the floating-point format of the machine.
Two failure modes follow directly from that. If the member you read is wider than the member you wrote, part of what you read was never assigned at all, so the value is indeterminate rather than merely surprising. If it is the same width, the value is a reinterpretation whose meaning is implementation-defined and may even be a trap representation for a type like float; the one dependable reinterpretation is through an unsigned char array member, because unsigned char has no padding bits and no trap representations. Initialisation obeys the same one-member logic: a plain brace list initialises the first member, and a designated initialiser such as {.f = 2.5f} picks which member starts out active.
placeholder
<stdio.h>
union Value {
int i;
float f;
char tag[8];
};
int main(void)
{
union Value v;
v.i = 1000;
printf("wrote i: v.i = %d\n", v.i);
v.f = 2.5f; /* same bytes, new active member */
printf("wrote f: v.f = %g\n", v.f);
printf("v.i now reinterprets those bytes: %d\n", v.i);
printf("sizes: int %zu, float %zu, tag %zu, union %zu\n",
sizeof v.i, sizeof v.f, sizeof v.tag, sizeof v);
printf("all members start at one address: %d\n",
(void *)&v.i == (void *)&v.tag);
return 0;
}
A union is one block of storage seen through several member types, so only the member written most recently holds a defined value.
Worked examples
Which member does an initialiser reach
Shows that a plain brace list initialises the first member, that a designator selects a different one, and that union assignment carries the whole storage over.
<stdio.h>
union Reading {
long counter;
double level;
};
int main(void)
{
union Reading a = {7}; /* first member */
union Reading b = {.level = 0.25}; /* named member */
printf("a.counter = %ld\n", a.counter);
printf("b.level = %.2f\n", b.level);
a = b; /* copies the shared bytes */
printf("a.level = %.2f\n", a.level);
return 0;
}
Example explained
Line 1{7} carries no designator, so it initialises counter; a brace list can never reach a later member without naming it.
Line 2{.level = 0.25} names the member, so level is the active one and counter is left holding a double's bit pattern.
Line 3a = b copies the union's storage as a whole, so the active member effectively travels with the bytes and a.level reads back 0.25.
A byte-level window on an int
Uses an unsigned char member to look at the individual bytes of a value stored through another member, and reads byte order off the result.
<stdio.h>
union Bytes {
unsigned int u;
unsigned char b[sizeof(unsigned int)];
};
int main(void)
{
union Bytes x;
size_t i;
x.u = 0x01020304u;
for (i = 0; i < sizeof x.b; i++)
printf("b[%zu] = %02X\n", i, x.b[i]);
return 0;
}
Example explained
Line 1x.u = 0x01020304u writes four bytes, and b[] is a view of those same bytes, not a copy of them.
Line 2Sizing b as unsigned char[sizeof(unsigned int)] keeps the loop inside the union's storage even if int is not four bytes.
Line 3The low byte 04 shows up at index 0, so this machine stores the least significant byte first; a big-endian machine prints 01 03... in the reverse order.
Line 4Reading through unsigned char is the safe direction: it has no padding bits and no trap representations, so every byte value is meaningful.
Important notes
The reinterpreted numbers above (1075838976, and byte order 04 03 02 01) are x86-64 results: 4-byte int, IEEE-754 float, little-endian. Different representations print different numbers, which is precisely why member switching is not a portable conversion.
A union's size can exceed its largest member when trailing padding is needed for alignment, so do not assume sizeof(union) equals sizeof of the biggest member on every target.
Common mistakes
Treating a member switch as a conversion: after u.i = 1000, reading u.f gives a denormal near 1.4e-42, not 1000.0, because the bits are simply re-decoded.
Writing a narrow member and reading a wide one, such as setting a char member then reading a double: the remaining bytes were never assigned, so the value is indeterminate and can differ between builds.
Storing two live values in one union, for example a counter in .i and a scratch float in .f: the second write destroys the first, and the union offers nothing that can tell you which member is current.
Try it yourself
Change, predict, then run
Declare union Mix { unsigned int u; unsigned char b[4]; float f; }, set u to 0x40490FDBu, then print f with %f and all four bytes with %02X. From the byte order you see, state whether your machine is little- or big-endian.
Open the C workspaceCheck your understanding
A program sets u.i = 1 on a union of int and float, then prints u.f with %f and gets a tiny number instead of 1.000000. What explains it?
- u.f reads the same four bytes that hold the integer 1 and decodes them under float rules, which gives a denormal value
- The union only allocated space for its first member, so writing i left f pointing past the union's storage
- Assigning to u.i clears u.f to zero, and printf rounds that to a very small nonzero number
- %f expects a double, so the float argument is promoted incorrectly and the value is lost
Show answer
Union members share one block of storage and no conversion happens when you change members, so the integer's bit pattern 0x00000001 is read as a float and comes out around 1.4e-42. The %f answer is tempting because the double rule is real, but printf's default argument promotion converts float to double correctly, so it is not the cause here.