student@ubuntu:~$
advanced-c Lesson 11 9 min read

Struct Pointers & Arrow Operator

Passing structs efficiently and accessing fields through pointers

Reading: Hanly & Koffman: §10.3 (pp. 584–587), §10.5 (pp. 596–606)

Quick check before you start: Can you define a struct and access its fields with the dot operator? If yes, read on. If not, review Lesson 8a.

Practice this topic: Struct Pointers skill drill

After this lesson, you will be able to:

  • Use the -> operator to access fields through a struct pointer
  • Allocate structs dynamically on the heap
  • Handle nested dynamic allocations
  • Free nested structs in the correct order
  • Predict which struct mutations persist when passing by value vs by pointer

The Arrow Operator

When you have a pointer to a struct, you cannot use the dot operator directly. You need ->:

Student *ptr = &s1;
printf("GPA: %.2f\n", ptr->gpa);

The arrow operator dereferences the pointer and accesses the field in one step. It is exactly equivalent to (*ptr).gpa, but cleaner.

Rule of thumb: variable uses ., pointer uses ->.

Dynamic Struct Allocation

Structs on the stack disappear when the function returns. To keep a struct alive, allocate it on the heap:

Student *s = calloc(1, sizeof(Student));
if (s == NULL) {
    fprintf(stderr, "Error: calloc failed\n");
    exit(EXIT_FAILURE);
}

strcpy(s->name, "Alice");
s->id = 12345;
s->gpa = 3.7;

Now s points to heap memory that persists until you free it.

Nested Dynamic Allocation

Sometimes a struct field itself needs heap memory. For example, a student with a dynamically sized name:

typedef struct {
    char *name;    // pointer, not array
    int id;
    double gpa;
} Student;

Student *create_student(const char *name, int id, double gpa) {
    Student *s = calloc(1, sizeof(Student));
    if (s == NULL) { return NULL; }

    s->name = calloc(strlen(name) + 1, sizeof(char));
    if (s->name == NULL) {
        free(s);
        return NULL;
    }

    strcpy(s->name, name);
    s->id = id;
    s->gpa = gpa;
    return s;
}

Two allocations: one for the struct, one for the name string inside it.

Freeing Order Matters

When you free a struct with nested allocations, free the inner allocations first:

void free_student(Student *s) {
    if (s == NULL) { return; }
    free(s->name);   // free inner first
    s->name = NULL;
    free(s);          // then free the struct
}

If you free the struct first, you lose your only pointer to s->name — that is a memory leak. Think of it like unpacking a box: take out the contents before you throw away the box.

Passing Structs to Functions

Pass struct pointers to avoid copying the entire struct:

void print_student(const Student *s) {
    printf("%s (ID: %d, GPA: %.2f)\n", s->name, s->id, s->gpa);
}

The const keyword tells the compiler (and the reader) that this function will not modify the struct. Use it whenever a function only reads the data.

Pass-by-value: what travels and what stays

The language also lets you declare a parameter as the struct itself instead of a pointer:

void clean(Student e) {
    free(e.name);
    e.name = NULL;
    e.id = 0;
}

e here is a copy of the caller’s struct, made when the function was called. Three things follow from that:

  1. free(e.name) releases the heap block, because the copy’s name field holds the same heap address the caller’s struct does. Freeing through either pointer releases the same memory.
  2. e.name = NULL; updates only the copy, not the caller’s struct. After the function returns, the caller’s struct still holds the (now-dangling) original address.
  3. e.id = 0; has no effect on the caller for the same reason. The integer field lives in the copy’s slot, not the caller’s.

Pass-by-value gives the function a private copy of everything except whatever lives behind a pointer. If a lab spec says “clean the entity” and the cleanup function takes the entity by value, free() does real work but field reassignment does not. To actually modify the caller’s struct, take a Student * and use -> instead.

Why this idiom exists at all: taking a pointer is usually better, but some library and callback designs (like a void (*cleaner)(Entity) field in a generic container) lock you into pass-by-value because the function-pointer type they declared took the struct by value. Then you write the cleanup the way the contract demands and accept that the assignment-after-free is a no-op on the caller’s side.


Check Your Understanding
When should you use -> instead of . to access a struct field?
AWhen the struct is on the stack
BWhen the field is an array
CWhen you have a pointer to the struct
DWhen the struct uses typedef
Answer: C. The arrow operator (->) is used when you have a pointer to a struct. It dereferences the pointer and accesses the field in one step. If you have the struct itself (not a pointer), use the dot operator.

What Comes Next

You can create and manage structs in memory. Next, you will learn to read and write files in C — the bridge between your program and persistent data on disk.

Next: File I/O in C