Structures, Unions,
Enums & typedef
Group related data into custom types, share memory between members, replace magic numbers with named constants, and map hardware registers directly onto C structs.
Why Structures Exist
Suppose you need to store information about a student: roll number, name, age, and marks. Without structures, you’d declare four separate variables — and for 100 students, that’s hundreds of unrelated variables with no way to keep each student’s data bundled together.
What is a Structure?
A structure is a user-defined data type that groups variables of different data types under one name. Unlike an array, which holds many values of the same type, a structure can store an integer, a float, a character, an array, and even other structures — all together as one unit.
Student
Roll No, Name, CGPA — three different types describing one learner.
Car
Model, Year, Price — text, integer, and currency for one vehicle.
Book
Title, Author, Pages — bundled metadata for a single book record.
Sensor
Temperature, Pressure, Humidity — one reading, three float fields.
Declaring & Creating Structure Variables
struct StructureName
{
dataType member1;
dataType member2;
/* ... */
}; /* ← semicolon is MANDATORY here */struct Student
{
int roll;
char name[30];
int age;
float marks;
};struct Student { ... }; only defines a blueprint — the compiler now knows what a “Student” looks like, but no memory has been reserved yet. Memory is only created when you declare a variable of that type: struct Student s1;.struct Student s1; /* one variable — memory now allocated */
struct Student s1, s2, s3; /* three independent student records */Accessing Members & Initialization
Use the dot operator (.) to access a structure member through a variable.
#include <stdio.h>
#include <string.h>
struct Student
{
int roll;
char name[30];
int age;
float marks;
};
int main()
{
struct Student s1;
s1.roll = 101;
strcpy(s1.name, "Alice"); /* arrays can't use = directly */
s1.age = 20;
s1.marks = 89.5;
printf("Roll : %d\n", s1.roll);
printf("Name : %s\n", s1.name);
printf("Age : %d\n", s1.age);
printf("Marks : %.2f\n", s1.marks);
return 0;
}name is a char array, and arrays cannot be assigned directly in C — the same rule from Chapter 4. You must use strcpy() to copy string content into an array member.Two Initialization Styles
Memory Layout & Array vs Structure
Structure members are stored in declaration order, though the compiler may insert padding between them (covered in detail in 10). Each member occupies its own storage — structures don’t share memory the way unions do.
struct Data { int a; char b; }; you might expect sizeof to be 4+1=5 bytes. On many systems it’s actually 8 bytes due to padding. We’ll explain exactly why in 10–12.Array vs Structure
| Property | Array | Structure |
|---|---|---|
| Element types | Same type throughout (homogeneous) | Different types allowed (heterogeneous) |
| Access syntax | arr[i] (index) | s.member (name) |
| Example | int marks[5]; — five integers | struct Student — int + char[] + float together |
| Direct assignment | Not allowed (a = b) | Allowed (s2 = s1) — see 9 |
Arrays of Structures
To store 100 students, declaring s1 through s100 individually is impractical. Instead, use an array of structures — an array whose elements are themselves complete structures.
struct Student
{
int roll;
char name[30];
float marks;
};
struct Student students[3]; /* 3 complete student records */
/* Accessing a member of one element */
students[0].roll = 101;
strcpy(students[0].name, "Alice");
students[0].marks = 91.5;
/* Reading all records in a loop */
for(int i = 0; i < 3; i++)
{
scanf("%d", &students[i].roll);
scanf("%s", students[i].name);
scanf("%f", &students[i].marks);
}
/* Finding the student with the highest marks */
int maxIndex = 0;
for(int i = 1; i < 3; i++)
{
if(students[i].marks > students[maxIndex].marks)
maxIndex = i;
}
printf("Top student: %s\n", students[maxIndex].name);students[0] is a complete struct Student with its own roll, name, and marks — not a primitive value like in a normal array. This is the most common pattern for managing collections of records in C.Nested Structures
A structure can contain another structure as one of its members — used whenever one real-world object naturally contains another.
#include <stdio.h>
struct Date
{
int day;
int month;
int year;
};
struct Student
{
int roll;
char name[30];
struct Date dob; /* nested structure member */
};
int main()
{
struct Student s;
s.roll = 101;
s.dob.day = 15; /* access through TWO dots */
s.dob.month = 8;
s.dob.year = 2005;
printf("%02d/%02d/%04d\n", s.dob.day, s.dob.month, s.dob.year);
return 0;
}Common Nested Structure Patterns
Employee → Address
An employee’s home address (city, PIN code) is naturally a sub-record within the employee.
Car → Engine
A car’s engine specs (horsepower, fuel type) form their own logical group inside the car.
Computer → CPU/RAM
Hardware specs naturally nest: a Computer struct containing CPU, RAM, and Storage sub-structs.
Passing Structures to Functions
There are two ways to pass a structure to a function — and the choice matters for performance.
Arrow Operator (->) & Returning Structures
When you have a pointer to a structure, the dot operator doesn’t work directly. Use the arrow operator (->) instead — it’s shorthand for dereferencing the pointer, then accessing the member.
struct Student s = {101};
struct Student *ptr = &s;
/* ptr.roll; ← WRONG, compile error */
printf("%d\n", ptr->roll); /* CORRECT — arrow operator */
printf("%d\n", (*ptr).roll); /* equivalent, but less common */| You have… | Use | Example |
|---|---|---|
| A structure variable | . | s1.roll |
| A pointer to a structure | -> | ptr->roll |
Returning Structures from Functions
struct Point
{
int x;
int y;
};
struct Point createPoint()
{
struct Point p = {10, 20};
return p; /* the structure is copied out */
}
int main()
{
struct Point p1 = createPoint();
printf("(%d, %d)\n", p1.x, p1.y); /* (10, 20) */
return 0;
}SensorData structure. The API becomes cleaner and the related values stay together.Structure Assignment — Unlike Arrays!
struct Student s1 = {101, "Alice", 91.5};
struct Student s2;
s2 = s1; /* ALL members copied — valid for structures! */
/* compare: int arr2[5] = arr1; ← would be ILLEGAL for arrays */Structure Padding
Consider struct Data { char a; int b; };. Naively you’d expect 1+4=5 bytes. On most systems, sizeof(struct Data) actually returns 8 bytes. The extra 3 bytes are structure padding — unused bytes the compiler inserts between members to satisfy alignment requirements.
Why Padding Exists
CPUs read multi-byte values (like a 4-byte int) faster when they start at an address that is a multiple of their size — an “aligned” address. Some architectures even fault on misaligned access. The compiler inserts padding so every member lands on a properly aligned address.
Without Padding (naive, often invalid)
With Padding (what actually happens)
3 padding bytes push b to address 0x1004 — a multiple of 4, satisfying int’s 4-byte alignment. Total size: 1 + 3 (pad) + 4 = 8 bytes.
struct Test
{
char a; /* 1 byte, needs 1-byte alignment */
short b; /* 2 bytes, needs 2-byte alignment */
int c; /* 4 bytes, needs 4-byte alignment */
};
/* Typical layout: a(1) + pad(1) + b(2) + c(4) = 8 bytes total */| Data Type | Typical Alignment |
|---|---|
char | 1 byte |
short | 2 bytes |
int | 4 bytes |
float | 4 bytes |
double | 8 bytes (platform-dependent) |
sizeof() on your actual target — don’t assume the numbers shown here apply universally.Alignment & Reducing Padding
Alignment means placing a variable at an address that satisfies the CPU’s requirements — e.g. a 4-byte int at an address that’s a multiple of 4. Member order in your structure directly affects how much padding the compiler must insert.
struct A /* char, int, char — typically 12 bytes */
{
char c; /* 1 byte */
int i; /* needs 3 bytes padding before it */
char d; /* 1 byte, then 3 more padding bytes after */
};
struct B /* int, char, char — typically 8 bytes */
{
int i; /* 4 bytes, naturally aligned */
char c; /* 1 byte */
char d; /* 1 byte — only 2 bytes padding needed */
};| Structure | Member Order | Typical Size |
|---|---|---|
struct A | char, int, char | 12 bytes |
struct B | int, char, char | 8 bytes |
double → int → short → char). This minimizes the gaps the compiler needs to insert and can meaningfully shrink your structure’s memory footprint — important when you have arrays of thousands of these structures.Padding may also be added at the end of a structure so its total size is a multiple of its strictest member’s alignment requirement — this matters when the structure itself is used in an array, so every element starts properly aligned.
Packed Structures & sizeof()
Sometimes padding must be removed entirely — for network packets, hardware registers, EEPROM layouts, or any binary format that must match an external specification exactly.
struct __attribute__((packed)) Packet
{
char id; /* 1 byte */
int value; /* 4 bytes — normally padded, now NOT */
};
/* sizeof(struct Packet) == 5, not 8 — padding removed */packed only when the binary layout must match an external spec exactly, never as a default style choice.struct Student
{
int roll; /* 4 bytes */
char grade; /* 1 byte */
};
printf("%zu\n", sizeof(struct Student));
/* Many beginners expect 5 — typical output is 8, due to padding */sizeof() on your target compiler.Unions
Suppose a variable should hold either an integer, a float, or a character — but never all three at the same time. A structure would reserve memory for all three simultaneously, wasting space. A union solves this: all members share the same memory location, and only one member holds a meaningful value at any given moment.
union Data
{
int i;
float f;
char c;
};
union Data d;
d.i = 100;
printf("%d\n", d.i); /* 100 */A structure reserves separate space for every member; a union overlays all members on the same bytes.
union Data d;
d.i = 100; /* d's memory now represents 100 as an int */
d.f = 3.14; /* same bytes reinterpreted — d.i is no longer valid */
/* Only the MOST RECENTLY written member should be read */| Property | Structure | Union |
|---|---|---|
| Memory per member | Separate | Shared (overlapping) |
| Members valid simultaneously? | All of them | Only the last one written |
| Total size | Sum of members (+padding) | Size of largest member (+alignment) |
Enumerations (enum)
Imagine int state = 0; somewhere in a large codebase. What does 0 mean? Nobody can tell without hunting through documentation. An enumeration replaces these “magic numbers” with named integer constants.
enum State
{
OFF, /* = 0 by default */
ON /* = 1 by default */
};
enum State state = ON;
printf("%d\n", state); /* 1 */
/* Day-of-week example: each name auto-increments from 0 */
enum Day { MON, TUE, WED, THU, FRI };
enum Day today = WED;
printf("%d\n", today); /* 2 */enum ErrorCode
{
OK = 0,
WARNING = 100,
ERROR = 200
};
/* Embedded: motor state machine — self-documenting code */
enum MotorState
{
MOTOR_OFF,
MOTOR_STARTING,
MOTOR_RUNNING,
MOTOR_FAULT
};
enum MotorState current = MOTOR_RUNNING;
if(current == MOTOR_FAULT)
{
/* handle fault — reads far better than "if(current == 3)" */
}Readability
MOTOR_RUNNING is instantly understood; 2 requires a lookup table or a comment.
Easier Debugging
A debugger can show the symbolic name MOTOR_FAULT instead of a bare integer.
Fewer Magic Numbers
Every hardcoded number in conditionals becomes a named, searchable identifier.
typedef
Repeatedly writing struct Student or unsigned long everywhere is tedious. typedef creates a shorter alias for an existing type — it does not create a new type, just a new name for one that already exists.
typedef existing_type new_name;
typedef unsigned int uint;
uint age; /* same as: unsigned int age; */typedef with Structures (Very Common in Embedded C)
typedef struct
{
int roll;
char name[30];
} Student;
Student s1; /* no need to write "struct Student" anymore */typedef int* IntPtr;
IntPtr p1, p2; /* BOTH are int* — because IntPtr IS int* */
/* compare to: int *p1, p2; ← only p1 is a pointer here! */uint exactly as unsigned int in every respect — there’s no type-safety boundary between them. This is a common interview trap: typedef improves readability, but it does not create a distinct, incompatible type.typedef constantly for register structures, fixed-width types (uint8_t, uint32_t from stdint.h are themselves typedefs), and function pointer types — it makes complex declarations dramatically more readable.Bit-fields
Embedded systems frequently work with hardware registers where individual bits carry different meanings. Using a full int for a single on/off flag wastes 31 bits. Bit-fields let you specify exactly how many bits a member occupies.
struct Register
{
unsigned int enable : 1; /* 1 bit */
unsigned int mode : 2; /* 2 bits */
unsigned int error : 1; /* 1 bit */
unsigned int unused : 4; /* 4 bits */
};
struct Register reg;
reg.enable = 1;
reg.mode = 2;
reg.error = 0;| Approach | Memory for 3 flags |
|---|---|
Three separate int variables | 12 bytes (4 each) |
| Bit-fields packed into one register | As little as 1 byte |
reg |= (1 << 5) from Chapter 2) because it’s fully portable across compilers, even though bit-fields read more naturally in code.Self-Referential Structures
Sometimes a structure needs to reference another object of its own type — like a train coach pointing to the next coach. A structure cannot directly contain another instance of itself (that would require infinite memory), but it can contain a pointer to another instance.
#include <stdio.h>
struct Node
{
int data;
struct Node *next; /* pointer to ANOTHER Node — not a Node itself */
};
int main()
{
struct Node n1 = {10, NULL};
struct Node n2 = {20, NULL};
n1.next = &n2; /* chain n1 → n2 */
printf("%d\n", n1.data); /* 10 */
printf("%d\n", n1.next->data); /* 20 — follow the pointer */
return 0;
}n1’s next pointer chains to n2; n2’s next is NULL, marking the end of the chain.
Structures in Embedded Firmware
Structures appear everywhere in real embedded firmware: peripheral configuration, sensor readings, communication packets, device drivers, and RTOS task control blocks.
typedef struct
{
unsigned int baudRate;
unsigned char parity;
unsigned char stopBits;
} UART_Config;
UART_Config uart1;
uart1.baudRate = 115200;
uart1.parity = 0;
uart1.stopBits = 1;
/* Sensor readings grouped into one return value */
typedef struct
{
float temperature;
float humidity;
float pressure;
} SensorData;
SensorData readSensor(); /* one clean call instead of 3 output params */Peripheral Config
UART, SPI, I2C settings grouped into one configuration struct passed to an init function.
Sensor Data
Temperature, humidity, pressure returned together — cleaner than three separate output parameters.
Comm Packets
Header, payload, checksum modeled as a struct that mirrors the wire protocol byte-for-byte.
RTOS Task Blocks
Task state, priority, stack pointer all bundled per-task in real-time operating systems.
Register Mapping
Instead of remembering individual hardware addresses, embedded engineers model a peripheral’s registers as a structure, then point to the peripheral’s base address. Member names replace raw hex addresses entirely.
typedef struct
{
volatile unsigned int CONTROL;
volatile unsigned int STATUS;
volatile unsigned int DATA;
} UART_Register;
/* The peripheral's actual base address in memory */
#define UART ((UART_Register *)0x40011000)
UART->CONTROL = 0x01; /* enable the peripheral */
UART->DATA = 0x55; /* send a byte */
if(UART->STATUS & 0x01)
{
/* Data ready — far more readable than raw pointer arithmetic */
}volatile, the compiler may assume the value can’t change and optimize away repeated reads, serving a stale cached value instead of the actual register content.typedef struct for the register layout, volatile on every field, and a #define casting the base address — is the standard idiom for memory-mapped peripherals across virtually every microcontroller vendor’s header files (STM32, ESP32, AVR, and more).Practical Design Patterns
typedef struct
{
unsigned char pin;
unsigned char state;
} LED;
void LED_Init(LED *led);
void LED_On(LED *led);
void LED_Off(LED *led);
LED led1;
led1.pin = 13;
LED_Init(&led1);
LED_On(&led1);
/* This design scales effortlessly to controlling many LEDs */typedef struct for the object, then write functions that take a pointer to it (LED_On(LED *led)). This is the closest C gets to object-oriented design — the structure is the “object,” and the pointer-taking functions are its “methods.”Complete Comparison Tables
Structure vs Union
| Feature | Structure | Union |
|---|---|---|
| Memory | Separate per member | Shared among members |
| Members valid simultaneously? | All valid | Only the most recently written |
| Size | Sum of members + padding | Largest member + alignment |
| Best for | Related data that coexists | Memory optimization, multiple views of same bytes |
enum vs #define
| Aspect | enum | #define |
|---|---|---|
| Nature | Typed constants | Text substitution (preprocessor) |
| Debugger visibility | Shows symbolic name | Only shows the literal value |
| Compiler type checks | Yes | None |
typedef vs #define
| Aspect | typedef | #define |
|---|---|---|
| Creates | A type alias | A text replacement |
| Processed by | Compiler (understands the type) | Preprocessor (blind substitution) |
| Type safety | Safer | Less safe |
Dot vs Arrow Operator
| Operator | Used with |
|---|---|
. | A structure variable — s1.roll |
-> | A pointer to a structure — ptr->roll |
Common Mistakes
Structures
Padding, Unions & typedef
Best Practices & Memory Tricks
Use meaningful structure names
Student, not A. The name should describe the real-world object being modeled.
Group logically related data
If two fields always change together and describe one entity, they belong in the same structure.
Order members largest-to-smallest
Reduces padding and shrinks total structure size — matters when you have arrays of thousands of records.
Pass large structures by pointer
Avoid copying entire structures into function parameters; pass &struct instead.
Use unions only when memory sharing is intentional
Don’t reach for a union just to save a few bytes — use it when only one interpretation of the data is ever valid at a time.
Use enum instead of magic numbers
Replace bare integers in conditionals and state variables with named, self-documenting constants.
Use typedef for complex declarations
typedef struct {...} Student; eliminates repetitive struct keywords throughout your code.
Always volatile for memory-mapped registers
Never let the compiler optimize away a read or write to hardware — mark every register field volatile.
“Structures keep everything; unions keep only the latest”
| Concept | Memory hook |
|---|---|
| Structure | Different data, one object — each member has its own room |
| Union | Same memory, different views — one room, many costumes |
| enum | Named integer constants — numbers with name tags |
| typedef | New name, same type — a nickname, not a new person |
| Bit-field | Bit-level storage — renting by the square inch, not the room |
| volatile | Always read, always write — never trust the cache |
| . vs -> | Dot for a variable, arrow for a pointer |
Interview Questions
struct { char c; int i; char d; } typically needs 12 bytes, while struct { int i; char c; char d; } with the same three members reordered typically needs only 8 bytes — because the larger type is aligned first, requiring less padding overall.. accesses a member through a structure variable directly: s1.roll. -> accesses a member through a pointer to a structure: ptr->roll, which is shorthand for (*ptr).roll. Using . on a pointer, or -> on a plain variable, is a compile error.enum replaces unreadable "magic numbers" with named, self-documenting constants. if(state == MOTOR_FAULT) is instantly understood; if(state == 3) requires looking up what 3 means. Enums also give the compiler more type information than #define, and debuggers can display the symbolic name instead of a bare integer.typedef creates a type alias, not a distinct new type. typedef unsigned int uint; makes uint behave exactly like unsigned int in every respect — there is no compiler-enforced type-safety boundary between the alias and the original type. This is a common interview trap.struct Node *next; inside struct Node). It cannot contain an actual instance of itself, because the compiler would need to know the structure's size to compute its own size — an infinite, unsolvable recursion. A pointer has a fixed, known size regardless of what it points to, breaking that cycle. This pattern is the foundation of linked lists, trees, and graphs.Frequently Asked Questions
volatile, the compiler may assume a memory location's value cannot change unless the program itself writes to it, and optimize away what it sees as "redundant" repeated reads. Hardware registers, however, can change asynchronously — a sensor updates a value, a UART receives a byte — completely outside the compiler's view. volatile forces a genuine memory access every single time, preventing the compiler from serving a stale cached value.packed as a general style choice; it's a special-case tool.reg |= (1 << n)) for register access specifically because it has fully predictable, portable behavior.Practice Programs & Chapter Summary
- Create a
Studentstructure with roll, name, and marks. Declare a variable, fill it, and print all fields. - Create an
Employeestructure and print all members using a singleprintfper field. - Create a union containing
int,float, andchar. Printsizeof()the union and explain the result. - Create an
enumfor the seven days of the week and print the integer value ofWED. - Create a
typedefforunsigned longand declare a variable using the new name.
- Store records for five students in an array of structures; find and print the student with the highest marks.
- Create a nested structure: an
Employeecontaining anAddressstructure (city, PIN code). - Write a function that takes a structure pointer and modifies one of its members; verify the change in
main(). - Compare the size of two structures with the same three members but different declaration orders using
sizeof(). - Implement a traffic-light state machine using
enum(RED, YELLOW, GREEN) and a function that prints the next state.
- Design a structure for 10 employees, each containing ID, name, salary, and a nested Date-of-joining structure. Find and display the employee with the highest salary.
- Design an 8-bit status register using bit-fields: bit 0 Power, bit 1 Error, bits 2–3 Mode, bits 4–7 Reserved. Write a program to set and display each field.
- Design a communication packet structure (1-byte header, 4-byte timestamp, 2-byte length, 1-byte checksum). Arrange members to minimize padding, then verify the size with
sizeof(). - Design a microcontroller GPIO register map (Direction, Input, Output, Pull-up registers) using a
typedef struct, and access it through a pointer to a fixed base address. - Build a singly linked list of 5 nodes using a self-referential structure. Write functions to insert a node at the end and print the entire list by traversal.
- A structure groups variables of different types under one name; declaring it doesn’t allocate memory — creating a variable does.
- Access members with
.for variables and->for pointers;ptr->mis shorthand for(*ptr).m. - Designated initializers (
.field = value) are clearer and order-independent compared to positional initialization. - Arrays of structures store collections of records; nested structures model one object naturally containing another.
- Pass large structures by pointer to avoid expensive copies; structures (unlike arrays) can be assigned directly with
=. - Structure padding inserts unused bytes to satisfy CPU alignment requirements —
sizeofis rarely the simple sum of member sizes. - Member order affects total padding; ordering larger types first typically minimizes structure size.
- Packed structures remove padding for exact binary layouts (network packets, hardware registers) at the cost of possibly slower or faulting unaligned access.
- A union shares one memory location among all members; only the most recently written member is valid.
enumreplaces magic numbers with named integer constants, improving readability and debuggability.typedefcreates a type alias (not a new type) to simplify complex or repetitive declarations.- Bit-fields allow bit-level storage for hardware registers, though their exact layout is implementation-defined.
- Self-referential structures (a struct containing a pointer to its own type) are the foundation of linked lists, trees, and graphs.
- Embedded register mapping combines
typedef struct,volatilefields, and a base-address#defineto give hardware registers readable names.