Part of C in 100s

C in 100 Seconds: Structs and Pointers | Episode 24

Celest KimCelest Kim

Video: C in 100 Seconds: Structs and Pointers — The Arrow Operator | Episode 24 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

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:

  1. malloc(sizeof(Person)) — allocate a struct's worth of bytes.
  2. Initialize fields via ->.
  3. Use.
  4. 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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.