C in 100 Seconds: Structs and Pointers | Episode 24
Video: C in 100 Seconds: Structs and Pointers — The Arrow Operator | Episode 24 by Taught by Celeste AI - AI Coding Coach
C Structs and Pointers: The Arrow Operator
Person *p = &alice; p->age++;. The arrow->is sugar for(*p).age— dereference, then access field. The most common operator on struct pointers.
When you have a pointer to a struct (rather than the struct itself), use -> to access fields. Saves the awkward (*p).field and reads naturally.
The basic shape
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[50];
int age;
} Person;
void birthday(Person *p) {
p->age++;
}
int main() {
Person alice = {"Alice", 25};
Person *ptr = &alice;
printf("Name: %s\n", ptr->name); // arrow operator
printf("Age: %d\n", ptr->age);
birthday(&alice);
printf("After birthday: %d\n", alice.age); // 26
Person *bob = malloc(sizeof(Person));
strcpy(bob->name, "Bob");
bob->age = 30;
printf("\nHeap: %s, %d\n", bob->name, bob->age);
free(bob);
return 0;
}
Three equivalent forms
Person alice = {"Alice", 25};
Person *p = &alice;
p->age = 26; // arrow form (idiomatic)
(*p).age = 26; // explicit dereference + dot
alice.age = 26; // direct (when you have the struct itself)
All three set alice.age to 26. The arrow form is shorter and reads more naturally — "p's age."
The arrow operator was added to C specifically because (*p).field is so awkward.
Why pass struct pointers
Three reasons:
1. Avoid copying large structs.
typedef struct {
char data[1000];
int meta[100];
} Big;
void process_value(Big b) { ... } // copies 1400+ bytes
void process_ptr(Big *b) { ... } // copies 8 bytes (pointer)
For anything bigger than a register (~16 bytes), pointers are faster.
2. Modify the caller's struct.
void birthday(Person *p) {
p->age++;
}
birthday(&alice); // alice.age changes
Same as episode 19's swap example — you need a pointer to modify.
3. Heap-allocated structs.
Person *bob = malloc(sizeof(Person));
bob->age = 30;
free(bob);
malloc always returns a pointer. Use -> to access fields.
With const for read-only
void print_person(const Person *p) {
printf("%s, %d\n", p->name, p->age);
// p->age = 99; // ERROR — const prevents modification
}
const Person *p says "I won't modify the struct through this pointer." Documents intent and lets the compiler enforce it.
Linked structures
typedef struct Node {
int value;
struct Node *next;
} Node;
Node *head = malloc(sizeof(Node));
head->value = 1;
head->next = malloc(sizeof(Node));
head->next->value = 2;
head->next->next = NULL;
-> chains naturally: head->next->value. Linked lists, trees, graphs all use this idiom heavily. Episodes 42-50 cover them.
Heap struct allocation
Person *bob = malloc(sizeof(Person));
if (bob == NULL) { /* handle */ }
strcpy(bob->name, "Bob");
bob->age = 30;
// ... use bob ...
free(bob);
Standard pattern:
malloc(sizeof(Person))— allocate a struct's worth of bytes.- Initialize fields via
->. - Use.
free(bob).
Always check the malloc return; always pair with a free.
sizeof on a struct vs pointer
Person alice;
Person *p = &alice;
sizeof(alice) // size of the struct (depends on fields)
sizeof(p) // 8 (pointer size on 64-bit)
sizeof(*p) // size of the struct (dereference type)
When mallocing for a struct via a pointer, this idiom works:
Person *p = malloc(sizeof(*p)); // size of what p points to
If you change Person to a different type, this still works without modification. Cleaner than malloc(sizeof(Person)).
Returning struct by pointer
Person *create_person(const char *name, int age) {
Person *p = malloc(sizeof(*p));
if (p == NULL) return NULL;
strncpy(p->name, name, sizeof(p->name) - 1);
p->name[sizeof(p->name) - 1] = '\0';
p->age = age;
return p;
}
Person *alice = create_person("Alice", 25);
// ... use ...
free(alice);
Caller is responsible for freeing. Document this in your API or use a destructor:
void destroy_person(Person *p) {
if (p == NULL) return;
free(p);
}
For more complex structs (with internal allocations), the destructor frees them too:
typedef struct {
char *bio; // heap-allocated string
int age;
} Person;
void destroy_person(Person *p) {
if (p == NULL) return;
free(p->bio); // free the inner allocation first
free(p);
}
Order matters: free contents before the container.
Arrays of struct pointers
Person *people[3];
for (int i = 0; i < 3; i++) {
people[i] = malloc(sizeof(*people[i]));
}
people[0]->age = 25;
people[1]->name[0] = 'B';
people[i]->age — index, then arrow. Common in heterogeneous collections (e.g., a list of objects with different lifetimes).
For simpler cases, an array of struct values is cleaner:
Person people[3]; // all in one block; no malloc per element
people[0].age = 25;
Common mistakes
p.field instead of p->field. p is a pointer; . doesn't apply directly. Compiler warns/errors.
*p.field thinking it's (*p).field. Operator precedence: . binds tighter than *, so *p.field is *(p.field) — dereferences a field, not the pointer. Use (*p).field or p->field.
Forgetting to free struct contents. If a struct has malloc'd fields, free(p) only releases the struct's bytes — internal allocations leak.
Passing struct value when pointer would do. Big structs get expensive to copy.
Dereferencing NULL struct pointer. Person *p = NULL; p->age = 5; segfaults.
Modifying through a const pointer. Doesn't compile — good. (Casting away const = undefined behavior.)
What's next
Episode 25: unions. Multiple fields sharing the same memory. Used for variant types, low-level binary parsing.
Recap
p->field is sugar for (*p).field — dereference and access. Use -> whenever you have a struct pointer. Pass big structs by pointer (cheap; allows modification). Use const T *p for read-only access. malloc(sizeof(*p)) is the cleaner allocation idiom. Linked structures chain naturally: node->next->value. Always free struct contents before the struct itself.
Next episode: unions.