When I first started writing C, I leaned on arrays and separate variables for everything, until the code became a mess of parallel arrays that were easy to get out of sync. Structures fixed that instantly — they let you group related data into one coherent unit. Unions, on the other hand, took me a bit longer to really “get,” because they solve a different problem: sharing memory between different interpretations of the same data. Once both clicked, a huge amount of C code — from simple record-keeping programs to protocol parsers — suddenly made a lot more sense.
This guide covers both concepts from the ground up, with full working code, memory layout explanations, and the practical patterns you’ll actually use.
Table of Contents
- Why Structures Exist
- Declaring and Initializing Structures
- Accessing and Modifying Structure Members
- Nested Structures
- Arrays of Structures
- Pointers to Structures
- Passing Structures to Functions
- Structure Padding and Memory Alignment
- Unions: Sharing Memory
- Structures vs. Unions: Key Differences
- typedef with Structures and Unions
- Best Practices
- Performance Considerations
- Debugging Structures and Unions
- Common Mistakes
- Real-World Applications
- Interview Questions
- FAQs
- Summary and Key Takeaways
- References
1. Why Structures Exist
Imagine modeling a student record: name, roll number, and GPA. Without structures, you’d need three separate arrays — names[], rollNumbers[], gpas[] — and you’d have to trust yourself to keep index i consistent across all three everywhere in your code. One mistake, and student data silently gets mixed up. A struct bundles these related fields into a single named type, so the compiler enforces the grouping and your code reads far more naturally.
2. Declaring and Initializing Structures
#include <stdio.h>
#include <string.h>
struct Student {
char name[50];
int rollNumber;
float gpa;
};
int main(void) {
struct Student s1 = {"Ayesha Khan", 101, 3.8f};
// Designated initializers (C99+) - clearer and order-independent
struct Student s2 = {.rollNumber = 102, .gpa = 3.5f, .name = "Bilal Ahmed"};
printf("s1: %s, Roll: %d, GPA: %.2f\n", s1.name, s1.rollNumber, s1.gpa);
printf("s2: %s, Roll: %d, GPA: %.2f\n", s2.name, s2.rollNumber, s2.gpa);
return 0;
}
Output:
s1: Ayesha Khan, Roll: 101, GPA: 3.80
s2: Bilal Ahmed, Roll: 102, GPA: 3.50
Designated initializers (.fieldName = value) are especially useful in larger structures — you don’t have to remember or match the exact declared field order, and the code documents itself.
3. Accessing and Modifying Structure Members
You use the dot operator (.) for a structure variable and the arrow operator (->) for a pointer to a structure.
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point p1 = {3, 4};
p1.x = 10; // modify directly
struct Point *ptr = &p1;
ptr->y = 20; // equivalent to (*ptr).y = 20
printf("Point: (%d, %d)\n", p1.x, p1.y);
return 0;
}
Output:
Point: (10, 20)
4. Nested Structures
Structures can contain other structures as members, which is how you model hierarchical, real-world data.
#include <stdio.h>
struct Date {
int day, month, year;
};
struct Employee {
char name[50];
struct Date joiningDate;
};
int main(void) {
struct Employee e1 = {"Sana Malik", {15, 6, 2022}};
printf("Employee: %s\n", e1.name);
printf("Joining Date: %02d-%02d-%04d\n",
e1.joiningDate.day, e1.joiningDate.month, e1.joiningDate.year);
return 0;
}
Output:
Employee: Sana Malik
Joining Date: 15-06-2022
5. Arrays of Structures
This is one of the most common real-world patterns — a collection of records, like a small in-memory database table.
#include <stdio.h>
struct Product {
char name[30];
float price;
};
int main(void) {
struct Product inventory[3] = {
{"Notebook", 2.50f},
{"Pen", 0.75f},
{"Backpack", 25.00f}
};
float total = 0;
for (int i = 0; i < 3; i++) {
printf("%-10s: $%.2f\n", inventory[i].name, inventory[i].price);
total += inventory[i].price;
}
printf("Total: $%.2f\n", total);
return 0;
}
Output:
Notebook : $2.50
Pen : $0.75
Backpack : $25.00
Total: $28.25
6. Pointers to Structures
Working with structures through pointers avoids copying large amounts of data and is essential for building dynamic data structures like linked lists and trees.
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
struct Node *next;
};
int main(void) {
struct Node *head = malloc(sizeof(struct Node));
head->data = 1;
head->next = malloc(sizeof(struct Node));
head->next->data = 2;
head->next->next = NULL;
struct Node *current = head;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
// Clean up
free(head->next);
free(head);
return 0;
}
Output:
1 -> 2 -> NULL
Notice struct Node contains a pointer to itself (struct Node *next) — this self-referential structure is exactly how linked lists, trees, and graphs are built in C.
7. Passing Structures to Functions
You can pass structures by value (a full copy) or by pointer (a reference, no copy).
#include <stdio.h>
struct Rectangle {
float width, height;
};
// Pass by value: original is untouched, a full copy is made
float calculateArea(struct Rectangle r) {
return r.width * r.height;
}
// Pass by pointer: can modify the original, avoids copying
void scaleRectangle(struct Rectangle *r, float factor) {
r->width *= factor;
r->height *= factor;
}
int main(void) {
struct Rectangle rect = {4.0f, 5.0f};
printf("Original area: %.2f\n", calculateArea(rect));
scaleRectangle(&rect, 2.0f);
printf("After scaling, area: %.2f\n", calculateArea(rect));
return 0;
}
Output:
Original area: 20.00
After scaling, area: 80.00
For large structures, always prefer passing a pointer (often const struct Rectangle * if you don’t need to modify it) — passing by value copies every byte of the structure onto the stack, which is wasteful for anything beyond a few small fields.
8. Structure Padding and Memory Alignment
This is where structures get genuinely interesting at the internals level. Compilers align structure members in memory according to the target architecture’s alignment requirements, which means the compiler may insert invisible padding bytes between members.
#include <stdio.h>
struct Example1 {
char a; // 1 byte
int b; // 4 bytes
char c; // 1 byte
};
struct Example2 {
int b; // 4 bytes
char a; // 1 byte
char c; // 1 byte
};
int main(void) {
printf("sizeof(Example1) = %zu\n", sizeof(struct Example1));
printf("sizeof(Example2) = %zu\n", sizeof(struct Example2));
return 0;
}
Output (typical on a 64-bit system):
sizeof(Example1) = 12
sizeof(Example2) = 8
Even though both structures contain the exact same members, Example1 is larger because the compiler inserts padding to keep int b aligned on a 4-byte boundary, and adds trailing padding so arrays of the structure stay aligned too. Example2 avoids most of that waste simply by ordering members from largest to smallest. This is a real, practical technique: order structure members from largest to smallest type to minimize padding in memory-sensitive applications.
You can eliminate padding almost entirely with a compiler-specific directive, though it comes at a performance cost on some architectures due to unaligned memory access:
#pragma pack(push, 1)
struct Packed {
char a;
int b;
char c;
};
#pragma pack(pop)
9. Unions: Sharing Memory
A union looks syntactically like a structure, but all its members share the same memory location. The union’s total size is the size of its largest member, and writing to one member overwrites the bytes of all the others.
#include <stdio.h>
union Data {
int i;
float f;
char str[20];
};
int main(void) {
union Data data;
data.i = 10;
printf("data.i = %d\n", data.i);
data.f = 3.14f;
printf("data.f = %.2f\n", data.f);
printf("data.i after setting f = %d (garbage, overwritten memory)\n", data.i);
printf("sizeof(union Data) = %zu\n", sizeof(union Data));
return 0;
}
Output (typical):
data.i = 10
data.f = 3.14
data.i after setting f = 1078523331 (garbage, overwritten memory)
Notice sizeof(union Data) equals 20 bytes (the size of the largest member, str[20]), not the sum of all members. And once you write to data.f, reading data.i no longer gives you meaningful data — you’re reading the same memory bytes reinterpreted as a different type. This is exactly why unions are useful for type-punning and tagged unions, but dangerous if you forget which member you last wrote.
A Practical Tagged Union Pattern
#include <stdio.h>
enum ValueType { TYPE_INT, TYPE_FLOAT, TYPE_STRING };
struct TaggedValue {
enum ValueType type;
union {
int i;
float f;
char str[20];
} value;
};
void printValue(struct TaggedValue v) {
switch (v.type) {
case TYPE_INT: printf("Int: %d\n", v.value.i); break;
case TYPE_FLOAT: printf("Float: %.2f\n", v.value.f); break;
case TYPE_STRING: printf("String: %s\n", v.value.str); break;
}
}
int main(void) {
struct TaggedValue a = {.type = TYPE_INT, .value.i = 42};
struct TaggedValue b = {.type = TYPE_STRING, .value.str = "hello"};
printValue(a);
printValue(b);
return 0;
}
Output:
Int: 42
String: hello
This “tagged union” pattern — a struct pairing a type indicator (the enum) with a union of possible values — is how many real interpreters and parsers represent variant data types safely, since the tag tells you which union member is currently valid.
10. Structures vs. Unions: Key Differences
| Aspect | Structure | Union |
|---|---|---|
| Memory | Each member has its own space | All members share the same space |
| Size | Sum of all members (plus padding) | Size of the largest member |
| Simultaneous access | All members hold valid data at once | Only the last-written member holds valid data |
| Use case | Grouping unrelated-but-related fields (a record) | Representing one value as different types (space-saving, type-punning) |
11. typedef with Structures and Unions
typedef lets you avoid writing struct or union every time you declare a variable, which makes code noticeably cleaner.
#include <stdio.h>
typedef struct {
int x, y;
} Point;
typedef union {
int i;
float f;
} Number;
int main(void) {
Point p = {5, 10}; // no "struct" keyword needed
Number n;
n.f = 9.5f;
printf("Point: (%d, %d)\n", p.x, p.y);
printf("Number as float: %.2f\n", n.f);
return 0;
}
Output:
Point: (5, 10)
Number as float: 9.50
12. Best Practices
- Order structure members from largest to smallest to reduce padding waste.
- Use
typedeffor cleaner, more readable type names in larger codebases. - Pass large structures by pointer (ideally
constpointer if read-only) instead of by value. - Always track which member of a union is currently valid — a tag field (enum) alongside the union is the standard, safe pattern.
- Use designated initializers for clarity, especially with structures that have many fields.
- Zero-initialize structures you plan to fill incrementally (
struct Foo f = {0};) to avoid leftover garbage in unused fields.
13. Performance Considerations
- Structure padding can meaningfully bloat memory usage in large arrays of structures — reordering members is a free, zero-risk optimization worth doing habitually.
- Passing structures by value in function calls copies every byte, which adds up in hot loops; pass by pointer for anything beyond a couple of small fields.
- Unions are a deliberate memory-saving technique — when you know only one “view” of data is needed at a time, a union uses far less memory than a structure holding every possible type separately.
- Be cautious with
#pragma pack— tightly packed structures can cause slower memory access on architectures that penalize unaligned reads, even though they save space.
14. Debugging Structures and Unions
- Use
sizeof()liberally during development to confirm your assumptions about memory layout, especially before writing structures to binary files. - In GDB,
printon a structure variable shows all fields at once, which is much faster for inspection than printing members one by one. - For unions, always double-check which member you last wrote before reading — a debugger will happily show you a “value” that’s actually garbage from a different type’s bit pattern.
- When debugging binary file formats built from structs, watch for padding differences between the machine that wrote the file and the machine reading it back.
15. Common Mistakes
- Assuming
sizeof(struct)equals the sum of member sizes — padding usually makes it larger. - Reading a union member you didn’t most recently write, and getting garbage or misinterpreted data.
- Comparing structures directly with
==— C doesn’t support this; you must compare member by member (or usememcmpcarefully, mindful of padding bytes). - Forgetting to allocate memory for pointer members inside a structure before using them.
- Copying structures containing pointers by value (“shallow copy”) and ending up with two structures pointing to the same dynamically allocated memory, leading to double frees.
- Not using a tag/discriminator with a union, causing bugs when the “wrong” member is read.
16. Real-World Applications
Structures and unions are everywhere: modeling database records, representing network packet headers (a struct with bit-fields mapped to a protocol’s exact byte layout), game entity data, tagged unions in interpreters and compilers for representing different token or AST node types, and hardware register mapping in embedded systems, where a union lets you view the same memory as either a whole register or its individual bit-fields.
17. Interview Questions
- What’s the difference between a structure and a union in terms of memory allocation?
- Why might
sizeof(struct)not equal the sum of its members’ sizes? - How would you implement a linked list using structures?
- What is a tagged union, and why is it useful?
- Explain shallow copy vs. deep copy in the context of structures containing pointers.
- How does
#pragma packaffect structure size and performance? - Can a structure contain a pointer to itself? Why is this necessary for certain data structures?
18. FAQs
Can a union contain a structure? Yes. Structures and unions can be nested inside each other in either direction, which is exactly the basis of the tagged-union pattern shown earlier.
Why does my structure take more memory than expected? Compiler-inserted padding for alignment. Reordering members from largest to smallest type typically reduces or eliminates this.
Is it safe to compare two structures with ==? No — C does not allow direct equality comparison of structures. Compare each member individually, or use memcmp only if you’re certain there’s no uninitialized padding involved.
19. Summary and Key Takeaways
Structures let you group related data into a single, well-defined type, which is the foundation for records, linked data structures, and organized program design in C. Unions let multiple types share the same memory, which is powerful for space efficiency and type-punning, but only safe when paired with a way to track which member is currently valid. Understanding padding and alignment isn’t just trivia — it directly affects memory usage and, in some cases, performance, especially in large arrays of structures or binary file formats.
20. References
- ISO/IEC 9899 — Programming languages C (Section 6.7.2.1, “Structure and union specifiers”)
- GCC Online Documentation — https://gcc.gnu.org/onlinedocs/
- GCC documentation on
#pragma packand structure layout - The C Standard Library reference for
<stddef.h>(offsetofmacro, useful for inspecting padding)