Progress: 6 of 16 topics37%

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

c
1struct Example {
2 int x; // 4 bytes
3 float y; // 4 bytes
4 char z; // 1 byte
5};
6// Total: 9+ bytes (with padding)

Each member has its own memory space

Union Memory Layout

c
1union Example {
2 int x; // 4 bytes
3 float y; // 4 bytes
4 char z; // 1 byte
5};
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

c
1union union_name {
2 data_type member1;
3 data_type member2;
4 // ... more members
5};

Example: Basic Union Declaration

c
1#include <stdio.h>
2
3union Data {
4 int integer;
5 float floating_point;
6 char character;
7};
8
9int main() {
10 union Data data;
11
12 // Store and access integer
13 data.integer = 42;
14 printf("Integer: %d\n", data.integer);
15
16 // Store and access float (overwrites integer)
17 data.floating_point = 3.14f;
18 printf("Float: %.2f\n", data.floating_point);
19
20 // Store and access character (overwrites float)
21 data.character = 'A';
22 printf("Character: %c\n", data.character);
23
24 // Accessing integer now gives unpredictable results
25 printf("Integer after storing char: %d\n", data.integer);
26
27 return 0;
28}
29
30// Output:
31// Integer: 42
32// Float: 3.14
33// Character: A
34// 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

AspectStructureUnion
Memory UsageSum of all member sizes (plus padding)Size of largest member
Member AccessAll members can be accessed simultaneouslyOnly one member should be accessed at a time
Use CaseWhen you need all data simultaneouslyWhen you need different data types alternatively
InitializationCan initialize all membersCan initialize only the first member

Example: Memory Usage Comparison

c
1#include <stdio.h>
2
3struct PersonStruct {
4 char name[20]; // 20 bytes
5 int age; // 4 bytes
6 float salary; // 4 bytes
7};
8
9union PersonUnion {
10 char name[20]; // 20 bytes
11 int age; // 4 bytes
12 float salary; // 4 bytes
13};
14
15int 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));
18
19 return 0;
20}
21
22// 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

c
1#include <stdio.h>
2#include <string.h>
3
4union Example {
5 int num;
6 float decimal;
7 char str[20];
8};
9
10int main() {
11 // Method 1: Initialize first member only
12 union Example u1 = {100};
13
14 // Method 2: Designated initializer (C99 and later)
15 union Example u2 = {.decimal = 3.14f};
16 union Example u3 = {.str = "Hello"};
17
18 // Method 3: Initialize after declaration
19 union Example u4;
20 u4.num = 42;
21
22 // Method 4: Initialize with string using strcpy
23 union Example u5;
24 strcpy(u5.str, "World");
25
26 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);
31
32 return 0;
33}
34
35// Output:
36// u1.num: 100
37// u2.decimal: 3.14
38// u3.str: Hello
39// u4.num: 42
40// 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

c
1#include <stdio.h>
2
3union NumberConverter {
4 int as_int;
5 float as_float;
6 char as_bytes[4];
7};
8
9void printBytes(unsigned char *bytes, int size) {
10 for (int i = 0; i < size; i++) {
11 printf("%02X ", bytes[i]);
12 }
13 printf("\n");
14}
15
16int main() {
17 union NumberConverter converter;
18
19 // Convert integer to bytes
20 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);
24
25 // Convert float to bytes
26 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);
30
31 return 0;
32}
33
34// Output (on little-endian system):
35// Integer: 0x12345678
36// As bytes: 78 56 34 12
37//
38// Float: 3.14159
39// As bytes: D0 0F 49 40

Example 2: Tagged Union (Variant Type)

c
1#include <stdio.h>
2#include <string.h>
3
4enum DataType {
5 TYPE_INT,
6 TYPE_FLOAT,
7 TYPE_STRING
8};
9
10struct Variant {
11 enum DataType type;
12 union {
13 int int_value;
14 float float_value;
15 char string_value[50];
16 } data;
17};
18
19void 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}
32
33int main() {
34 struct Variant variants[3];
35
36 // Create integer variant
37 variants[0].type = TYPE_INT;
38 variants[0].data.int_value = 42;
39
40 // Create float variant
41 variants[1].type = TYPE_FLOAT;
42 variants[1].data.float_value = 3.14f;
43
44 // Create string variant
45 variants[2].type = TYPE_STRING;
46 strcpy(variants[2].data.string_value, "Hello, World!");
47
48 // Print all variants
49 for (int i = 0; i < 3; i++) {
50 printf("Variant %d - ", i + 1);
51 printVariant(&variants[i]);
52 }
53
54 return 0;
55}
56
57// Output:
58// Variant 1 - Integer: 42
59// Variant 2 - Float: 3.14
60// 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

c
1#include <stdio.h>
2#include <string.h>
3
4enum ConfigType {
5 CONFIG_BOOLEAN,
6 CONFIG_INTEGER,
7 CONFIG_STRING
8};
9
10struct ConfigItem {
11 char name[32];
12 enum ConfigType type;
13 union {
14 int boolean;
15 int integer;
16 char string[64];
17 } value;
18};
19
20void 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}
25
26void 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}
31
32void 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}
37
38void 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}
52
53int main() {
54 struct ConfigItem config[4];
55
56 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);
60
61 printf("Configuration Settings:\n");
62 printf("======================\n");
63 for (int i = 0; i < 4; i++) {
64 printConfig(&config[i]);
65 }
66
67 printf("\nMemory usage per config item: %zu bytes\n", sizeof(struct ConfigItem));
68
69 return 0;
70}
71
72// Output:
73// Configuration Settings:
74// ======================
75// debug_mode : true
76// max_connections : 100
77// server_name : MyServer
78// port : 8080
79//
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

c
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5union PointerUnion {
6 int *int_ptr;
7 float *float_ptr;
8 char *string_ptr;
9};
10
11int main() {
12 union PointerUnion ptr_union;
13
14 // Allocate and use integer pointer
15 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);
19
20 // Allocate and use float pointer
21 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);
25
26 // Allocate and use string pointer
27 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);
31
32 return 0;
33}
34
35// Output:
36// Integer value: 42
37// Float value: 3.14
38// String value: Hello, Union!

Example: Array of Union Pointers

c
1#include <stdio.h>
2#include <stdlib.h>
3
4union Data {
5 int integer;
6 float decimal;
7 char character;
8};
9
10int main() {
11 union Data *data_array[3];
12
13 // Allocate memory for each union
14 for (int i = 0; i < 3; i++) {
15 data_array[i] = malloc(sizeof(union Data));
16 }
17
18 // Initialize with different data types
19 data_array[0]->integer = 100;
20 data_array[1]->decimal = 2.718f;
21 data_array[2]->character = 'Z';
22
23 // Access the data
24 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);
27
28 // Free allocated memory
29 for (int i = 0; i < 3; i++) {
30 free(data_array[i]);
31 }
32
33 return 0;
34}
35
36// Output:
37// data_array[0] integer: 100
38// data_array[1] decimal: 2.718
39// 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.

c
1#include <stdio.h>
2
3struct Point {
4 union {
5 struct {
6 float x, y;
7 };
8 float coords[2];
9 };
10};
11
12int main() {
13 struct Point p;
14
15 // Access using named members
16 p.x = 3.0f;
17 p.y = 4.0f;
18
19 printf("Point coordinates: (%.1f, %.1f)\n", p.x, p.y);
20
21 // Access using array notation
22 printf("Using array notation: (%.1f, %.1f)\n", p.coords[0], p.coords[1]);
23
24 // Modify using array notation
25 p.coords[0] = 5.0f;
26 p.coords[1] = 6.0f;
27
28 printf("Modified point: (%.1f, %.1f)\n", p.x, p.y);
29
30 return 0;
31}
32
33// 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:

c
1union Example {
2 int num;
3 float decimal;
4};
5
6union Example u;
7u.decimal = 3.14f;
8// WRONG: num wasn't the last member assigned
9printf("%d\n", u.num); // Undefined behavior
10
11// CORRECT: Access the member that was last assigned
12printf("%.2f\n", u.decimal); // Safe

Endianness Considerations

When using unions for type punning, be aware that results may vary between different architectures:

c
1union TypePun {
2 int full;
3 char bytes[4];
4};
5
6union TypePun tp;
7tp.full = 0x12345678;
8
9// 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