📚 Chapter 6 🟠 Intermediate → Advanced ⏱ 60–90 min

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.

🎯 Learning Objectives
Declare and initialize structures correctly
Understand structure padding and alignment
Use unions to share memory between members
Replace magic numbers with enum constants
Simplify declarations with typedef
Map hardware registers using structs & bit-fields
1Structure Basics

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.

✗ Without structures — scattered variables
int roll; char name[30]; int age; float marks; /* For 100 students: 400 unrelated variables! */
✓ With a structure — one logical unit
struct Student { int roll; char name[30]; int age; float marks; }; /* One struct, then: struct Student s[100]; */
💡
The Core Insight
Different pieces of information that belong to one object should stay together. A student’s roll number, name, age, and marks aren’t independent facts — they describe a single entity. A structure groups related variables into one logical unit, regardless of their individual data types.
2Structure Basics

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.

💳
Employee ID Card Analogy
An ID card has multiple fields — ID number, name, department, salary — all describing one employee. They have different types (number, text, text, currency) but belong together on one card. A structure represents exactly this idea in code.
👤

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.

3Structure Basics

Declaring & Creating Structure Variables

C — Structure declaration syntax
struct StructureName
{
    dataType member1;
    dataType member2;
    /* ... */
};   /* ← semicolon is MANDATORY here */
C — A complete Student structure
struct Student
{
    int   roll;
    char  name[30];
    int   age;
    float marks;
};
⚠️
Declaring a Structure Doesn’t Allocate Memory
Writing 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;.
C — Creating one or many structure variables
struct Student s1;          /* one variable — memory now allocated */

struct Student s1, s2, s3;  /* three independent student records   */
4Structure Basics

Accessing Members & Initialization

Use the dot operator (.) to access a structure member through a variable.

C — Complete program: assign, then print
#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;
}
Output
Roll : 101 Name : Alice Age : 20 Marks : 89.50
Why Not s1.name = "Alice"?
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

Positional (order matters)
struct Student s1 = { 101, "Alice", 20, 89.5 };
Designated (C99, order-free)
struct Student s1 = { .roll = 101, .age = 20, .marks = 89.5, .name = "Alice" };
Why Designated Initializers Win
They are easier to read, the order of fields doesn’t matter, and they are far safer for large structures — you can’t accidentally swap two same-type fields by writing them in the wrong position.
5Structure Basics

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.

⚠️
sizeof Isn’t Always the Sum of Members
For 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

PropertyArrayStructure
Element typesSame type throughout (homogeneous)Different types allowed (heterogeneous)
Access syntaxarr[i] (index)s.member (name)
Exampleint marks[5]; — five integersstruct Student — int + char[] + float together
Direct assignmentNot allowed (a = b)Allowed (s2 = s1) — see 9
6Structures in Depth

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.

C — Array of structures: declare, fill, find max
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);
💡
Each Element is a Full Record
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.
7Structures in Depth

Nested Structures

A structure can contain another structure as one of its members — used whenever one real-world object naturally contains another.

C — A Student that contains a Date of birth
#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;
}
Output
15/08/2005

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.

8Structures in Depth

Passing Structures to Functions

There are two ways to pass a structure to a function — and the choice matters for performance.

Method 1 — Pass entire structure
void display(struct Student s) { printf("%d", s.roll); } display(student1); /* ENTIRE structure copied — expensive for large structs */
Method 2 — Pass pointer (preferred)
void display(struct Student *s) { printf("%d", s->roll); } display(&student1); /* Only the ADDRESS is copied — fast regardless of struct size */
Prefer Passing by Pointer for Large Structures
Copying a structure with many members (or large arrays inside it) every time you call a function is expensive in both memory and CPU time. Passing a pointer copies only a few bytes (the address) regardless of how large the structure is.
9Structures in Depth

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.

C — Dot vs arrow
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…UseExample
A structure variable.s1.roll
A pointer to a structure->ptr->roll

Returning Structures from Functions

C — A function that returns a whole structure
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;
}
🔌
Embedded Use Case: Sensor Readings
Instead of a sensor function returning temperature, pressure, and humidity as three separate output parameters, return one SensorData structure. The API becomes cleaner and the related values stay together.

Structure Assignment — Unlike Arrays!

C — Structures CAN be assigned directly
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 */
10Padding & Alignment

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)

a
0x1000
b (misaligned!)
0x1001

With Padding (what actually happens)

a
0x1000
pad
0x1001
pad
0x1002
pad
0x1003
b (int, 4 bytes)
0x1004

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.

C — Typical alignment requirements (implementation-defined)
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 TypeTypical Alignment
char1 byte
short2 bytes
int4 bytes
float4 bytes
double8 bytes (platform-dependent)
⚠️
Alignment is Implementation-Defined
Exact alignment rules depend on the compiler and target architecture. Always verify with sizeof() on your actual target — don’t assume the numbers shown here apply universally.
11Padding & Alignment

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.

C — Same members, different order, different size
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 */
};
StructureMember OrderTypical Size
struct Achar, int, char12 bytes
struct Bint, char, char8 bytes
Order Larger Types First
A simple rule of thumb: declare members from largest alignment requirement to smallest (doubleintshortchar). 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.

12Padding & Alignment

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.

C — GCC packed attribute
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 Structures Have a Cost
Removing padding can cause slower memory access — or even a hardware fault — on architectures that don’t support unaligned memory access. Use packed only when the binary layout must match an external spec exactly, never as a default style choice.
C — sizeof rarely equals the sum of member sizes
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 */
🔌
Hardware Register Layout Must Be Exact
If a 32-bit hardware register is laid out as bits 31–16 Status, 15–8 Mode, 7–0 Flags, the C structure modeling it must match that binary layout byte-for-byte. Unexpected padding silently breaks communication with the hardware — always verify with sizeof() on your target compiler.
13Other Custom Types

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.

C — Union syntax and basic use
union Data
{
    int   i;
    float f;
    char  c;
};

union Data d;
d.i = 100;
printf("%d\n", d.i);    /* 100 */
Structure (separate memory) int float char Total: 4+4+1 = 9 bytes (+padding) Union (shared memory) int float char all share ONE location Total: 4 bytes (size of largest member)

A structure reserves separate space for every member; a union overlays all members on the same bytes.

C — Writing one member overwrites the others
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 */
PropertyStructureUnion
Memory per memberSeparateShared (overlapping)
Members valid simultaneously?All of themOnly the last one written
Total sizeSum of members (+padding)Size of largest member (+alignment)
🔌
Embedded Use: Multiple Views of the Same Bytes
A communication packet’s raw bytes might need interpreting as an integer, a float, or a byte array depending on context. A union lets you provide all three “views” without copying memory — write once as bytes, read back as whatever type makes sense.
14Other Custom Types

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.

C — enum syntax and default values
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 */
C — Custom values and an embedded state machine
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.

15Other Custom Types

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.

C — typedef syntax and basic alias
typedef existing_type new_name;

typedef unsigned int uint;

uint age;        /* same as: unsigned int age; */

typedef with Structures (Very Common in Embedded C)

C — Eliminating the "struct" keyword everywhere
typedef struct
{
    int  roll;
    char name[30];
} Student;

Student s1;     /* no need to write "struct Student" anymore */
C — typedef with pointers — read carefully
typedef int* IntPtr;

IntPtr p1, p2;     /* BOTH are int* — because IntPtr IS int* */
/* compare to:    int *p1, p2;   ← only p1 is a pointer here! */
⚠️
typedef Is Just an Alias, Not a New Type
The compiler treats 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.
Why typedef Is Everywhere in Embedded Code
Embedded codebases use 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.
16Other Custom Types

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.

C — Bit-field syntax for an 8-bit register
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;
8-bit register layout — each field uses exactly the bits it needs
bit 7–4
0
0
0
0
unused (4 bits)
bit 3
0
error (1 bit)
bit 2–1
1
0
mode = 2 (2 bits)
bit 0
1
enable = 1 (1 bit)
ApproachMemory for 3 flags
Three separate int variables12 bytes (4 each)
Bit-fields packed into one registerAs little as 1 byte
⚠️
Bit-field Layout Is Implementation-Defined
Different compilers may order bits differently (e.g., left-to-right vs right-to-left), and there’s no portable guarantee of exact memory layout. For hardware register access, many embedded projects prefer explicit bit masking (reg |= (1 << 5) from Chapter 2) because it’s fully portable across compilers, even though bit-fields read more naturally in code.
17Embedded Applications

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.

C — A self-referential Node, the basis of linked lists
#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;
}
Output
10 20
data = 10 next → data = 20 next = NULL

n1’s next pointer chains to n2; n2’s next is NULL, marking the end of the chain.

🔗
The Foundation of Dynamic Data Structures
Self-referential structures are how C builds linked lists, trees, graphs, queues, and stacks. Almost every dynamic data structure you’ll study next relies on exactly this pattern: a struct holding data plus one or more pointers to other structs of the same type.
18Embedded Applications

Structures in Embedded Firmware

Structures appear everywhere in real embedded firmware: peripheral configuration, sensor readings, communication packets, device drivers, and RTOS task control blocks.

C — UART configuration and sensor data structures
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.

19Embedded Applications

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.

C — Mapping a UART peripheral's registers
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 */
}
⚠️
Always volatile for Hardware Registers
Hardware registers can change without your program writing to them — a sensor updates a value, a UART receives a byte. Without 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.
💡
Why This Pattern Is Everywhere
This combination — 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).
20Embedded Applications

Practical Design Patterns

C — An LED driver built around a structure
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 */
✗ Loose, scattered variables
int temperature; int humidity; int pressure; /* No grouping, no clear API, easy to mismatch parameters */
✓ One structured type
typedef struct { float temperature; float humidity; float pressure; } SensorData; /* Clean API, single source of truth */
The Pattern: typedef struct + Pointer Functions
Define a 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.”
21Review

Complete Comparison Tables

Structure vs Union

FeatureStructureUnion
MemorySeparate per memberShared among members
Members valid simultaneously?All validOnly the most recently written
SizeSum of members + paddingLargest member + alignment
Best forRelated data that coexistsMemory optimization, multiple views of same bytes

enum vs #define

Aspectenum#define
NatureTyped constantsText substitution (preprocessor)
Debugger visibilityShows symbolic nameOnly shows the literal value
Compiler type checksYesNone

typedef vs #define

Aspecttypedef#define
CreatesA type aliasA text replacement
Processed byCompiler (understands the type)Preprocessor (blind substitution)
Type safetySaferLess safe

Dot vs Arrow Operator

OperatorUsed with
.A structure variables1.roll
->A pointer to a structure — ptr->roll
22Review

Common Mistakes

Structures

✗ Missing semicolon after closing brace
struct Student { int roll; } /* compile error — semicolon required! */
✓ Semicolon after the closing brace
struct Student { int roll; };
✗ Assigning a string with =
s1.name = "John"; /* error — arrays can't be assigned */
✓ Use strcpy()
strcpy(s1.name, "John");
✗ Using . with a pointer
struct Student *ptr = &s; ptr.roll = 1; /* compile error */
✓ Use -> with a pointer
struct Student *ptr = &s; ptr->roll = 1; /* correct */

Padding, Unions & typedef

✗ Assuming sizeof = sum of members
struct Data { char a; int b; }; /* assumed: 5 bytes — actual: usually 8 */
✓ Always verify with sizeof()
printf("%zu", sizeof(struct Data)); /* never assume — always measure */
✗ Reading a stale union member
union Data d; d.f = 3.14; printf("%d", d.i); /* d.i is garbage — only d.f was written */
✓ Read only the most recently written member
union Data d; d.f = 3.14; printf("%f", d.f); /* correct member */
✗ Forgetting volatile on registers
unsigned int *STATUS = (unsigned int*)0x4001; while(!(*STATUS & 1)) {} /* compiler may optimize away the re-read! */
✓ Always use volatile for registers
volatile unsigned int *STATUS = (volatile unsigned int*)0x4001; while(!(*STATUS & 1)) {} /* always re-read */
23Review

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.

struct ≠ union
structseparate memory
unionshared memory

“Structures keep everything; unions keep only the latest”

ConceptMemory hook
StructureDifferent data, one object — each member has its own room
UnionSame memory, different views — one room, many costumes
enumNamed integer constants — numbers with name tags
typedefNew name, same type — a nickname, not a new person
Bit-fieldBit-level storage — renting by the square inch, not the room
volatileAlways read, always write — never trust the cache
. vs ->Dot for a variable, arrow for a pointer
24Review

Interview Questions

Q1
What is a structure? Why was it introduced?
A structure is a user-defined data type that groups variables of different data types under one name. It was introduced to keep logically related data (like a student's roll number, name, age, and marks) bundled together as one object, instead of managing dozens of unrelated independent variables.
Q2
Difference between a structure and a union?
A structure allocates separate memory for every member, so all members hold valid values simultaneously. A union makes all members share the same memory location — only the most recently written member holds a meaningful value. A structure's size is roughly the sum of its members (plus padding); a union's size is roughly that of its largest member.
Q3
What is structure padding, and why does it exist?
Structure padding is extra, unused bytes the compiler inserts between (or after) members to satisfy the target architecture's alignment requirements. CPUs generally read aligned data (e.g. a 4-byte int at an address that's a multiple of 4) faster than misaligned data, and some architectures fault on misaligned access entirely. Padding ensures every member lands on a properly aligned address.
Q4
Can two structures with identical members have different sizes?
Yes. Member order affects how much padding the compiler inserts. 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.
Q5
Difference between the dot (.) and arrow (->) operators?
. 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.
Q6
Why use enum instead of plain integer constants?
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.
Q7
Does typedef create a new type?
No. 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.
Q8
What is a self-referential structure, and why can't it contain itself directly?
A self-referential structure contains a pointer to another structure of the same type (e.g. 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

Why is volatile important when mapping hardware registers?
Without 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.
When should I use a packed structure?
Only when the structure's binary layout must exactly match an external specification — a network packet format, an EEPROM layout, or a hardware register map defined by a datasheet. Packed structures remove the compiler's normal padding, which can make memory access slower (or fault entirely) on architectures that don't support unaligned access. Never use packed as a general style choice; it's a special-case tool.
Why are bit-field layouts considered risky for portable hardware code?
The C standard does not specify the exact bit ordering, padding, or storage layout for bit-fields — it's left to each compiler implementation. Code that relies on a specific bit-field layout may behave differently when compiled with a different compiler or for a different target architecture. Many embedded teams prefer explicit bitwise masking (reg |= (1 << n)) for register access specifically because it has fully predictable, portable behavior.
25Review

Practice Programs & Chapter Summary

🟢 Easy
  • Create a Student structure with roll, name, and marks. Declare a variable, fill it, and print all fields.
  • Create an Employee structure and print all members using a single printf per field.
  • Create a union containing int, float, and char. Print sizeof() the union and explain the result.
  • Create an enum for the seven days of the week and print the integer value of WED.
  • Create a typedef for unsigned long and declare a variable using the new name.
🔵 Medium
  • Store records for five students in an array of structures; find and print the student with the highest marks.
  • Create a nested structure: an Employee containing an Address structure (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.
🔴 Challenge
  • 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.
✅ What you mastered in Chapter 6
  • 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->m is 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 —sizeof is 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.
  • enum replaces magic numbers with named integer constants, improving readability and debuggability.
  • typedef creates 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, volatile fields, and a base-address #define to give hardware registers readable names.
📚 Chapter 7 — File Handling & Preprocessor Directives
Persisting data to disk and controlling compilation
FILE, fopen, fclose
Reading & Writing Files
Text vs Binary Files
Error Handling
#include
#define & Macros
Conditional Compilation
Header Files
Include Guards
Compiler Workflow