Unions and Bit-Fields
A union lets several members share the same memory, holding only one of them at a time. Combined with a tag that records which member is in use, it models values that can be one of several kinds. Bit-fields, at the other end, pack several tiny values into a single integer.
Unions
A union is declared like a struct, but all members start at the same address, and the union is only as big as its largest member:
#include <stdio.h> union number { int i; double d; char bytes[8]; }; int main(void) { union number n; n.i = 42; printf("as int: %d\n", n.i); n.d = 2.5; // overwrites the same memory printf("as double: %.1f\n", n.d); printf("sizeof: %zu\n", sizeof n); return 0; }
as int: 42 as double: 2.5 sizeof: 8
Only the member you wrote last holds a meaningful value. Reading a different member reinterprets the same bytes, which is rarely what you want.
Tagged unions
The usual pattern pairs a union with an enum that records which member is valid — a "tagged union" or "variant":
#include <stdio.h> enum shape_kind { CIRCLE, RECTANGLE, TRIANGLE }; struct shape { enum shape_kind kind; union { struct { double radius; } circle; struct { double width, height; } rect; struct { double base, height; } tri; }; }; double area(const struct shape *s) { switch (s->kind) { case CIRCLE: return 3.14159265 * s->circle.radius * s->circle.radius; case RECTANGLE: return s->rect.width * s->rect.height; case TRIANGLE: return 0.5 * s->tri.base * s->tri.height; } return 0; } int main(void) { struct shape shapes[] = { { .kind = CIRCLE, .circle = { 1.0 } }, { .kind = RECTANGLE, .rect = { 3.0, 2.0 } }, { .kind = TRIANGLE, .tri = { 4.0, 5.0 } }, }; for (int i = 0; i < 3; i++) { printf("shape %d: area %.2f\n", i, area(&shapes[i])); } printf("each shape takes %zu bytes\n", sizeof shapes[0]); return 0; }
shape 0: area 3.14 shape 1: area 6.00 shape 2: area 10.00 each shape takes 24 bytes
The union here is anonymous (C11): its members are reached directly as s->circle instead of s->u.circle. Interpreters use this pattern to store values that can be numbers, strings or lists; event systems use it for messages of different types.
Bit-fields
A struct member can be given a width in bits. The compiler packs adjacent bit-fields into the same storage unit:
#include <stdio.h> struct pixel_flags { unsigned visible : 1; unsigned selected : 1; unsigned layer : 4; // 0..15 unsigned opacity : 7; // 0..127 }; int main(void) { struct pixel_flags f = { .visible = 1, .layer = 9, .opacity = 100 }; f.selected = 1; printf("visible %u, selected %u, layer %u, opacity %u\n", f.visible, f.selected, f.layer, f.opacity); printf("size: %zu bytes\n", sizeof f); f.layer = 15; printf("layer now %u\n", f.layer); return 0; }
visible 1, selected 1, layer 9, opacity 100 size: 4 bytes layer now 15
Assigning a value too big for the field keeps only the low bits, and compilers warn when they can see it happening.
Bit-fields are convenient, but the exact layout — bit order, padding, how fields straddle storage units — is up to the compiler. For data that must match an exact format (files, network packets, hardware registers), use plain integers with shifts and masks (lesson 14) instead.
Inspecting bytes
Looking at the bytes of a value through an unsigned char pointer is always allowed, and shows how the machine stores numbers. Most computers today are little-endian: the lowest byte comes first.
#include <stdio.h> #include <stdint.h> int main(void) { uint32_t value = 0x11223344; const unsigned char *bytes = (const unsigned char *) &value; for (size_t i = 0; i < sizeof value; i++) { printf("%02X ", bytes[i]); } printf("(%s-endian)\n", bytes[0] == 0x44 ? "little" : "big"); return 0; }
44 33 22 11 (little-endian)
Try it yourself
Model a configuration value that can be an integer, a decimal or a string, and print each kind correctly.
port = 8080 ratio = 0.75 hostname = "example.org"