C in 100 Seconds: Structs — Group Your Data | Episode 23
Video: C in 100 Seconds: Structs | Episode 23 by Taught by Celeste AI - AI Coding Coach
C Structs
typedef struct { char name[50]; int age; } Student;groups related data into a named type. Access fields with.. C's version of records — no methods, just data.
Structs let you bundle several values under one name. They're how every non-trivial C program organises data — without them you'd have parallel arrays everywhere.
The basic shape
#include <stdio.h>
typedef struct {
char name[50];
int age;
float gpa;
} Student;
int main() {
Student s = {"Alice", 20, 3.85};
printf("Name: %s\n", s.name);
printf("Age: %d\n", s.age);
printf("GPA: %.2f\n", s.gpa);
s.age = 21;
printf("\nUpdated age: %d\n", s.age);
return 0;
}
struct vs typedef struct
C has two ways to declare:
// Plain struct — must use "struct" prefix when referring
struct Student {
char name[50];
int age;
};
struct Student s = {"Alice", 20};
// typedef struct — defines a new type name
typedef struct {
char name[50];
int age;
} Student;
Student s = {"Alice", 20}; // no "struct" prefix needed
The typedef form lets you write Student instead of struct Student. Cleaner for everyday use; idiomatic in modern C.
The named-and-typedef'd form lets you both:
typedef struct Student { // both name AND typedef
char name[50];
int age;
struct Student *next; // can refer to itself by struct name
} Student;
Useful for self-referential structs (linked lists, trees) — the body needs to reference struct Student before the typedef is complete.
Initialization
Student s = {"Alice", 20, 3.85}; // positional
Student s = { .name = "Alice", .age = 20, .gpa = 3.85 }; // designated (C99+)
Student s = {0}; // all fields zero
Student s; // uninitialized — fields are garbage
Designated initializers (with .field = value) are clearer for many fields. Positional initializers work but are order-dependent.
{0} zero-initializes the whole struct — common idiom.
Accessing fields with .
s.name // string field
s.age // int field
s.gpa // float field
s.age = 21; // assign
Standard dot-notation. s is the struct; name is the field. Same as Python, JavaScript, Java for instance fields.
Sizes and alignment
typedef struct {
char c;
int i;
char c2;
} Mixed;
printf("%lu\n", sizeof(Mixed)); // 12 on most platforms, not 6
The struct is padded — int typically aligns to 4-byte boundaries, so the compiler inserts padding bytes. Layout:
Offset 0: char c (1 byte)
Offset 1-3: padding (3 bytes)
Offset 4-7: int i (4 bytes)
Offset 8: char c2 (1 byte)
Offset 9-11: padding (3 bytes for the struct's alignment)
Total: 12 bytes
Reorder for tightness:
typedef struct {
int i;
char c;
char c2;
// 2 bytes padding at end
} Tighter; // 8 bytes
For high-density storage, sort fields biggest-to-smallest. Most code doesn't worry about it.
Passing structs
void print_student(Student s) { // pass by value — COPIES
printf("%s\n", s.name);
}
void birthday(Student *s) { // pass by pointer — references
s->age++; // -> for "dereference and access field"
}
print_student(alice); // copies the whole struct
birthday(&alice); // passes pointer
Pass small structs by value is fine. For large structs, pass by pointer to avoid copying.
s->age is sugar for (*s).age — dereference then access. Episode 24 dives into this.
Returning structs
Student create_student(const char *name, int age) {
Student s = {0};
strcpy(s.name, name);
s.age = age;
return s;
}
Student alice = create_student("Alice", 20);
Returning structs by value works (copies). Modern compilers often optimize this to "return value optimization" — no actual copy. But if the struct is huge, prefer:
- Pass an output pointer:
void create(Student *out, ...). - Return a pointer to heap memory:
Student *p = malloc(sizeof(Student));.
Arrays of structs
Student class[3] = {
{"Alice", 20, 3.85},
{"Bob", 22, 3.50},
{"Charlie", 19, 3.95},
};
for (int i = 0; i < 3; i++) {
printf("%s: %.2f\n", class[i].name, class[i].gpa);
}
class[i] is the i-th student; .name accesses fields.
Nested structs
typedef struct {
int x;
int y;
} Point;
typedef struct {
Point top_left;
Point bottom_right;
int color;
} Rectangle;
Rectangle r = {{0, 0}, {10, 5}, 0xFF0000};
printf("%d, %d\n", r.top_left.x, r.bottom_right.y);
Structs containing structs. Field access chains: r.top_left.x.
Comparing structs
Student a = {"Alice", 20, 3.85};
Student b = {"Alice", 20, 3.85};
if (a == b) { ... } // ERROR — can't compare structs with ==
if (memcmp(&a, &b, sizeof(Student)) == 0) { ... } // bytewise compare
C doesn't have built-in struct equality. Use memcmp for bytewise compare — but watch out for padding bytes (which contain garbage). For reliable equality, write a per-field comparison.
Common mistakes
Forgetting typedef. struct Student s works; Student s doesn't, unless you typedef.
Returning pointer to local struct. Student* foo() { Student s; return &s; } — s is destroyed when foo returns.
Comparing with ==. Doesn't compile (or worse, compares pointers if you forgot *). Use memcmp carefully or per-field.
Padding surprises. sizeof(Mixed) isn't always sum of fields. Reorder for compactness.
Mutating through copy. void func(Student s) { s.age = 99; } modifies the copy, not the caller's struct.
memset on a struct with pointers. memset(&s, 0, sizeof(s)); zeros the bytes — pointers become NULL, ints become 0, but if any field needed special init (like a malloc), that's lost.
What's next
Episode 24: structs and pointers — the arrow operator. Why s->field and (*s).field are the same.
Recap
typedef struct { fields } TypeName; for a record type. Initialise with positional or designated form ({ .field = value }). Access fields with .; through pointer with ->. Padding is automatic — sizeof(struct) may exceed sum of fields. Pass small structs by value, large by pointer. C doesn't compare structs with == — use memcmp or per-field. Self-referential needs the named-typedef form.
Next episode: structs and pointers.