advanced-c Lesson 2 20 min read

How Do I Use Pointers to Access Struct Members?

The -> operator, dynamic struct allocation, and nested memory management

Reading: C Text: Ch. 10 §3/§5 (pp. 620–649, Structs as Function Arguments)

After this lesson, you will be able to:

  • Use the arrow operator (->) to access fields through a struct pointer
  • Dynamically allocate structs with calloc and avoid the sizeof pointer bug
  • Pass struct pointers to functions for efficient in-place modification
  • Implement nested allocation and free inner allocations before outer

Dot vs. Arrow

You’ve seen s.name for accessing struct fields. But when you have a pointer to a struct, you need the arrow operator (->):

Student s;
s.gpa = 3.85;          // Dot: s is a struct value

Student *p = &s;
p->gpa = 3.85;         // Arrow: p is a pointer to a struct

p->gpa is shorthand for (*p).gpa — dereference the pointer, then access the field. The arrow saves you from writing parentheses everywhere.

Check Your Understanding
You have Student *p = &s;. Which correctly accesses the student's GPA?
A p.gpa — dot works on both values and pointers
B p->gpa — arrow is required because p is a pointer
C *p.gpa — dereference first, then access the field
D &p.gpa — use address-of to access through a pointer
Answer: B. When you have a pointer to a struct, use -> to access fields. p->gpa is shorthand for (*p).gpa. Option A fails because . expects a struct value, not a pointer. Option C is a precedence trap: *p.gpa parses as *(p.gpa) due to . binding tighter than *, which doesn't compile since p isn't a struct.

Struct Pointers in Practice

The Rule

Have Use Example
Struct value . s.name
Struct pointer -> p->name

Dynamic Struct Allocation

Student *s = calloc(1, sizeof(Student));
if (s == NULL) { /* handle error */ }

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

// When done:
free(s);

Common Pitfall: Use sizeof(Student) — not sizeof(Student *). sizeof(Student *) is the size of a pointer (8 bytes), not the struct. This allocates far too little memory and causes buffer overflows.

Check Your Understanding
Given Student *p = calloc(1, sizeof(Student));, which line correctly sets the student's GPA to 3.5?
A p.gpa = 3.5;
B p->gpa = 3.5;
C *p.gpa = 3.5;
D &p->gpa = 3.5;
Answer: B. Since p is a pointer to a struct, you use the arrow operator ->. Option A uses dot notation, which only works on struct values, not pointers. Option C is a precedence trap — *p.gpa is parsed as *(p.gpa), which doesn't compile. Option D takes the address of the field, which doesn't let you assign to it.

Dynamic Arrays of Structs

int n = 30;
Student *roster = calloc(n, sizeof(Student));

for (int i = 0; i < n; i++)
{
    roster[i].id = 1000 + i;
    roster[i].gpa = 0.0;
    strcpy(roster[i].name, "TBD");
}

// Access through pointer arithmetic:
// roster[i] is the same as *(roster + i)
// roster[i].name is the same as (roster + i)->name

free(roster);

Passing Struct Pointers to Functions

Pass by pointer to avoid copying and to allow modification:

void update_gpa(Student *s, double new_gpa)
{
    s->gpa = new_gpa;     // Modifies the original
}

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

Use const when the function only reads:

void print_roster(const Student *roster, int size)
{
    for (int i = 0; i < size; i++)
    {
        print_student(&roster[i]);
    }
}

Structs with Pointer Fields (Nested Allocation)

When a struct has char * instead of char[50], you need two allocations:

typedef struct
{
    char *name;       // Pointer to dynamically allocated string
    int id;
    double gpa;
} Student;

Student *s = calloc(1, sizeof(Student));
s->name = calloc(50, sizeof(char));        // Separate allocation
strcpy(s->name, "Alice");

And two frees — in the right order:

free(s->name);    // Free the string FIRST
free(s);          // Then free the struct

Common Pitfall: If you free the struct before the string, you’ve lost the pointer to the string — memory leak. Always free inner allocations before outer ones.

The Trick: For labs, use fixed-size char name[50] inside structs when possible. It avoids nested allocation and simplifies cleanup. Save char * for when you genuinely need variable-length strings.

From Java: In Java, nested objects are handled by the garbage collector — you never worry about freeing inner fields before outer objects. In C, you’re the garbage collector. Free from the inside out.

Check Your Understanding
A struct has char *name (a pointer field). You allocate with Student *s = calloc(1, sizeof(Student)); s->name = calloc(50, sizeof(char));. What is the correct order to free?
A free(s); free(s->name);
B free(s); — one call frees everything
C free(s->name); free(s->name); free(s);
D free(s->name); free(s);
Answer: D. Free inner allocations first, then outer. Option A frees the struct first, losing access to s->name — that's a memory leak and undefined behavior. Option B only frees the struct; free doesn't recursively free pointer fields. Option C double-frees s->name, which is undefined behavior.
Why does this matter?

Nested allocation shows up constantly in real C code — any struct with dynamically allocated strings, arrays, or sub-structs requires inside-out freeing. In Lab 8, every Word struct has a separately allocated string. Getting the free order wrong means either memory leaks (Valgrind will catch these) or crashes from use-after-free.

Check Your Understanding
You write Student *s = calloc(1, sizeof(Student *)); for a struct that is 64 bytes. What happens?
A It works correctly — sizeof(Student *) is equivalent to sizeof(Student)
B Compile error — you can't use sizeof on a pointer type
C You allocate only 8 bytes (size of a pointer) instead of 64, causing buffer overflows when you write to struct fields
D The program crashes immediately at the calloc call
Answer: C. sizeof(Student *) is the size of a pointer (8 bytes on 64-bit systems), not the struct itself. You only get 8 bytes, but then try to write 64 bytes of struct data into that space — a buffer overflow that corrupts memory. The calloc call itself succeeds (8 bytes is a valid request), so there's no immediate crash, just silent corruption.
Quick Check: When do you use . vs ->?

Use . when you have the struct value itself (Student s; s.name). Use -> when you have a pointer to the struct (Student *p; p->name). p->name is shorthand for (*p).name.

Quick Check: Why must you free inner allocations before outer ones?

If you free the outer struct first, you lose access to the pointer fields inside it. The inner allocations become unreachable — a memory leak. Free from inside out: free(s->name) then free(s).

Quick Check: What's wrong with calloc(1, sizeof(Student *))?

sizeof(Student *) is the size of a pointer (8 bytes), not the struct (which might be 64+ bytes). This allocates far too little memory. Use sizeof(Student) to allocate enough for the entire struct.


Big Picture: Struct pointers bridge the gap between static data and dynamic programs. Without them, every struct would live on the stack with a fixed lifetime. With them, you can allocate records on demand, build arrays that grow at runtime, and pass data efficiently to functions. This is the machinery behind every database, linked list, and file parser you’ll write.


Structs + Pointers = Real Data Structures

With struct pointers, you can dynamically allocate individual records, build arrays of records, and pass them efficiently to functions. This is the foundation for file I/O (reading records from disk), sorting (rearranging struct arrays), and linked lists (self-referential structs).

Next: file I/O. You’ll learn fopen, fclose, fprintf, and fscanf — reading data from files into struct arrays, which is how most real programs get their data.