Structure vs Union in C — Memory Layout, Size, and Use Cases
Structure and Union both are used to create a custom data type. As C language is a general-purpose, procedural computer programming language we often need functionality similar to OO language like C++. This is obtained through structure and interfaces.
Structure in C
Structure is used to create a custom data type that groups a number of primitive ones. It is more like several data type elements are packed and created into one data type.
Address of all members is different in memory.
Declaration
struct address
{
char name;
char street;
int city_id;
int state_id;
long description;
};
Size of structure
The size of the structure element is the combined size of all elements inside it. That will come to 18 bytes. You can test with the compiler.
#include<stdio.h>
struct address
{
char name;
char street;
int city_id;
int state_id;
long description;
};
int main(void) {
struct address addr;
printf("%d\n",sizeof(address));
return 0;
}
Success — 24 bytes output
18 byte + 6 unused bytes added by compiler for alignment in memory.
Union in C
Union is used to create a custom data type that is a union of all members declared into it. Basically a data type of minimum size that can hold a value of any kind of its member.
Address of all members is always the same in memory.
Declaration
union address
{
char name;
char street;
int city_id;
int state_id;
long description;
};
Size of Union
Size of union is the size of the largest member.
#include<stdio.h>
union address
{
char name;
char street;
int city_id;
int state_id;
long description;
};
int main(void) {
union address addr;
printf("%d\n",sizeof(addr));
return 0;
}
Success — 8 bytes output
8 byte = long size (largest member)
Key Differences — struct vs union
| Property | struct | union |
|---|---|---|
| Memory allocation | Separate block for each member | Single shared block (size of largest member) |
| sizeof | Sum of all members (+ padding) | Size of largest member (+ padding) |
| Member addresses | All different | All the same |
| Active members at once | All members valid simultaneously | Only one member valid at a time |
| Default initialisation | = {0} zeroes all members |
= {0} zeroes only the first member |
| Typical use | Records with multiple independent fields | Type-punning, variant data, memory-mapped registers |
| C++ inheritance | Allowed (can have base class) | Not allowed in standard C++ |
Memory Layout Diagram
Consider both types defined with the same three members:
#include <stdio.h>
struct Demo { char a; int b; double c; };
union Demo { char a; int b; double c; };
/* struct Demo layout (typical 64-bit, with padding):
┌────┬───┬───┬───┬────────────────────┐
│ a │pad│pad│pad│ b (4 bytes) │ ← a=1, pad=3, b=4
├────────────────────────────────────────┤
│ c (8 bytes) │ ← c=8
└────────────────────────────────────────┘
sizeof(struct Demo) = 16 bytes
union Demo layout:
┌────────────────────────────────────────┐
│ shared 8 bytes │
│ a (1) │
│ b (4) │
│ c (8) ← largest, sets union size │
└────────────────────────────────────────┘
sizeof(union Demo) = 8 bytes
*/
Writing to one union member and then reading another is type-punning — defined behaviour in C (C99 §6.5.2.3) but undefined behaviour in C++ unless you use memcpy or std::bit_cast (C++20).
Accessing Members
Dot notation (.) for stack variables, arrow notation (->) for pointers — identical syntax for both struct and union.
#include <stdio.h>
struct Point { int x; int y; };
union Data { int i; float f; char c; };
int main(void) {
/* struct — all members valid simultaneously */
struct Point p = { .x = 10, .y = 20 };
printf("x=%d, y=%d\n", p.x, p.y); /* x=10, y=20 */
/* union — only last-written member is valid */
union Data d;
d.i = 65;
printf("i=%d, c=%c\n", d.i, d.c); /* i=65, c=A (same bytes!) */
d.f = 3.14f;
printf("f=%.2f\n", d.f); /* f=3.14 */
/* d.i is now garbage — f overwrote those bytes */
/* pointer syntax */
struct Point *pp = &p;
printf("x=%d\n", pp->x); /* arrow for pointers */
return 0;
}
Embedded Systems Use Cases
Unions are especially common in embedded C for two patterns:
1. Memory-mapped register overlay
Access a hardware register both as a raw 32-bit word and as individual bitfields — without any casting:
/* UART status register — 32-bit peripheral register */
typedef union {
uint32_t word; /* full register access */
struct {
uint32_t rx_ready : 1;
uint32_t tx_empty : 1;
uint32_t overrun : 1;
uint32_t reserved : 29;
} bits;
} UartStatus;
volatile UartStatus *uart = (UartStatus *)0x40011000;
if (uart->bits.rx_ready) {
char ch = read_fifo();
}
uart->word = 0; /* clear entire register in one write */
2. Protocol message variant
A single buffer that can hold different message types — common in CAN bus and serial protocol parsers:
typedef enum { MSG_SENSOR, MSG_CMD, MSG_ACK } MsgType;
typedef struct {
MsgType type;
union {
struct { uint16_t sensor_id; int32_t value; } sensor;
struct { uint8_t cmd; uint8_t param; } cmd;
struct { uint8_t status; } ack;
} payload;
} Message;
void handle(Message *m) {
switch (m->type) {
case MSG_SENSOR: printf("sensor %d = %d\n",
m->payload.sensor.sensor_id,
m->payload.sensor.value); break;
case MSG_CMD: execute(m->payload.cmd.cmd,
m->payload.cmd.param); break;
case MSG_ACK: check_status(m->payload.ack.status); break;
}
}
Quick Reference
| Task | struct | union |
|---|---|---|
| Declare variable | struct Point p; | union Data d; |
| Initialise all | = {.x=1, .y=2} | = {.i = 42} (first member) |
| Member access | p.x | d.i |
| Pointer access | pp->x | dp->i |
| sizeof | Sum + padding | Largest + padding |
| typedef shorthand | typedef struct { … } Point; | typedef union { … } Data; |
| Nested usage | struct inside union | union inside struct |