1
🔰 Your First Union — One Slot, Three Types
Define a union, store one value at a time, see shared memory in action
Basics
A union looks exactly like a struct — but all members share the same block of memory. The union is only large enough to hold its biggest member. You can store an
int or a float or a char — but only one at a time. Writing to one member overwrites what the others see. Here we store each type in turn and read it back immediately.
#include <stdio.h> union Data { int i; float f; char c; }; int main() { union Data d; /* Store int — read int immediately */ d.i = 42; printf("Stored int : d.i = %d\n", d.i); /* Store float — overwrites the int bytes */ d.f = 3.14f; printf("Stored float : d.f = %.2f\n", d.f); /* Store char — overwrites the float bytes */ d.c = 'A'; printf("Stored char : d.c = %c\n", d.c); /* Size — only as big as the largest member (float = 4 bytes) */ printf("\nsizeof(union Data) = %zu bytes\n", sizeof(union Data)); printf("sizeof(int) = %zu bytes\n", sizeof(int)); printf("sizeof(float) = %zu bytes\n", sizeof(float)); printf("sizeof(char) = %zu bytes\n", sizeof(char)); return 0; }
Stored int : d.i = 42 Stored float : d.f = 3.14 Stored char : d.c = A sizeof(union Data) = 4 bytes sizeof(int) = 4 bytes sizeof(float) = 4 bytes sizeof(char) = 1 byte
Union size rule:
sizeof(union) = size of its largest member. Here both int and float are 4 bytes, so the union is 4 bytes total — regardless of how many members it has.example 2
2
🧠 Shared Memory — Reading Stale Values
Write one member, read another — see what shared memory really means
Shared Memory
The most important thing to understand about unions: writing one member and reading a different member gives you the raw bytes of the first value interpreted as the second type. This is called reading a stale member. The program intentionally shows this — write an
int, then read .f and .c to see the reinterpreted bytes. Understanding this prevents hard-to-find bugs.
#include <stdio.h> typedef union { int i; float f; char c; } Data; int main() { Data d; /* Write only .i */ d.i = 1065353216; /* 0x3F800000 in hex */ printf("Wrote d.i = %d\n\n", d.i); /* Read ALL members — they all see the same 4 bytes */ printf("Read d.i = %d\n", d.i); /* correct: 1065353216 */ printf("Read d.f = %f\n", d.f); /* 0x3F800000 = 1.0f exactly */ printf("Read d.c = %d (0x%02X)\n", d.c, (unsigned char)d.c); /* .c reads only the first byte of the 4-byte int */ printf("\n--- All 4 bytes of the union ---\n"); unsigned char *p = (unsigned char*)&d; for (int k = 0; k < 4; k++) printf(" byte[%d] = 0x%02X\n", k, p[k]); return 0; }
Wrote d.i = 1065353216 Read d.i = 1065353216 Read d.f = 1.000000 Read d.c = 0 (0x00) --- All 4 bytes of the union --- byte[0] = 0x00 byte[1] = 0x00 byte[2] = 0x80 byte[3] = 0x3F
memory layout — all members overlap at the same address
d.i (int)
0x00
0x00
0x80
0x3F
← 4 bytes
d.f (float)
0x00
0x00
0x80
0x3F
← same 4 bytes = 1.0f
d.c (char)
0x00
·
·
·
← only 1 byte visible
Only the last-written member is valid to read. Reading a different member is technically undefined behaviour in standard C (though widely used in practice). Always track which member you last wrote — that is the only safe one to read. The tagged union in Example 4 solves this properly.
example 3
3
⚖️ Union vs Struct — Size Comparison
Same fields, different containers — see the memory difference clearly
Size Compare
The clearest way to understand unions is to put the same fields in both a struct and a union and compare their sizes. The struct allocates memory for every field — they all live at different addresses. The union gives them all the same address — total size = largest field only. This is the fundamental trade-off: union saves memory but holds only one value at a time.
#include <stdio.h> /* Struct — every field gets its OWN memory */ typedef struct { char c; /* 1 byte (+ 3 padding) */ int i; /* 4 bytes */ float f; /* 4 bytes */ double d; /* 8 bytes */ } MyStruct; /* total ≈ 24 bytes (with alignment) */ /* Union — ALL fields share the SAME memory */ typedef union { char c; /* 1 byte */ int i; /* 4 bytes */ float f; /* 4 bytes */ double d; /* 8 bytes ← biggest */ } MyUnion; /* total = 8 bytes (= sizeof double) */ int main() { MyStruct s; MyUnion u; printf("--- sizeof ---\n"); printf("MyStruct : %zu bytes\n", sizeof(MyStruct)); printf("MyUnion : %zu bytes\n", sizeof(MyUnion)); printf("\n--- struct member addresses ---\n"); printf("&s.c = %p\n", (void*)&s.c); printf("&s.i = %p\n", (void*)&s.i); printf("&s.f = %p\n", (void*)&s.f); printf("&s.d = %p\n", (void*)&s.d); printf("\n--- union member addresses ---\n"); printf("&u.c = %p\n", (void*)&u.c); printf("&u.i = %p\n", (void*)&u.i); printf("&u.f = %p\n", (void*)&u.f); printf("&u.d = %p ← all same!\n", (void*)&u.d); return 0; }
--- sizeof --- MyStruct : 24 bytes MyUnion : 8 bytes --- struct member addresses --- &s.c = 0x7ffd1000 &s.i = 0x7ffd1004 &s.f = 0x7ffd1008 &s.d = 0x7ffd1010 --- union member addresses --- &u.c = 0x7ffd2000 &u.i = 0x7ffd2000 &u.f = 0x7ffd2000 &u.d = 0x7ffd2000 ← all same!
Every union member starts at address 0. They all share the same starting address — the union's own address. The struct members are spread across 24 bytes. The union uses only 8 — a 3× memory saving here.
example 4
4
🏷️ Tagged Union — Safe One-of-Many Value
A struct wraps the union with a tag field — always know which member is active
Tagged Union
The tagged union (also called a discriminated union) is the correct, safe way to use a union. A surrounding
struct adds a tag field — an enum or int that records which member was last written. Before reading, check the tag. This pattern is used in compilers, interpreters, JSON parsers, and virtually every real-world union use case.
#include <stdio.h> #include <string.h> /* Tag: tells us which member is currently valid */ typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STR } Tag; typedef struct { Tag tag; /* which field is active? */ union { int i; float f; char str[20]; } val; /* the shared memory slot */ } Value; /* Safe print — checks tag before reading */ void printValue(const Value *v) { switch (v->tag) { case TYPE_INT: printf("int : %d\n", v->val.i); break; case TYPE_FLOAT: printf("float : %.2f\n", v->val.f); break; case TYPE_STR: printf("str : \"%s\"\n",v->val.str); break; } } int main() { Value a, b, c; a.tag = TYPE_INT; a.val.i = 42; b.tag = TYPE_FLOAT; b.val.f = 3.14f; c.tag = TYPE_STR; strcpy(c.val.str, "Ananta"); printf("--- Tagged values ---\n"); printValue(&a); printValue(&b); printValue(&c); /* Change type safely — update BOTH tag and value */ printf("\nChanging a from int to float...\n"); a.tag = TYPE_FLOAT; a.val.f = 9.99f; printValue(&a); return 0; }
--- Tagged values --- int : 42 float : 3.14 str : "Ananta" Changing a from int to float... float : 9.99
Always update the tag alongside the value. The pattern is always: set
tag = TYPE_X, then set val.x = .... The switch(v->tag) in printValue guarantees you only ever read the correct member. This is how Rust's enums and C++'s std::variant work internally.example 5
5
🔬 Byte Inspector — See Inside an Integer
Store an int, read it byte-by-byte through an unsigned char array member
Byte Inspect
A classic union trick: one member is an
int, the other is an array of 4 unsigned char. Since they share memory, writing the int and reading the char array gives you each individual byte of the integer. This reveals the endianness of the machine — whether the least-significant byte is stored first (little-endian) or last (big-endian).
#include <stdio.h> typedef union { unsigned int value; /* full 32-bit integer */ unsigned char bytes[4]; /* same 4 bytes, one at a time */ } IntBytes; void inspect(unsigned int n) { IntBytes ib; ib.value = n; printf("Value : %u (0x%08X)\n", ib.value, ib.value); printf("Bytes : "); for (int i = 0; i < 4; i++) printf("[%d]=0x%02X ", i, ib.bytes[i]); printf("\n"); /* On little-endian: bytes[0] holds the LEAST significant byte */ printf("Endianness : %s-endian\n\n", ib.bytes[0] == (n & 0xFF) ? "little" : "big"); } int main() { inspect(0x01020304); /* 4 distinct bytes */ inspect(255); /* 0x000000FF */ inspect(65536); /* 0x00010000 */ return 0; }
Value : 16909060 (0x01020304) Bytes : [0]=0x04 [1]=0x03 [2]=0x02 [3]=0x01 Endianness : little-endian Value : 255 (0x000000FF) Bytes : [0]=0xFF [1]=0x00 [2]=0x00 [3]=0x00 Endianness : little-endian Value : 65536 (0x00010000) Bytes : [0]=0x00 [1]=0x00 [2]=0x01 [3]=0x00 Endianness : little-endian
Little-endian (x86, ARM, most modern CPUs): byte[0] holds the least significant byte. So for
0x01020304, byte[0]=0x04, byte[3]=0x01. Big-endian (network order, older MIPS/SPARC): it's reversed — byte[0]=0x01. This matters whenever you send binary data over a network.example 6
6
🔢 Float Bit Viewer — Sign, Exponent, Mantissa
Overlay a float with an unsigned int to read its raw IEEE 754 bits
Type Punning
An IEEE 754
float is 32 bits arranged as: 1 sign bit + 8 exponent bits + 23 mantissa bits. By storing a float in a union alongside an unsigned int, you can read the raw bit pattern and extract each field using bitwise operators — no casting, no undefined behaviour. This is the standard technique for type punning in C.
#include <stdio.h> typedef union { float f; unsigned int bits; /* same 4 bytes as the float */ } FloatBits; void showBits(float x) { FloatBits fb; fb.f = x; /* write float */ unsigned int b = fb.bits; /* read as int */ int sign = (b >> 31) & 1; /* bit 31 */ int exponent = (b >> 23) & 0xFF; /* bits 30..23 */ int mantissa = b & 0x7FFFFF; /* bits 22..0 */ printf("f = %g\n", x); printf(" binary : "); for (int i = 31; i >= 0; i--) { printf("%d", (b >> i) & 1); if (i == 31 || i == 23) printf(" "); } printf("\n"); printf(" sign : %d\n", sign); printf(" exponent : %d (bias=%d)\n", exponent, exponent-127); printf(" mantissa : 0x%06X\n\n", mantissa); } int main() { showBits(1.0f); showBits(-2.5f); showBits(0.0f); return 0; }
f = 1 binary : 0 01111111 00000000000000000000000 sign : 0 exponent : 127 (bias=0) mantissa : 0x000000 f = -2.5 binary : 1 10000000 01000000000000000000000 sign : 1 exponent : 128 (bias=1) mantissa : 0x200000 f = 0 binary : 0 00000000 00000000000000000000000 sign : 0 exponent : 0 (bias=-127) mantissa : 0x000000
Type punning via union is legal in C (C99 and later). The same trick via pointer casts (
*(int*)&f) is undefined behaviour in C and C++. Always use the union method when you need to inspect raw bytes of a floating-point number.example 7
7
📦 Union Inside a Struct — Product Variants
Different product types need different fields — one struct handles all using an inner union
Union in Struct
A
Product struct uses an embedded union to hold type-specific data. A Book needs an ISBN and page count. A Drink needs volume in ml. A Cloth needs a size string. Without a union you'd waste memory keeping all fields for every product. With a union, each product object is exactly as big as the largest variant — and only that variant's memory is used.
#include <stdio.h> #include <string.h> typedef enum { BOOK, DRINK, CLOTH } ProductType; typedef struct { char name[25]; float price; ProductType type; union { /* only one is ever used */ struct { char isbn[14]; int pages; } book; struct { int ml; } drink; struct { char size[5]; /* S, M, L, XL */ } cloth; } info; } Product; void printProduct(const Product *p) { printf("%-18s Rs%7.2f | ", p->name, p->price); switch (p->type) { case BOOK: printf("ISBN:%s pages:%d\n", p->info.book.isbn, p->info.book.pages); break; case DRINK: printf("volume:%dml\n", p->info.drink.ml); break; case CLOTH: printf("size:%s\n", p->info.cloth.size); break; } } int main() { Product items[3]; strcpy(items[0].name, "Let Us C"); items[0].price = 350.0f; items[0].type = BOOK; strcpy(items[0].info.book.isbn, "978-8131722329"); items[0].info.book.pages = 680; strcpy(items[1].name, "Mango Juice"); items[1].price = 45.0f; items[1].type = DRINK; items[1].info.drink.ml = 250; strcpy(items[2].name, "Cotton T-Shirt"); items[2].price = 499.0f; items[2].type = CLOTH; strcpy(items[2].info.cloth.size, "XL"); printf("%-18s %10s | Details\n", "Product", "Price"); printf("%s\n", "-------------------------------------------------------"); for (int i = 0; i < 3; i++) printProduct(&items[i]); return 0; }
Product Price | Details ------------------------------------------------------- Let Us C Rs350.00 | ISBN:978-8131722329 pages:680 Mango Juice Rs45.00 | volume:250ml Cotton T-Shirt Rs499.00 | size:XL
This pattern — struct wrapping enum + union — appears everywhere in systems code. C compilers use it for AST nodes (a node is an if-statement or a function-call or a literal). JSON parsers use it for values. GUI frameworks use it for events.
example 8
8
📡 Network Packet — Parse Header Bytes
Store a 4-byte packet header as int or byte array — read fields either way
Embedded / Net
In networking and embedded systems, you often receive raw bytes and need to parse them as structured fields — or build structured fields and send them as raw bytes. A union with a
uint32_t (whole word) and a struct of bitfields gives you both views simultaneously. Write the fields through the struct, read the wire bytes through the integer — no manual bit-shifting needed.
#include <stdio.h> #include <stdint.h> /* 4-byte packet header laid out as bitfields */ typedef union { uint32_t raw; /* the whole 32 bits as one word */ unsigned char bytes[4]; /* individual wire bytes */ struct { uint32_t version : 4; /* bits 0-3: IP version */ uint32_t ihl : 4; /* bits 4-7: header length */ uint32_t dscp : 6; /* bits 8-13: diff services */ uint32_t ecn : 2; /* bits 14-15: congestion */ uint32_t length : 16; /* bits 16-31: total length */ } fields; } IPv4Header; int main() { IPv4Header hdr; /* Build header by setting fields */ hdr.fields.version = 4; /* IPv4 */ hdr.fields.ihl = 5; /* 5 * 4 = 20 byte header */ hdr.fields.dscp = 0; hdr.fields.ecn = 0; hdr.fields.length = 60; /* total packet length */ printf("--- Structured view ---\n"); printf("Version : %u\n", hdr.fields.version); printf("IHL : %u (= %u bytes)\n", hdr.fields.ihl, hdr.fields.ihl*4); printf("Length : %u\n", hdr.fields.length); printf("\n--- Raw wire bytes ---\n"); printf("raw uint32 : 0x%08X\n", hdr.raw); for (int i = 0; i < 4; i++) printf(" byte[%d] : 0x%02X\n", i, hdr.bytes[i]); /* Receive a raw packet and parse it */ printf("\n--- Parsing received bytes ---\n"); IPv4Header recv; recv.bytes[0] = 0x45; /* version=4, ihl=5 */ recv.bytes[1] = 0x00; recv.bytes[2] = 0x00; recv.bytes[3] = 0x3C; /* 60 in big-endian high byte */ printf("Version parsed: %u\n", recv.fields.version); printf("IHL parsed: %u\n", recv.fields.ihl); return 0; }
--- Structured view --- Version : 4 IHL : 5 (= 20 bytes) Length : 60 --- Raw wire bytes --- raw uint32 : 0x003C0045 byte[0] : 0x45 byte[1] : 0x00 byte[2] : 0x3C byte[3] : 0x00 --- Parsing received bytes --- Version parsed: 4 IHL parsed: 5
Bitfields + union is the standard pattern in every network driver, USB stack, and hardware register file in C. You write readable field names in code; the union gives you the raw bytes to send over the wire — no manual shifting or masking required.
example 9
9
🗂️ Array of Tagged Unions — Simple Symbol Table
Store ints, floats, and strings together in one array using tagged unions
Array of Unions
A common real-world need: store a mixed list of values — some integers, some floats, some strings — in a single array. An array of tagged unions makes this clean and type-safe. Each element knows its own type via the tag. This is exactly how a scripting language (Python, Lua, JavaScript) stores its variables internally in a C runtime.
#include <stdio.h> #include <string.h> typedef enum { INT, FLOAT, STRING } VarType; typedef struct { char varName[10]; /* variable name */ VarType type; union { int i; float f; char s[20]; } val; } Symbol; void printSymbol(const Symbol *sym) { printf(" %-8s = ", sym->varName); switch (sym->type) { case INT: printf("%d (int)\n", sym->val.i); break; case FLOAT: printf("%.2f (float)\n", sym->val.f); break; case STRING: printf("\"%s\" (string)\n",sym->val.s); break; } } /* Look up a variable by name */ Symbol* lookup(Symbol *table, int n, const char *name) { for (int i = 0; i < n; i++) if (strcmp(table[i].varName, name) == 0) return &table[i]; return NULL; } int main() { Symbol symtab[5]; strcpy(symtab[0].varName, "age"); symtab[0].type = INT; symtab[0].val.i = 21; strcpy(symtab[1].varName, "score"); symtab[1].type = FLOAT; symtab[1].val.f = 95.5f; strcpy(symtab[2].varName, "city"); symtab[2].type = STRING; strcpy(symtab[2].val.s, "Haridwar"); strcpy(symtab[3].varName, "year"); symtab[3].type = INT; symtab[3].val.i = 2024; strcpy(symtab[4].varName, "pi"); symtab[4].type = FLOAT; symtab[4].val.f = 3.14159f; printf("--- Symbol Table ---\n"); for (int i = 0; i < 5; i++) printSymbol(&symtab[i]); printf("\n--- Lookup ---\n"); Symbol *found = lookup(symtab, 5, "city"); if (found) printSymbol(found); found = lookup(symtab, 5, "missing"); printf("\"missing\": %s\n", found ? "found" : "not found"); return 0; }
--- Symbol Table --- age = 21 (int) score = 95.50 (float) city = "Haridwar" (string) year = 2024 (int) pi = 3.14 (float) --- Lookup --- city = "Haridwar" (string) "missing": not found
This is how Python and Lua store all variables. Their C-level
Value type is exactly this pattern — a tag plus a union. Every Python object has a type field and a data payload. The union makes the data payload exactly as large as needed — nothing wasted.example 10
10
🏗️ Variant Record — Shape Calculator
Circle, rectangle, and triangle as one type — compute area for any shape
Complete App
The complete pattern — everything from examples 4, 7, and 9 combined into a real mini application. A
Shape tagged union holds three shape variants (circle, rectangle, triangle). A single area() function checks the tag and computes the correct formula. An array of mixed shapes is processed in one loop — clean, memory-efficient, and easily extensible.
#include <stdio.h> #include <math.h> /* compile with -lm */ typedef enum { CIRCLE, RECT, TRIANGLE } ShapeType; typedef struct { ShapeType type; char label[15]; union { struct { double radius; } circle; struct { double width, height; } rect; struct { double base, height; } tri; } dim; } Shape; /* Factory helpers */ Shape makeCircle(char *lbl, double r) { Shape s; s.type = CIRCLE; snprintf(s.label, 15, "%s", lbl); s.dim.circle.radius = r; return s; } Shape makeRect(char *lbl, double w, double h) { Shape s; s.type = RECT; snprintf(s.label, 15, "%s", lbl); s.dim.rect.width = w; s.dim.rect.height = h; return s; } Shape makeTri(char *lbl, double b, double h) { Shape s; s.type = TRIANGLE; snprintf(s.label, 15, "%s", lbl); s.dim.tri.base = b; s.dim.tri.height = h; return s; } /* Single area function for ALL shape types */ double area(const Shape *s) { switch (s->type) { case CIRCLE: return 3.14159 * s->dim.circle.radius * s->dim.circle.radius; case RECT: return s->dim.rect.width * s->dim.rect.height; case TRIANGLE: return 0.5 * s->dim.tri.base * s->dim.tri.height; } return 0; } void describe(const Shape *s) { const char *names[] = {"Circle", "Rectangle", "Triangle"}; printf("%-14s %-10s area = %8.2f\n", s->label, names[s->type], area(s)); } int main() { Shape scene[] = { makeCircle("Sun", 7.0), makeRect ("Floor", 12.0, 8.0), makeTri ("Roof", 10.0, 6.0), makeCircle("Wheel", 3.5), makeRect ("Door", 2.0, 4.5), makeTri ("Flag", 5.0, 3.0), }; int n = sizeof(scene) / sizeof(scene[0]); printf("%-14s %-10s %s\n", "Label", "Type", "Area"); printf("------------------------------------------\n"); double total = 0; for (int i = 0; i < n; i++) { describe(&scene[i]); total += area(&scene[i]); } printf("------------------------------------------\n"); printf("%-24s total = %8.2f\n", "", total); return 0; }
Label Type Area
------------------------------------------
Sun Circle area = 153.94
Floor Rectangle area = 96.00
Roof Triangle area = 30.00
Wheel Circle area = 38.48
Door Rectangle area = 9.00
Flag Triangle area = 7.50
------------------------------------------
total = 334.92All 4 union patterns in one program:
makeCircle/Rect/Tri return a struct with embedded union · factory sets the tag + value together · area() dispatches on the tag · mixed-type array processed in a single clean loop. Add a new shape by adding one enum value, one struct in the union, one factory, and one case — nothing else changes.checklist
- Ex 1 —
sizeof(union)= size of its largest member. All members share the same memory block. - Ex 2 — Only the last-written member is valid to read. Reading any other gives raw byte reinterpretation.
- Ex 3 — Struct allocates memory for every field. Union allocates once for the largest — all start at the same address.
- Ex 4 — Tagged union = union + enum tag inside a struct. Always check the tag before reading. Always set both tag and value together.
- Ex 5 —
union { uint; uchar[4]; }lets you read individual bytes of any integer. Reveals endianness of the machine. - Ex 6 — Type punning via union is legal in C. Writing
floatand readinguintgives raw IEEE 754 bits safely. - Ex 7 — Union nested inside a struct with an enum type field = variant record. Used in compilers, JSON parsers, GUI events.
- Ex 8 — Bitfields + union = zero-shift structured access to raw bytes. Standard pattern in network drivers and hardware registers.
- Ex 9 — Array of tagged unions = mixed-type container. How Python / Lua store all runtime variables internally in C.
- Ex 10 — Factory + tag + single dispatch function + mixed array. Add a new variant by touching only 4 places — nothing else breaks.