C Structures - Complete Guide
Master C structures including structures vs unions, structure arrays, pointers to structures, nested structures, bit fields, typedef, and programming best practices.
Structures
User-defined types
vs Unions
Key differences
Arrays & Pointers
Advanced usage
Bit Fields
Memory optimization
Introduction to C Structures
Structures (struct) in C allow you to combine different data types into a single user-defined data type. They enable you to create complex data types that group related variables together, making code more organized and manageable.
Why Structures Matter
- Data Grouping: Combine related data items
- Code Organization: Better code structure and readability
- Real-world Modeling: Represent real-world entities (Student, Employee, etc.)
- Function Arguments: Pass multiple related values as single parameter
- Data Structures: Foundation for linked lists, trees, graphs
- Memory Efficiency: Better memory organization than individual variables
Structure Concepts
- Structure: Collection of different data types
- Union: Shares memory between members
- Array of Structures: Multiple structure instances
- Pointer to Structure: Dynamic structure access
- typedef: Create type aliases
- Nested Structures: Structure within structure
Core Concepts of Structures
Structures create user-defined data types that group related variables of different types. Each variable in a structure is called a member. Structures allocate memory for all members separately, while unions share memory among members. Understanding this fundamental difference is crucial for effective C programming.
Structures vs Unions Comparison
Here is a comprehensive comparison of structures and unions in C:
| Feature | Structure (struct) | Union (union) |
|---|---|---|
| Keyword | struct |
union |
| Memory Allocation | Memory for all members (sum of sizes) | Memory for largest member only |
| Memory Usage | More memory (all members stored) | Less memory (shared space) |
| Access to Members | All members accessible simultaneously | Only one member valid at a time |
| Initialization | All members can be initialized | Only first member can be initialized |
| Use Case | Group related data of different types | Store different types in same memory |
| Size | Sum of all member sizes (+ padding) | Size of largest member (+ padding) |
| Example | struct Student {int id; char name[50];}; |
union Data {int i; float f; char str[20];}; |
Memory Layout Visualization:
Structure Memory Layout
Total: 28 bytes (all members have separate memory)
Union Memory Layout
Total: 20 bytes (all members share same memory)
Basic Structures - Declaration and Usage
Structures allow you to define a new data type that groups variables of different types. Each variable in the structure is called a member or field.
Syntax:
// Structure declaration
struct structure_name {
data_type member1;
data_type member2;
// ... more members
};
// Structure variable declaration
struct structure_name variable_name;
// Accessing members
variable_name.member1 = value;
Key Concepts:
- Member Access: Use dot (.) operator for structure variables
- Memory Allocation: Contiguous memory for all members
- Padding: Compiler may add padding bytes for alignment
- Initialization: Can initialize during declaration
- Assignment: Structures can be assigned to each other (member-wise copy)
- Size: Use
sizeof()to get total size
Basic Structure Examples:
Each program below is small and complete. Compile and run them one at a time.
#include <stdio.h>
#include <string.h>
struct Student {
int id;
char name[50];
float marks;
char grade;
};
int main(void) {
struct Student s;
s.id = 101;
strcpy(s.name, "John Doe");
s.marks = 85.5f;
s.grade = 'A';
printf("ID=%d, Name=%s, Marks=%.1f, Grade=%c\n",
s.id, s.name, s.marks, s.grade);
struct Student t = {102, "Jane Smith", 92.0f, 'A'};
printf("ID=%d, Name=%s\n", t.id, t.name);
return 0;
}
ID=101, Name=John Doe, Marks=85.5, Grade=A
ID=102, Name=Jane Smith
#include <stdio.h>
struct Student {
int id;
char name[50];
float marks;
char grade;
};
int main(void) {
struct Student s = {
.id = 103,
.name = "Bob Johnson",
.marks = 78.5f,
.grade = 'B'
};
printf("%s — marks %.1f\n", s.name, s.marks);
printf("sizeof(struct Student) = %zu bytes (may include padding)\n",
sizeof(struct Student));
return 0;
}
Bob Johnson — marks 78.5
sizeof(struct Student) = 60 bytes (may include padding)
#include <stdio.h>
#include <string.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point a = {10, 20};
struct Point b = a;
b.x = 99;
printf("a: (%d,%d) b: (%d,%d)\n", a.x, a.y, b.x, b.y);
return 0;
}
a: (10,20) b: (99,20)
#include <stdio.h>
struct Counter {
int n;
};
void try_change(struct Counter c) {
c.n = 100;
printf("inside function: n = %d\n", c.n);
}
int main(void) {
struct Counter c = {1};
try_change(c);
printf("after function: n = %d\n", c.n);
return 0;
}
inside function: n = 100
after function: n = 1
#include <stdio.h>
struct Rect {
int width;
int height;
};
struct Rect make_rect(int w, int h) {
struct Rect r;
r.width = w;
r.height = h;
return r;
}
int main(void) {
struct Rect r = make_rect(4, 5);
printf("area = %d\n", r.width * r.height);
return 0;
}
area = 20
#include <stdio.h>
#include <string.h>
struct Student {
int id;
char name[32];
};
int main(void) {
struct Student a = {1, "Amy"};
struct Student b = {1, "Amy"};
int same = (a.id == b.id) && (strcmp(a.name, b.name) == 0);
printf("same? %s\n", same ? "yes" : "no");
return 0;
}
same? yes
#include <stdio.h>
int main(void) {
struct {
int x;
int y;
} p = {3, 4}, q = {10, 20};
printf("p=(%d,%d) q=(%d,%d)\n", p.x, p.y, q.x, q.y);
return 0;
}
p=(3,4) q=(10,20)
sizeof() a structure may be larger than the sum of its members. Use #pragma pack(1) to disable padding
(but may cause performance issues on some architectures).
Unions - Complete Guide
Unions are similar to structures but all members share the same memory location. Only one member can contain a value at any given time. Unions are useful for saving memory when you need to store different types of data in the same location.
Syntax:
// Union declaration
union union_name {
data_type member1;
data_type member2;
// ... more members
};
// Union variable declaration
union union_name variable_name;
// Accessing members
variable_name.member1 = value;
Key Characteristics:
- Shared Memory: All members share the same memory space
- Size: Size of union = size of its largest member
- One Active Member: Only one member contains valid data at a time
- Memory Efficiency: Uses less memory than equivalent structure
- Type Punning: Can be used to interpret same memory as different types
- Initialization: Can only initialize first member during declaration
Union Examples:
Each example is a short, complete program. Only the member you last wrote to is valid until you write another.
#include <stdio.h>
int main(void) {
union U {
int i;
float f;
} u;
u.i = 42;
printf("as int: %d\n", u.i);
u.f = 3.14f;
printf("as float: %f\n", u.f);
return 0;
}
as int: 42
as float: 3.140000
#include <stdio.h>
struct S {
int i;
float f;
};
union U {
int i;
float f;
};
int main(void) {
printf("sizeof(struct S) = %zu\n", sizeof(struct S));
printf("sizeof(union U) = %zu\n", sizeof(union U));
return 0;
}
sizeof(struct S) = 8
sizeof(union U) = 4
#include <stdio.h>
typedef enum { KIND_INT, KIND_FLOAT } Kind;
typedef struct {
Kind kind;
union {
int i;
float f;
} u;
} Value;
int main(void) {
Value v;
v.kind = KIND_INT;
v.u.i = 7;
if (v.kind == KIND_INT)
printf("%d\n", v.u.i);
v.kind = KIND_FLOAT;
v.u.f = 2.5f;
if (v.kind == KIND_FLOAT)
printf("%f\n", v.u.f);
return 0;
}
7
2.500000
#include <stdio.h>
#include <stdint.h>
int main(void) {
union {
float f;
uint32_t u;
} x;
x.f = 1.0f;
printf("float %f has bits 0x%08X\n", x.f, x.u);
return 0;
}
float 1.000000 has bits 0x3F800000
- Only one union member contains valid data at any time
- Reading from a member other than the last one written to causes undefined behavior
- Always keep track of which member is currently active
- Use tagged unions (struct with type field + union) for safety
- Unions can be used for type punning in C99 (legal), but be careful
- Initialize unions properly - only first member can be initialized in declaration
- Unions containing pointers need special care (alignment issues)
Structure Arrays - Complete Guide
Arrays of structures allow you to store multiple instances of a structure in contiguous memory. This is useful for managing collections of similar data, like student records, employee data, or inventory items.
Syntax:
// Array of structures
struct StructureName array_name[size];
// Accessing elements
array_name[index].member = value;
// Dynamic array of structures
struct StructureName *ptr = malloc(n * sizeof(struct StructureName));
Key Characteristics:
- Contiguous Memory: All structures stored sequentially
- Indexed Access: Use array indexing to access elements
- Memory Layout: Predictable memory pattern
- Efficient Processing: Can process all elements in loops
- Dynamic Arrays: Can allocate at runtime using malloc/calloc
- Multi-dimensional: Can create 2D arrays of structures
Structure Array Examples:
Short programs: static arrays, initialization, dynamic allocation, and a 2×2 grid.
#include <stdio.h>
struct Item {
int id;
int qty;
};
int main(void) {
struct Item cart[3];
for (int i = 0; i < 3; i++) {
cart[i].id = i + 1;
cart[i].qty = (i + 1) * 2;
}
for (int i = 0; i < 3; i++)
printf("item %d: qty %d\n", cart[i].id, cart[i].qty);
return 0;
}
item 1: qty 2
item 2: qty 4
item 3: qty 6
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point pts[] = {{0, 0}, {1, 2}, {3, 4}};
int n = (int)(sizeof pts / sizeof pts[0]);
for (int i = 0; i < n; i++)
printf("(%d,%d)\n", pts[i].x, pts[i].y);
return 0;
}
(0,0)
(1,2)
(3,4)
#include <stdio.h>
#include <stdlib.h>
struct Record {
int key;
double value;
};
int main(void) {
int n = 3;
struct Record *r = malloc((size_t)n * sizeof *r);
if (!r) return 1;
for (int i = 0; i < n; i++) {
r[i].key = i;
r[i].value = i * 1.5;
}
for (int i = 0; i < n; i++)
printf("%d -> %.1f\n", r[i].key, r[i].value);
free(r);
return 0;
}
0 -> 0.0
1 -> 1.5
2 -> 3.0
#include <stdio.h>
struct Score {
char name[20];
int points;
};
int main(void) {
struct Score a[] = {
{"Ann", 80},
{"Ben", 95},
{"Cal", 70}
};
int n = (int)(sizeof a / sizeof a[0]);
int best = 0;
for (int i = 1; i < n; i++)
if (a[i].points > a[best].points)
best = i;
printf("top: %s (%d)\n", a[best].name, a[best].points);
return 0;
}
top: Ben (95)
#include <stdio.h>
struct Cell {
int row;
int col;
};
int main(void) {
struct Cell grid[2][2];
for (int r = 0; r < 2; r++)
for (int c = 0; c < 2; c++) {
grid[r][c].row = r;
grid[r][c].col = c;
}
printf("[%d,%d] [%d,%d]\n",
grid[0][0].row, grid[0][0].col,
grid[1][1].row, grid[1][1].col);
return 0;
}
[0,0] [1,1]
Pointers to Structures - Complete Guide
Pointers to structures allow dynamic allocation of structures, passing structures efficiently to functions (by reference), and creating complex data structures like linked lists and trees.
Syntax:
// Pointer to structure
struct StructureName *ptr;
// Dynamic allocation
ptr = (struct StructureName*)malloc(sizeof(struct StructureName));
// Accessing members
(*ptr).member = value; // Method 1
ptr->member = value; // Method 2 (arrow operator)
Key Concepts:
- Arrow Operator (->): Shortcut for
(*ptr).member - Dynamic Allocation: Create structures at runtime
- Pass by Reference: Efficient function parameter passing
- Linked Structures: Foundation for linked lists, trees
- Memory Management: Must free allocated structures
- Pointer Arithmetic: Works with arrays of structures
Pointer to Structure Examples:
Use -> instead of (*ptr).member. Check for NULL after malloc and free what you allocate.
#include <stdio.h>
struct Box {
int w;
int h;
};
int main(void) {
struct Box b = {4, 5};
struct Box *p = &b;
printf("(*p).w = %d, p->h = %d\n", (*p).w, p->h);
p->w = 10;
printf("b.w = %d\n", b.w);
return 0;
}
(*p).w = 4, p->h = 5
b.w = 10
#include <stdio.h>
#include <stdlib.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point *p = malloc(sizeof *p);
if (!p) return 1;
p->x = 1;
p->y = 2;
printf("%d %d\n", p->x, p->y);
free(p);
return 0;
}
1 2
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int id;
double v;
} Row;
int main(void) {
int n = 3;
Row *r = malloc((size_t)n * sizeof *r);
if (!r) return 1;
for (int i = 0; i < n; i++) {
r[i].id = i;
r[i].v = i * 0.5;
}
Row *q = r;
for (int i = 0; i < n; i++, q++)
printf("%d: %.1f\n", q->id, q->v);
free(r);
return 0;
}
0: 0.0
1: 0.5
2: 1.0
#include <stdio.h>
struct Counter {
int n;
};
void bump(struct Counter *c) {
c->n++;
}
int main(void) {
struct Counter c = {0};
bump(&c);
bump(&c);
printf("n = %d\n", c.n);
return 0;
}
n = 2
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int value;
struct Node *next;
} Node;
int main(void) {
Node *a = malloc(sizeof *a);
Node *b = malloc(sizeof *b);
Node *c = malloc(sizeof *c);
if (!a || !b || !c) return 1;
a->value = 10;
a->next = b;
b->value = 20;
b->next = c;
c->value = 30;
c->next = NULL;
for (Node *p = a; p != NULL; p = p->next)
printf("%d\n", p->value);
free(a);
free(b);
free(c);
return 0;
}
10
20
30
Nested Structures and Typedef
Nested structures allow you to create complex data types by including structures within other structures. The typedef keyword creates type aliases for easier structure usage.
Syntax:
// Nested structure
struct Date {
int day, month, year;
};
struct Employee {
int id;
char name[50];
struct Date dob; // Nested structure
struct Date joinDate;
};
// Typedef for simpler syntax
typedef struct Employee Employee;
// Or combined:
typedef struct {
int id;
char name[50];
} Person;
Key Concepts:
- Nested Structures: Structure containing other structures
- Multi-level Access: Use multiple dot operators:
emp.dob.day - Typedef: Creates type alias for easier declaration
- Self-Referential: Structures containing pointers to same type
- Forward Declaration: Declare structure before defining for mutual references
- Anonymous Structures: Structures without tag names
Nested Structures and Typedef Examples:
Nested members use extra dots: outer.inner.field. typedef gives a shorter type name.
#include <stdio.h>
struct Date {
int day, month, year;
};
struct Person {
int id;
struct Date dob;
};
int main(void) {
struct Person p;
p.id = 1;
p.dob.day = 5;
p.dob.month = 3;
p.dob.year = 2000;
printf("id %d born %d/%d/%d\n",
p.id, p.dob.day, p.dob.month, p.dob.year);
return 0;
}
id 1 born 5/3/2000
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
int main(void) {
Point a = {3, 4};
printf("(%d,%d)\n", a.x, a.y);
return 0;
}
(3,4)
#include <stdio.h>
struct Address {
char city[32];
int zip;
};
struct Employee {
int id;
struct Address home;
};
int main(void) {
struct Employee e = {10, {"Austin", 78701}};
printf("%d lives in %s %d\n", e.id, e.home.city, e.home.zip);
return 0;
}
10 lives in Austin 78701
#include <stdio.h>
typedef struct {
int n;
int scores[3];
} Team;
int main(void) {
Team t = {1, {10, 20, 30}};
int sum = 0;
for (int i = 0; i < 3; i++)
sum += t.scores[i];
printf("team %d total %d\n", t.n, sum);
return 0;
}
team 1 total 60
#include <stdio.h>
#include <string.h>
typedef struct {
int id;
struct {
char first[24];
char last[24];
} name;
} Person;
typedef struct Node {
int v;
struct Node *next;
} Node;
int main(void) {
Person p;
p.id = 7;
strcpy(p.name.first, "Ann");
strcpy(p.name.last, "Lee");
printf("%s %s (id %d)\n", p.name.first, p.name.last, p.id);
Node n3 = {30, NULL};
Node n2 = {20, &n3};
Node n1 = {10, &n2};
for (Node *q = &n1; q != NULL; q = q->next)
printf("%d\n", q->v);
return 0;
}
Ann Lee (id 7)
10
20
30
Bit Fields and Advanced Topics
Bit fields allow you to specify the exact number of bits that a structure member should occupy. This is useful for memory optimization, hardware register access, and packing data efficiently.
Syntax:
struct {
type member_name : width;
// ... more members
};
type: int, signed int, unsigned int (C99 allows _Bool)
width: Number of bits (1 to sizeof(type)*8)
Key Characteristics:
- Memory Optimization: Pack multiple fields into single bytes
- Hardware Access: Map to hardware register bit fields
- Portability Issues: Implementation-dependent (order, padding)
- Limited Types: Mostly integer types
- No Address Operator: Cannot take address of bit field (&)
- Alignment: Compiler may insert padding between bit fields
Bit Field Visualization:
32-bit Integer with Bit Fields
IEEE 754 Single Precision Float (32 bits) using bit fields
Bit Field Examples:
Bit layout is implementation-defined; these snippets show typical uses. Do not take the address of a bit-field.
#include <stdio.h>
struct Flags {
unsigned int on : 1;
unsigned int err : 1;
unsigned int spare : 6;
};
int main(void) {
struct Flags f = {0};
f.on = 1;
printf("on=%u err=%u\n", f.on, f.err);
return 0;
}
on=1 err=0
#include <stdio.h>
struct Reading {
signed int temp_c : 8;
unsigned int humidity : 8;
};
int main(void) {
struct Reading r;
r.temp_c = -5;
r.humidity = 80;
printf("temp %d C, humidity %u\n", r.temp_c, r.humidity);
return 0;
}
temp -5 C, humidity 80
#include <stdio.h>
struct RGB565 {
unsigned int b : 5;
unsigned int g : 6;
unsigned int r : 5;
};
int main(void) {
struct RGB565 c;
c.r = 31;
c.g = 0;
c.b = 0;
printf("R=%u G=%u B=%u\n", c.r, c.g, c.b);
return 0;
}
R=31 G=0 B=0
#include <stdio.h>
#include <stdint.h>
int main(void) {
union {
uint8_t raw;
struct {
uint8_t lo : 4;
uint8_t hi : 4;
} n;
} u;
u.raw = 0;
u.n.lo = 0xF;
u.n.hi = 0x3;
printf("raw = 0x%02X\n", u.raw);
return 0;
}
raw = 0x3F(exact byte may depend on compiler bit-field ordering)
#include <stdio.h>
struct PackedDate {
unsigned int day : 5;
unsigned int month : 4;
unsigned int year : 7;
};
int main(void) {
struct PackedDate d;
d.day = 28;
d.month = 3;
d.year = 26;
printf("%u/%u/20%u\n", d.day, d.month, d.year);
return 0;
}
28/3/2026
- Implementation-defined: Order of bits, padding, alignment are compiler-dependent
- No addresses: Cannot take address of bit field with & operator
- Portability: Code with bit fields may not work across different compilers/platforms
- Limited types: Only int, signed int, unsigned int, _Bool (C99)
- Range checking: No automatic bounds checking - overflow wraps around
- Performance: May be slower due to bit manipulation
- Debugging: Harder to debug than regular variables
- Endianness: Bit order depends on endianness of platform
Common Mistakes and Best Practices
Structure Best Practices:
- Use typedef: Makes code cleaner and easier to read
- Initialize structures: Always initialize, especially dynamic ones
- Order members: Place larger members first to minimize padding
- Document: Comment structure purpose and member meanings
- Use const: When structures shouldn't be modified
- Avoid deep nesting: Too many levels make code hard to read
- Create helper functions: For common operations (init, copy, compare)
- Consider memory layout: For performance-critical code
- Test on different platforms: If using bit fields or specific alignment
- Use standard naming: Consistent naming conventions
Union Best Practices:
- Use tagged unions: Always keep track of active member
- Clear before use: When switching between members, especially strings
- Document active member: Clearly document which member should be accessed
- Use for memory optimization: When you need to store different types at different times
- Avoid for general use: Structures are usually better for grouping data
- Be careful with pointers: Union members with pointers need special handling
- Test thoroughly: Unions can cause subtle bugs
- Consider alternatives: Sometimes void pointers are better
Key Takeaways
- Structures (
struct) group related data of different types with separate memory for each member - Unions (
union) share memory between members - only one member is valid at a time - Use dot operator (.) for structure variables, arrow operator (->) for pointers to structures
- Structure arrays allow storing multiple instances, useful for collections
- Pointers to structures enable dynamic allocation and efficient function parameter passing
- Nested structures create complex data types by including structures within structures
typedefcreates type aliases for cleaner syntax:typedef struct { ... } TypeName;- Bit fields specify exact bit allocation for members, useful for memory optimization
- Structures cannot be compared directly with
==- must compare member by member - Structure padding may increase size - compilers add bytes for alignment
- Use designated initializers for clearer structure initialization
- Self-referential structures contain pointers to same type, used for linked lists and trees
- Unions are useful for variant types, hardware registers, and memory-efficient storage
- Always use tagged unions (struct with type field) to track active member
- Bit fields have portability issues - implementation-defined behavior
- Consider memory layout and alignment for performance-critical applications