Struct Pointers & Arrow Operator
Passing structs efficiently and accessing fields through pointers
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:
free(e.name)releases the heap block, because the copy’snamefield holds the same heap address the caller’s struct does. Freeing through either pointer releases the same memory.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.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.
-> instead of . to access a struct field?->) 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.