Unions in C Programming
Unions in C are user-defined data types that allow you to store different data types in the same memory location. Unlike structures that allocate separate memory for each member, all union members share the same memory space, making unions memory-efficient for certain use cases.
Key Takeaways
- Unions allow multiple data types to share the same memory location
- Only one member of a union can be accessed at a time safely
- Union size equals the size of its largest member
- Unions are useful for memory optimization and creating variant data types
- Type punning with unions requires careful consideration of endianness
What are Unions?
A union is a user-defined data type in C that allows you to store different data types in the same memory location. While structures allocate separate memory for each member, all members of a union share the same memory space. This makes unions useful when you need to store different types of data but only one type at a time.
Structure Memory Layout
1struct Example {2 int x; // 4 bytes3 float y; // 4 bytes4 char z; // 1 byte5};6// Total: 9+ bytes (with padding)
Each member has its own memory space
Union Memory Layout
1union Example {2 int x; // 4 bytes3 float y; // 4 bytes4 char z; // 1 byte5};6// Total: 4 bytes (size of largest member)
All members share the same memory space
Union Declaration and Syntax
The syntax for declaring a union is similar to structures, but uses the union
keyword instead of struct
.
Basic Syntax
1union union_name {2 data_type member1;3 data_type member2;4 // ... more members5};
Example: Basic Union Declaration
1#include <stdio.h>23union Data {4 int integer;5 float floating_point;6 char character;7};89int main() {10 union Data data;1112 // Store and access integer13 data.integer = 42;14 printf("Integer: %d\n", data.integer);1516 // Store and access float (overwrites integer)17 data.floating_point = 3.14f;18 printf("Float: %.2f\n", data.floating_point);1920 // Store and access character (overwrites float)21 data.character = 'A';22 printf("Character: %c\n", data.character);2324 // Accessing integer now gives unpredictable results25 printf("Integer after storing char: %d\n", data.integer);2627 return 0;28}2930// Output:31// Integer: 4232// Float: 3.1433// Character: A34// Integer after storing char: 65 (ASCII value of 'A')
Important: Only One Member at a Time
When you store a value in one union member, the values of other members become undefined. Only access the member that was most recently assigned a value.
Union vs Structure Comparison
Aspect | Structure | Union |
---|---|---|
Memory Usage | Sum of all member sizes (plus padding) | Size of largest member |
Member Access | All members can be accessed simultaneously | Only one member should be accessed at a time |
Use Case | When you need all data simultaneously | When you need different data types alternatively |
Initialization | Can initialize all members | Can initialize only the first member |
Example: Memory Usage Comparison
1#include <stdio.h>23struct PersonStruct {4 char name[20]; // 20 bytes5 int age; // 4 bytes6 float salary; // 4 bytes7};89union PersonUnion {10 char name[20]; // 20 bytes11 int age; // 4 bytes12 float salary; // 4 bytes13};1415int main() {16 printf("Size of struct PersonStruct: %zu bytes\n", sizeof(struct PersonStruct));17 printf("Size of union PersonUnion: %zu bytes\n", sizeof(union PersonUnion));1819 return 0;20}2122// Output:23// Size of struct PersonStruct: 28 bytes (with padding)24// Size of union PersonUnion: 20 bytes
Union Initialization
Unions can be initialized in several ways, but unlike structures, you can only initialize one member at a time.
Initialization Methods
1#include <stdio.h>2#include <string.h>34union Example {5 int num;6 float decimal;7 char str[20];8};910int main() {11 // Method 1: Initialize first member only12 union Example u1 = {100};1314 // Method 2: Designated initializer (C99 and later)15 union Example u2 = {.decimal = 3.14f};16 union Example u3 = {.str = "Hello"};1718 // Method 3: Initialize after declaration19 union Example u4;20 u4.num = 42;2122 // Method 4: Initialize with string using strcpy23 union Example u5;24 strcpy(u5.str, "World");2526 printf("u1.num: %d\n", u1.num);27 printf("u2.decimal: %.2f\n", u2.decimal);28 printf("u3.str: %s\n", u3.str);29 printf("u4.num: %d\n", u4.num);30 printf("u5.str: %s\n", u5.str);3132 return 0;33}3435// Output:36// u1.num: 10037// u2.decimal: 3.1438// u3.str: Hello39// u4.num: 4240// u5.str: World
When initializing a union with {'value'}
, the value is assigned to the first member. Use designated initializers {.member = value}
to initialize specific members.
Practical Examples
Example 1: Number Format Converter
1#include <stdio.h>23union NumberConverter {4 int as_int;5 float as_float;6 char as_bytes[4];7};89void printBytes(unsigned char *bytes, int size) {10 for (int i = 0; i < size; i++) {11 printf("%02X ", bytes[i]);12 }13 printf("\n");14}1516int main() {17 union NumberConverter converter;1819 // Convert integer to bytes20 converter.as_int = 0x12345678;21 printf("Integer: 0x%X\n", converter.as_int);22 printf("As bytes: ");23 printBytes((unsigned char*)converter.as_bytes, 4);2425 // Convert float to bytes26 converter.as_float = 3.14159f;27 printf("\nFloat: %.5f\n", converter.as_float);28 printf("As bytes: ");29 printBytes((unsigned char*)converter.as_bytes, 4);3031 return 0;32}3334// Output (on little-endian system):35// Integer: 0x1234567836// As bytes: 78 56 34 1237//38// Float: 3.1415939// As bytes: D0 0F 49 40
Example 2: Tagged Union (Variant Type)
1#include <stdio.h>2#include <string.h>34enum DataType {5 TYPE_INT,6 TYPE_FLOAT,7 TYPE_STRING8};910struct Variant {11 enum DataType type;12 union {13 int int_value;14 float float_value;15 char string_value[50];16 } data;17};1819void printVariant(struct Variant *var) {20 switch (var->type) {21 case TYPE_INT:22 printf("Integer: %d\n", var->data.int_value);23 break;24 case TYPE_FLOAT:25 printf("Float: %.2f\n", var->data.float_value);26 break;27 case TYPE_STRING:28 printf("String: %s\n", var->data.string_value);29 break;30 }31}3233int main() {34 struct Variant variants[3];3536 // Create integer variant37 variants[0].type = TYPE_INT;38 variants[0].data.int_value = 42;3940 // Create float variant41 variants[1].type = TYPE_FLOAT;42 variants[1].data.float_value = 3.14f;4344 // Create string variant45 variants[2].type = TYPE_STRING;46 strcpy(variants[2].data.string_value, "Hello, World!");4748 // Print all variants49 for (int i = 0; i < 3; i++) {50 printf("Variant %d - ", i + 1);51 printVariant(&variants[i]);52 }5354 return 0;55}5657// Output:58// Variant 1 - Integer: 4259// Variant 2 - Float: 3.1460// Variant 3 - String: Hello, World!
Best Practice: Tagged Unions
When using unions to store different data types, always include a tag (enum) to track which member is currently valid. This prevents accessing invalid data and makes your code more maintainable.
Example 3: Memory-Efficient Configuration Storage
1#include <stdio.h>2#include <string.h>34enum ConfigType {5 CONFIG_BOOLEAN,6 CONFIG_INTEGER,7 CONFIG_STRING8};910struct ConfigItem {11 char name[32];12 enum ConfigType type;13 union {14 int boolean;15 int integer;16 char string[64];17 } value;18};1920void setConfigBool(struct ConfigItem *item, const char *name, int value) {21 strcpy(item->name, name);22 item->type = CONFIG_BOOLEAN;23 item->value.boolean = value;24}2526void setConfigInt(struct ConfigItem *item, const char *name, int value) {27 strcpy(item->name, name);28 item->type = CONFIG_INTEGER;29 item->value.integer = value;30}3132void setConfigString(struct ConfigItem *item, const char *name, const char *value) {33 strcpy(item->name, name);34 item->type = CONFIG_STRING;35 strcpy(item->value.string, value);36}3738void printConfig(struct ConfigItem *item) {39 printf("%-20s: ", item->name);40 switch (item->type) {41 case CONFIG_BOOLEAN:42 printf("%s\n", item->value.boolean ? "true" : "false");43 break;44 case CONFIG_INTEGER:45 printf("%d\n", item->value.integer);46 break;47 case CONFIG_STRING:48 printf("%s\n", item->value.string);49 break;50 }51}5253int main() {54 struct ConfigItem config[4];5556 setConfigBool(&config[0], "debug_mode", 1);57 setConfigInt(&config[1], "max_connections", 100);58 setConfigString(&config[2], "server_name", "MyServer");59 setConfigInt(&config[3], "port", 8080);6061 printf("Configuration Settings:\n");62 printf("======================\n");63 for (int i = 0; i < 4; i++) {64 printConfig(&config[i]);65 }6667 printf("\nMemory usage per config item: %zu bytes\n", sizeof(struct ConfigItem));6869 return 0;70}7172// Output:73// Configuration Settings:74// ======================75// debug_mode : true76// max_connections : 10077// server_name : MyServer78// port : 808079//80// Memory usage per config item: 100 bytes
Unions with Pointers
You can have pointers to unions and unions containing pointers, which opens up more advanced usage patterns.
Example: Union with Pointers
1#include <stdio.h>2#include <stdlib.h>3#include <string.h>45union PointerUnion {6 int *int_ptr;7 float *float_ptr;8 char *string_ptr;9};1011int main() {12 union PointerUnion ptr_union;1314 // Allocate and use integer pointer15 ptr_union.int_ptr = malloc(sizeof(int));16 *(ptr_union.int_ptr) = 42;17 printf("Integer value: %d\n", *(ptr_union.int_ptr));18 free(ptr_union.int_ptr);1920 // Allocate and use float pointer21 ptr_union.float_ptr = malloc(sizeof(float));22 *(ptr_union.float_ptr) = 3.14f;23 printf("Float value: %.2f\n", *(ptr_union.float_ptr));24 free(ptr_union.float_ptr);2526 // Allocate and use string pointer27 ptr_union.string_ptr = malloc(20 * sizeof(char));28 strcpy(ptr_union.string_ptr, "Hello, Union!");29 printf("String value: %s\n", ptr_union.string_ptr);30 free(ptr_union.string_ptr);3132 return 0;33}3435// Output:36// Integer value: 4237// Float value: 3.1438// String value: Hello, Union!
Example: Array of Union Pointers
1#include <stdio.h>2#include <stdlib.h>34union Data {5 int integer;6 float decimal;7 char character;8};910int main() {11 union Data *data_array[3];1213 // Allocate memory for each union14 for (int i = 0; i < 3; i++) {15 data_array[i] = malloc(sizeof(union Data));16 }1718 // Initialize with different data types19 data_array[0]->integer = 100;20 data_array[1]->decimal = 2.718f;21 data_array[2]->character = 'Z';2223 // Access the data24 printf("data_array[0] integer: %d\n", data_array[0]->integer);25 printf("data_array[1] decimal: %.3f\n", data_array[1]->decimal);26 printf("data_array[2] character: %c\n", data_array[2]->character);2728 // Free allocated memory29 for (int i = 0; i < 3; i++) {30 free(data_array[i]);31 }3233 return 0;34}3536// Output:37// data_array[0] integer: 10038// data_array[1] decimal: 2.71839// data_array[2] character: Z
Anonymous Unions
C11 introduced anonymous unions, which allow you to access union members directly without specifying the union name. This is particularly useful when embedding unions within structures.
1#include <stdio.h>23struct Point {4 union {5 struct {6 float x, y;7 };8 float coords[2];9 };10};1112int main() {13 struct Point p;1415 // Access using named members16 p.x = 3.0f;17 p.y = 4.0f;1819 printf("Point coordinates: (%.1f, %.1f)\n", p.x, p.y);2021 // Access using array notation22 printf("Using array notation: (%.1f, %.1f)\n", p.coords[0], p.coords[1]);2324 // Modify using array notation25 p.coords[0] = 5.0f;26 p.coords[1] = 6.0f;2728 printf("Modified point: (%.1f, %.1f)\n", p.x, p.y);2930 return 0;31}3233// Output:34// Point coordinates: (3.0, 4.0)35// Using array notation: (3.0, 4.0)36// Modified point: (5.0, 6.0)
Common Pitfalls and Best Practices
Accessing Wrong Union Member
Accessing a union member that wasn't the last one to be assigned leads to undefined behavior:
1union Example {2 int num;3 float decimal;4};56union Example u;7u.decimal = 3.14f;8// WRONG: num wasn't the last member assigned9printf("%d\n", u.num); // Undefined behavior1011// CORRECT: Access the member that was last assigned12printf("%.2f\n", u.decimal); // Safe
Endianness Considerations
When using unions for type punning, be aware that results may vary between different architectures:
1union TypePun {2 int full;3 char bytes[4];4};56union TypePun tp;7tp.full = 0x12345678;89// On little-endian: bytes[0] = 0x78, bytes[1] = 0x56, etc.10// On big-endian: bytes[0] = 0x12, bytes[1] = 0x34, etc.11// Always test on target architecture!
Best Practices
- Always use tagged unions to track which member is currently valid
- Don't access union members that weren't the last to be assigned
- Be cautious with type punning and consider endianness
- Use unions when you need memory efficiency and only one data type at a time
- Document which union member should be accessed in different contexts
- Consider using structures if you need all data simultaneously