C Structs: The Complete Guide to Structures in C Programming
Table of Contents
What Are Structs?
Until now, every variable you have created held a single value — one integer, one character, one pointer. But real data is rarely so simple. A student has a name, an ID, a GPA, and an enrollment date. A network packet has a source address, destination address, port, and payload. C structs let you bundle related variables into a single, named type.
A struct is C’s way of creating custom data types. It groups different variables (called members or fields) under one name, even if those variables have different types. Structs are the foundation of organized programming in C — virtually every non-trivial C program uses them.
Declaration & Initialization
#include <stdio.h>
#include <string.h>
// Declare a struct type
struct Student {
char name[50];
int id;
float gpa;
int year;
};
int main(void) {
// Method 1: Declare and assign individually
struct Student s1;
strcpy(s1.name, "Alice");
s1.id = 1001;
s1.gpa = 3.85f;
s1.year = 2;
// Method 2: Initializer list (in order)
struct Student s2 = {"Bob", 1002, 3.92f, 3};
// Method 3: Designated initializers (C99+, any order)
struct Student s3 = {
.name = "Charlie",
.gpa = 3.75f,
.id = 1003,
.year = 1
};
// Method 4: Partial initialization (rest is zero)
struct Student s4 = {.name = "Diana"};
// s4.id = 0, s4.gpa = 0.0, s4.year = 0
printf("%s (ID: %d) — GPA: %.2f\n", s1.name, s1.id, s1.gpa);
printf("%s (ID: %d) — GPA: %.2f\n", s2.name, s2.id, s2.gpa);
printf("%s (ID: %d) — GPA: %.2f\n", s3.name, s3.id, s3.gpa);
return 0;
}
Designated initializers (Method 3) are the recommended approach. They make code self-documenting and order-independent — you can rearrange struct members without breaking initializations.
Accessing Members
The dot operator (.) accesses struct members:
struct Point {
int x;
int y;
};
struct Point p = {10, 20};
// Read members
printf("x = %d, y = %d\n", p.x, p.y);
// Write members
p.x = 30;
p.y = 40;
// Use in expressions
int distance_sq = p.x * p.x + p.y * p.y;
Struct Assignment
Structs support direct assignment — it copies all members:
struct Point a = {10, 20};
struct Point b;
b = a; // Copies ALL members from a to b
printf("b: (%d, %d)\n", b.x, b.y); // Output: b: (10, 20)
// Comparison: structs do NOT support == operator
// if (a == b) {} // ERROR: won't compile
// You must compare member by member:
if (a.x == b.x && a.y == b.y) {
printf("Equal!\n");
}
typedef with Structs
typedef creates an alias so you do not need to write struct every time:
// Without typedef
struct Vector3 {
float x, y, z;
};
struct Vector3 position; // Must write "struct Vector3"
// With typedef
typedef struct {
float x, y, z;
} Vec3;
Vec3 position; // Clean, no "struct" keyword
// Named struct with typedef (best of both worlds)
typedef struct Node {
int data;
struct Node *next; // Self-reference needs the struct name
} Node;
The last pattern is essential for self-referencing structs like linked list nodes. You need the struct Node name inside the definition because Node (the typedef) does not exist yet at that point.
Pointers to Structs
Struct pointers are fundamental in C — they enable linked data structures, efficient function calls, and dynamic allocation of structs:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[50];
int age;
float salary;
} Employee;
int main(void) {
// Stack struct with pointer
Employee emp = {"Alice", 30, 75000.0f};
Employee *ptr = &emp;
// Arrow operator (->) for pointer access
printf("Name: %s\n", ptr->name); // Same as (*ptr).name
printf("Age: %d\n", ptr->age);
printf("Salary: %.0f\n", ptr->salary);
// Modify through pointer
ptr->age = 31;
ptr->salary = 80000.0f;
// Heap-allocated struct
Employee *new_emp = malloc(sizeof(Employee));
if (!new_emp) return 1;
strcpy(new_emp->name, "Bob");
new_emp->age = 25;
new_emp->salary = 65000.0f;
printf("\n%s, Age %d\n", new_emp->name, new_emp->age);
free(new_emp);
return 0;
}
The arrow operator (->) is syntactic sugar for (*ptr).member. It is one of the most used operators in C. Whenever you have a pointer to a struct, use ->. Whenever you have the struct itself, use ..
Structs and Functions
Pass by Value vs Pass by Pointer
#include <stdio.h>
typedef struct {
double x, y;
} Point;
// Pass by value — makes a COPY (inefficient for large structs)
double distance_value(Point a, Point b) {
double dx = a.x - b.x;
double dy = a.y - b.y;
return dx * dx + dy * dy;
}
// Pass by pointer — no copy, efficient
// Use const to promise you won't modify
double distance_ptr(const Point *a, const Point *b) {
double dx = a->x - b->x;
double dy = a->y - b->y;
return dx * dx + dy * dy;
}
// Modify struct through pointer
void translate(Point *p, double dx, double dy) {
p->x += dx;
p->y += dy;
}
// Return struct by value
Point create_point(double x, double y) {
Point p = {x, y};
return p; // Returns a copy — safe
}
int main(void) {
Point a = {0.0, 0.0};
Point b = {3.0, 4.0};
printf("Distance²: %.1f\n", distance_ptr(&a, &b));
translate(&a, 1.0, 1.0);
printf("a moved to: (%.1f, %.1f)\n", a.x, a.y);
Point c = create_point(5.0, 6.0);
printf("c: (%.1f, %.1f)\n", c.x, c.y);
return 0;
}
Rule of thumb: Pass small structs (up to 16–32 bytes) by value. Pass larger structs by const pointer. If the function needs to modify the struct, always pass by pointer.
Nested Structs
#include <stdio.h>
typedef struct {
int day, month, year;
} Date;
typedef struct {
char street[100];
char city[50];
char state[3];
int zip;
} Address;
typedef struct {
char name[50];
Date birth_date;
Address home;
Address work;
} Person;
int main(void) {
Person p = {
.name = "Alice Johnson",
.birth_date = {15, 3, 1990},
.home = {
.street = "123 Oak Street",
.city = "Portland",
.state = "OR",
.zip = 97201
},
.work = {
.street = "456 Tech Blvd",
.city = "Portland",
.state = "OR",
.zip = 97204
}
};
printf("%s, born %d/%d/%d\n",
p.name, p.birth_date.month, p.birth_date.day, p.birth_date.year);
printf("Home: %s, %s, %s %d\n",
p.home.street, p.home.city, p.home.state, p.home.zip);
return 0;
}
Arrays of Structs
#include <stdio.h>
#include <string.h>
typedef struct {
char title[100];
char author[50];
int year;
float rating;
} Book;
int main(void) {
Book library[] = {
{"The C Programming Language", "Kernighan & Ritchie", 1978, 4.8f},
{"Expert C Programming", "Peter van der Linden", 1994, 4.5f},
{"C Interfaces and Impl", "David Hanson", 1996, 4.3f},
};
int count = sizeof(library) / sizeof(library[0]);
// Find highest rated
int best = 0;
for (int i = 1; i < count; i++) {
if (library[i].rating > library[best].rating) {
best = i;
}
}
printf("Best book: \"%s\" by %s (%.1f stars)\n",
library[best].title, library[best].author, library[best].rating);
return 0;
}
Struct Memory Layout & Padding
Compilers insert padding bytes between struct members to align them for efficient CPU access. This means the size of a struct is often larger than the sum of its members:
#include <stdio.h>
// Poorly ordered — wastes space
struct Bad {
char a; // 1 byte + 3 padding
int b; // 4 bytes
char c; // 1 byte + 3 padding
}; // Total: 12 bytes
// Well ordered — no wasted space
struct Good {
int b; // 4 bytes
char a; // 1 byte
char c; // 1 byte + 2 padding
}; // Total: 8 bytes
int main(void) {
printf("sizeof(Bad) = %zu\n", sizeof(struct Bad)); // 12
printf("sizeof(Good) = %zu\n", sizeof(struct Good)); // 8
return 0;
}
Rule: Order struct members from largest to smallest alignment. This minimizes padding and saves memory, especially in arrays of thousands of structs.
Real-World Patterns
Constructor / Destructor Pattern
#include <stdlib.h>
#include <string.h>
typedef struct {
char *name;
int *scores;
int score_count;
} Student;
// Constructor
Student *student_create(const char *name, int max_scores) {
Student *s = malloc(sizeof(Student));
if (!s) return NULL;
s->name = malloc(strlen(name) + 1);
if (!s->name) { free(s); return NULL; }
strcpy(s->name, name);
s->scores = calloc(max_scores, sizeof(int));
if (!s->scores) { free(s->name); free(s); return NULL; }
s->score_count = 0;
return s;
}
// Destructor
void student_destroy(Student *s) {
if (s) {
free(s->name);
free(s->scores);
free(s);
}
}
Opaque Pointer Pattern
// In header file (database.h) — struct definition hidden
typedef struct Database Database;
Database *db_open(const char *path);
int db_query(Database *db, const char *sql);
void db_close(Database *db);
// In source file (database.c) — actual definition
struct Database {
FILE *file;
char *path;
int transaction_count;
// Implementation details hidden from users
};
This pattern hides implementation details. Users only see the pointer — they cannot access members directly, which gives you freedom to change the struct layout without breaking code that uses it.
Practice Exercises
Exercise 1: Student Database
Create a program that manages an array of Student structs (name, ID, grades array). Include functions to add students, calculate averages, find the top student, and print all records sorted by GPA.
Exercise 2: Linked List with Structs
Implement a singly linked list using a Node struct with int data and Node *next. Include functions for insert, delete, search, print, and reverse. Free all nodes when done.
Exercise 3: JSON-like Config Parser
Create a struct-based configuration system. Define a Config struct with fields like host, port, max_connections, log_level. Write functions to load defaults, parse from a file (key=value format), and print the current configuration.
Structs transform C from a language of loose variables into one of organized, meaningful data types. They are the closest C gets to object-oriented programming. Next, we will explore their flexible cousins — unions and enums — which give you even more control over how data is stored and categorized.