Unions and bit-fields

A union has the members of a struct but shares one storage area for all of them: only one member is valid at a time, and the size is the largest member. Bit-fields go the other way and split one integer into named fields of a few bits. Both are tools for the same problem, representing a value that has several readings.

One storage, several views

Writing the integer member and reading the byte array is the standard demonstration. The four bytes of the integer appear in memory order, and on x86_64 that order is little-endian: the least significant byte comes first. The union itself takes four bytes, the size of its largest member.

#include <stdio.h> union Word { unsigned int value; unsigned char bytes[4]; }; int main(void) { union Word word; word.value = 0x01020304u; printf("value: %08X\n", word.value); printf("bytes: %u %u %u %u\n", word.bytes[0], word.bytes[1], word.bytes[2], word.bytes[3]); printf("size: %zu\n", sizeof word); return 0; }

gcc -Wall -Wextra union.c -o union
./union

value: 01020304
bytes: 4 3 2 1
size: 4

The bytes are 4, 3, 2, 1 from the lowest address, which is exactly the little-endian layout of 0x01020304. On a big-endian machine the same program would print 1, 2, 3, 4, which is why a file format defines its byte order instead of writing a struct directly.

A tagged union

A union alone cannot say which member is valid; a tag next to it can. The struct holds an int tag and a union payload, and the print function reads the tag before the member. This is how a dynamically typed value is represented in C, one field for the type and one for the data.

#include <stdio.h> struct Value { int tag; union { int number; char letter; } data; }; static void print(const struct Value *value) { if (value->tag == 1) { printf("number: %d\n", value->data.number); } else { printf("letter: %c\n", value->data.letter); } } int main(void) { struct Value a = {.tag = 1, .data.number = 42}; struct Value b = {.tag = 2, .data.letter = 'x'}; print(&a); print(&b); return 0; }

gcc -Wall -Wextra tagged.c -o tagged
./tagged

number: 42
letter: x

The tag values 1 and 2 would be better as an enumeration, which is the next lesson. The union is what makes the value one object of fixed size: the number and the letter share the same bytes, so a struct Value is the same size whatever it holds.

Bit-fields

A bit-field member declares how many bits it occupies inside an integer. Three one-bit fields fit in one unsigned int, and the struct takes four bytes in total. The fields behave like small unsigned integers: assignment and printing work as usual.

#include <stdio.h> struct Flags { unsigned int readable : 1; unsigned int writable : 1; unsigned int executable : 1; }; int main(void) { struct Flags flags = {1, 0, 0}; flags.writable = 1; printf("read %u write %u exec %u\n", flags.readable, flags.writable, flags.executable); printf("size: %zu\n", sizeof flags); return 0; }

gcc -Wall -Wextra bits.c -o bits
./bits

read 1 write 1 exec 0
size: 4

The size is four bytes for three bits because the storage unit is an unsigned int. A packed file header often uses bit-fields for its flags, but the standard leaves their order and padding to the implementation, which the next section shows.

Which bits are where

A union with a bit-field struct and a plain integer shows the layout. On this compiler the first field occupies the least significant bits, so three bits of 5 and five bits of 3 give 29. Another compiler could assign the fields from the top down.

#include <stdio.h> union Packed { struct { unsigned int low : 3; unsigned int high : 5; } parts; unsigned int value; }; int main(void) { union Packed packed; packed.value = 0; packed.parts.low = 5; packed.parts.high = 3; printf("value: %u\n", packed.value); return 0; }

gcc -Wall -Wextra layout.c -o layout
./layout

value: 29

29 is 5 + (3 << 3), so low is the bottom three bits and high follows it. The value proves the layout on this machine, and the same program on another ABI may print a different number; a file format therefore uses shifts and masks from lesson 11 instead of bit-fields.

Too many bits

A field of four bits holds values from 0 to 15. Storing 300 does not overflow the struct; it truncates to the low four bits, which is 12, and GCC reports the change. The warning is the only sign that the constant does not fit:

#include <stdio.h> struct Flags { unsigned int value : 4; }; int main(void) { struct Flags flags; flags.value = 300; printf("%u\n", flags.value); return 0; }

gcc -Wall -Wextra toowide.c -o toowide
./toowide

toowide.c: In function 'main':
toowide.c:11:19: warning: unsigned conversion from 'int' to 'unsigned char:4' changes value from '300' to '12' [-Woverflow]
11 | flags.value = 300;
| ^~~
12

300 is 0x12C, and the low four bits are 0xC, which is 12. The printed value confirms the truncation. A field that must hold a bigger range needs more bits, and the width is part of the struct definition, not of the assignment.

Exercises

  1. Use a union to print the two bytes of a short holding 0x1234 and explain the order you see.
  2. Add a third case to the tagged union, a double, and update the print function.
  3. Pack a date into bit-fields: 7 bits for the year since 2000, 4 for the month and 5 for the day. Print the size of the struct and decode a date.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. How much storage does a union reserve?

    Answer

    The size of its largest member

    All members share one storage area, so the union is as large as the biggest member.

  2. What does a tag add to a union?

    Answer

    A way to know which member is valid

    The union alone cannot say which member was written, so a tag next to it records the active kind.

  3. What does the standard guarantee about bit-field order and padding?

    Answer

    The layout is implementation-defined

    The order, padding and storage unit are left to the implementation, which is why files use shifts and masks instead.

  4. What happens when 300 is stored in a four-bit unsigned field?

    Answer

    The low four bits remain, which is 12

    A four-bit field keeps the low four bits, so 300 becomes 12 and GCC warns about the change with -Woverflow.

  5. What does this program print? #include <stdio.h> union Word { unsigned int value; unsigned char bytes[4]; }; int main(void) { union Word word; word.value = 0x01020304u; printf("%08X\n", word.value); printf("%u %u %u %u\n", word.bytes[0], word.bytes[1], word.bytes[2], word.bytes[3]); return 0; }

    Answer

    01020304 / 4 3 2 1

    The machine is little-endian, so the least significant byte 0x04 sits at the lowest address.

  6. What does this program print? #include <stdio.h> union Word { unsigned int value; unsigned char bytes[4]; }; int main(void) { printf("%zu\n", sizeof(union Word)); return 0; }

    Answer

    4

    The unsigned int member is four bytes and the byte array is also four, so the union takes four bytes.

  7. What does this program print? #include <stdio.h> union Packed { struct { unsigned int low : 3; unsigned int high : 5; } parts; unsigned int value; }; int main(void) { union Packed packed; packed.value = 0; packed.parts.low = 5; packed.parts.high = 3; printf("%u\n", packed.value); return 0; }

    Answer

    29

    Five in the low three bits plus three shifted left by three gives 5 + 24, which is 29.

  8. This program is built with gcc -std=c17 -Wall -Wextra toowide.c -o toowide. What does it print? #include <stdio.h> struct Flags { unsigned int value : 4; }; int main(void) { struct Flags flags; flags.value = 300; printf("%u\n", flags.value); return 0; }

    Answer

    12

    300 is 0x12C, and the low four bits are 0xC, which is 12.

  9. Which warning does GCC give for a value that does not fit a bit-field?

    Answer

    -Woverflow

    The conversion changes the value, so GCC reports it as an overflow of the narrow bit-field.

  10. Why do file formats avoid writing bit-fields directly?

    Answer

    Their bit order and padding vary between compilers

    A format needs a fixed byte layout, so it defines shifts and masks rather than trusting an implementation-defined bit-field layout.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev