Topic 80 of 100 Structures and Unions in C Interactive Tutorial

Pointers to Structures in C: Accessing Members Efficiently

12 mins read Last updated: 22 Aug 2026 26,900 views
C Compiler 0.00%

What is Pointers to Structures in C? Core Concept & Direct Summary

Core Objective & Problem Statement

How do structure pointers and the arrow operator `->` provide zero-copy access to composite records?

Direct Answer Summary

You've learned two things that pair extremely well: structures group related data into a single type, and pointers let you work with data indirectly and cheaply. Put them together and you get the dominant pattern of professional C — nearly all real C code manipulates structs through pointers.

Why? Three reasons:

  1. Efficiency. A struct might be hundreds of bytes. Passing it by value copies all of it on every call; passing a pointer copies 8 bytes.
  2. Modification. Functions need to change a struct's members, and C is pass-by-value. Pointers are the only way.
  3. Dynamic data. Heap-allocated structs (via malloc) are always accessed through pointers. There's no other option.

This page covers the mechanics — the -> operator, member access through addresses, passing struct pointers around, allocating structs dynamically — and the efficiency reasoning that makes pointers the default choice.

(Suggested image: the hero graphic — a struct box with three labeled members; beside it a pointer box holding the struct's address, with an arrow into the struct and a small -> symbol on the arrow)


How Does Pointers to Structures in C Work? In-Depth Mechanics & Architecture

.

Declaring a Pointer to a Structure

main.c
Run Online
typedef struct {
    int x;
    int y;
}
Point;
Point p = {
    10, 20
}
;
Point *ptr = &p;
// pointer to a Point

Nothing new here — &p gives the struct's address, and Point * is the type that holds it.

Without typedef (in case you're working with older code):

main.c
Run Online
struct Point {
    int x, y;
}
;
struct Point p = {
    10, 20
}
;
struct Point *ptr = &p;
// note: "struct Point *" — both words needed

The struct keyword is required throughout unless you typedef'd the type. That verbosity is one of the main reasons typedef is standard practice.


The Arrow Operator: ->

Here's the syntax difference that catches everyone. Two ways to access a member through a pointer:

main.c
Run Online
Point p = {
    10, 20
}
;
Point *ptr = &p;
(*ptr).x = 30;
// ✓ correct — dereference first, then use dot*ptr.x = 30;
// ✗ ERROR — parses as *(ptr.x)

Why the first one needs parentheses: the . operator binds tighter than *, so *ptr.x is read as *(ptr.x) — "dereference ptr.x," which is nonsense because ptr isn't a struct (it's a pointer). Hence the parentheses: dereference ptr, then select the member.

Because (*ptr).x is ugly and easy to get wrong, C provides the arrow operator:

main.c
Run Online
ptr->x = 30;
// ✓ exactly equivalent to (*ptr).xprintf("%d\n", ptr->y);
// 20

Read -> as "points to."ptr->x means "the x member of the struct that ptr points to." Once you read it that way, the whole thing becomes natural.

(Suggested image: the same access shown three ways — p.x, (*ptr).x, ptr->x — with a note that the last two are identical and the arrow exists for readability)


The Access Rules, Side by Side

What you haveHow to access a member
A struct variable: pp.member
A struct pointer: ptrptr->member
A struct pointer, explicitly(*ptr).member — identical to the above
An array of structs: arrarr[i].member
A pointer to an array of structsptr[i].member or (ptr + i)->member

The rule in one line:value gets a dot, pointer gets an arrow. (And (*ptr).member always works too, but nobody writes it once they know about ->.)

Where this bites: structs nest, so the operator changes as you descend:

main.c
Run Online
typedef struct {
    char name[30];
    Point position;
    // a struct member} Sprite;
    Sprite s = {
        .name = "hero", .position = {
            5, 7
        }
    };
    Sprite *sp = &s;
    s.position.x = 5;
    // value → value → value: all dotssp->position.x     = 5;       // pointer → then all dotssp->position       = (Point){1, 2};   // compound literal, FYI

Notice the pattern: only the first hop uses -> if you start from a pointer; everything after is a value, so dots. Chains like a->b->c only occur when members are themselves pointers (that's linked-list territory).

(Suggested image: a nested struct shown with arrows marking which hops use -> and which use . — this exact confusion is where most bugs come from)


Modifying a Struct Through a Pointer

Since a pointer holds the address, writes through it change the original:

main.c
Run Online
#include <stdio.h>
typedef struct {
    int x, y;
}
Point;
void movePoint(Point *p, int dx, int dy) {
    if (p == NULL) return;
    // always check
    p->x += dx;
    // modifies the CALLER's struct    p->y += dy;}
    int main(void) {
        Point p = {
            10, 20
        };
        printf("Before: (%d, %d)\n", p.x, p.y);
        movePoint(&p, 5, -3);
        // pass the address
        printf("After : (%d, %d)\n", p.x, p.y);
        return 0;
    }

Output:

main.c
Run Online
Before:(10, 20)After :(15, 17)

This is the whole reason struct pointers exist in function APIs: C is pass-by-value, so a struct passed by value is a copy — and modifying the copy does nothing. You met this pattern with swap in the pointers cluster; here it's the same mechanism applied to a compound type.

Contrast with by-value:

main.c
Run Online
void movePointBroken(Point p, int dx, int dy) {
    p.x += dx;
    // modifies the copy    p.y += dy;
}
// the copy dies — caller sees nothing

The rule: if a function needs to change a struct, it must take a pointer. No exceptions, because there's no other mechanism.


Efficiency: Why Pointers Are the Default

Beyond modification, there's the cost question. Consider a struct with realistic bulk:

main.c
Run Online
typedef struct {
    char name[64];
    char email[128];
    double balance;
    int transactionIds[32];
}
Account;
printf("%zu bytes\n", sizeof(Account));
// ~312 bytes (plus padding)

By value:

main.c
Run Online
void processAccount(Account acc);
// copies ~312 bytes EVERY call

By pointer:

main.c
Run Online
void processAccount(Account *acc);
// copies 8 bytes every call

Now put it in a loop:

main.c
Run Online
for (int i = 0;
i < 100000;
i++) {
    processAccount(accounts[i]);
    // 31 MB copied    // vs    processAccount(&accounts[i]);
    // 800 KB copied
}

The pointer version moves roughly 40× less data. On modern hardware you might not notice for 100 calls — but in a tight loop, an inner algorithm, or code handling thousands of records, this is the difference between instant and sluggish.

And there's a second effect: copying large structs also stresses the cache. Passing a pointer keeps the data in one place and lets the CPU work on it directly, which is often the bigger win in practice.

Practical guidance:

Struct sizeRecommendation
Small (2–3 ints, ~8–16 bytes)By value is fine — simpler, and the copy is cheap
Medium/large (anything with arrays or many members)By pointer
Any size — function must modify itBy pointer (required)
Any size — read-only in the functionBy const pointer

(Suggested image: the byte-count comparison — "312 bytes copied per call" vs "8 bytes per call" with the loop totals shown)


const Struct Pointers: Read-Only Access, Cheap

The professional default for read-only struct parameters:

main.c
Run Online
void printAccount(const Account *acc) {
    if (acc == NULL) return;
    printf("Name    : %s\n", acc->name);
    printf("Balance : %.2f\n", acc->balance);
    // acc->balance = 0;              // ✗ compile error — const protected}

What const Account * gives you:

  • Cheap passing — only the address travels
  • Enforced read-only — the compiler rejects any attempt to modify through this pointer
  • Clear intent — any reader sees instantly that this function doesn't change your data
  • Wider applicability — a function taking const Account * can be called with data the caller considers read-only, or even with temporary values

The naming convention that helps: functions taking const pointers are typically read-only operations (printAccount, calculateTotal, findById), while non-const pointer parameters signal modification (updateBalance, sortAccounts). Naming them consistently makes the API self-documenting.

Be aware of the two forms of const with pointers — they're different:

main.c
Run Online
const Account *p1;
// pointer TO const — can't modify *p1, CAN reassign p1Account * const p2 = &a;
// CONST pointer — can modify *p2, CANNOT reassign p2const Account * const p3 = &a;
// both const

In function parameters, const Account * is by far the most common — you want to prevent modification of the struct, not forbid the function from using its own local pointer variable.

(Suggested image: the two const forms compared — showing which part is locked in each case)


Dynamically Allocated Structs

Heap allocation is a third reason pointers are unavoidable: malloc returns an address, so a heap struct is always accessed through a pointer.

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
    char *name;
    // heap-allocated string    int   age;} Person;
    int main(void) {
        //
1. Allocate the struct itself Person *p = malloc(sizeof(Person));
        if (p == NULL) {
            puts("Allocation failed.");
            return 1;
        }
        //
2. Initialize its members p->name = malloc(50);
        // allocate room for the name    if (p->name == NULL) {        free(p);                           // clean up what we allocated        return 1;    }
        strcpy(p->name, "Priya");
        p->age = 22;
        printf("Name: %s\n", p->name);
        // arrow access throughout    printf("Age : %d\n", p->age);
        //
3. Free in reverse order of allocation free(p->name);
        // inner block first    free(p);                               // then the struct    p = NULL;
        return 0;
    }

Output:

Several important details:

  • malloc(sizeof(Person)) — allocates the struct's bytes. Note sizeof(Person), the whole struct, not a member.
  • Every access is p->member — because p is a pointer. There is no "variable" here at all.
  • Nested allocation — name is itself a pointer, so it needs its own malloc. A struct containing pointers means multiple allocations.
  • Partial failure cleanup — if the second malloc fails, we free the first. Otherwise the struct allocation leaks.
  • Reverse-order freeing — free the inner blocks before the container. If you free p first, you've lost the pointer to p->name and that memory leaks.
  • Nulling after free — the habit from the NULL pointers page.

The alternative — a fixed array member:

main.c
Run Online
typedef struct {
    char name[50];
    // fixed inline storage — no second malloc needed    int  age;
}
Person2;
Person2 *q = malloc(sizeof(Person2));
strcpy(q->name, "Ravi");
// no separate allocationfree(q);
// one free, done

That's simpler and less error-prone (one allocation, one free), at the cost of a fixed maximum name length. For most code, fixed-size array members are the better default — reach for the pointer member only when you genuinely need runtime-sized strings.

(Suggested image: the two struct layouts compared — pointer member requiring two allocations vs inline array member needing just one, with the matching free calls shown)


Pointer to an Array of Structs

This combination handles large dynamic collections:

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
    char name[30];
    float score;
}
Player;
int main(void) {
    int n;
    printf("How many players? ");
    scanf("%d", &n);
    if (n <= 0) return 1;
    // Allocate an array of n Players — one malloc, not n    Player *team = malloc(n * sizeof(Player));
    if (team == NULL) {
        puts("Allocation failed.");
        return 1;
    }
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
    for (int i = 0; i < n; i++) {
        printf("Player %d name: ", i + 1);
        fgets(team[i].name, sizeof(team[i].name), stdin);
        team[i].name[strcspn(team[i].name, "\n")] = '\0';
        printf("Player %d score: ", i + 1);
        scanf("%f", &team[i].score);
        while ((c = getchar()) != '\n' && c != EOF);
    }
    // Access via pointer indexing, then arrow, or both forms:    puts("\n--- Team ---");    for (int i = 0; i < n; i++) {        printf("%-15s %6.1f\n", team[i].name, team[i].score);    }
    // The pointer-arithmetic form — identical meaning:    puts("\n--- Same data, pointer form ---");    for (Player *cur = team; cur < team + n; cur++) {        printf("%-15s %6.1f\n", cur->name, cur->score);    }
    // Find the top scorer using a pointer    Player *best = team;    for (Player *cur = team + 1; cur < team + n; cur++) {        if (cur->score > best->score) {            best = cur;                          // just move the pointer — no copying        }    }
    printf("\nTop scorer: %s (%.1f)\n", best->name, best->score);
    free(team);
    team = NULL;
    return 0;
}

Sample Run:

main.c
Run Online
How many players? 3
Player 1 name: Priya
Player 1 score: 88.5
Player 2 name: Ravi
Player 2 score: 94.0
Player 3 name: Meera
Player 3 score: 79.5
--- Team ---
Priya 88.5
Ravi 94.0
Meera 79.5
--- Same data, pointer form ---
Priya 88.5
Ravi 94.0
Meera 79.5
Top scorer: Ravi(94.0)

Three access styles in one program, all equivalent:

StyleSyntaxWhen it reads best
Array indexingteam[i].nameWhen you need the index, or clarity
Pointer indexing(team + i)->nameRare — mostly historical
Walking pointercur->name with cur++When moving sequentially

The best pointer is the genuinely instructive part: instead of copying the top player's data into a separate variable (or tracking an index and re-indexing later), we just move a pointer to point at the best record. best->name then reaches the winner directly. Pointers avoid copying whole records, which matters once records are large — a pattern you'll use constantly with struct arrays.

(Suggested image: the array of structs with a best pointer arrow moving to the highest-scoring record — showing that no data is copied, only the arrow moves)


Common Mistakes

  1. Using . on a struct pointer — ptr.x doesn't compile. Pointers need ->.
  2. *ptr.x instead of (*ptr).x — precedence bites. Use ptr->x and sidestep it entirely.
  3. Passing a struct by value and expecting modification — the copy is discarded. Pass a pointer.
  4. Forgetting & at the call site — movePoint(p, ...) where the parameter is Point *p won't compile cleanly; you need movePoint(&p, ...).
  5. Allocating the struct but not its pointer members — p->name is a pointer; it needs its own malloc (or should be an inline array member).
  6. Freeing the struct before its members — free inner blocks first, container last. Otherwise the inner pointers are lost and leak.
  7. Not checking malloc for NULL — the struct pointer may be null; dereferencing p->member then crashes.
  8. Assuming sizeof(struct) equals the sum of members — padding. Use sizeof on the struct type, never add up members by hand.
  9. Using an uninitialized struct pointer — Person *p; p->age = 5; writes to a random address. Initialize to NULL or allocate.
  10. Passing large structs by value in loops — the silent performance cost. Pass const pointers.
  11. Modifying through a const pointer — good news, actually: the compiler catches it. But if you need to modify, drop the const deliberately rather than casting it away.

(Suggested image: "11 Struct Pointer Mistakes" checklist graphic — with #1, #4, and #6 highlighted)


Final Thoughts

Pointers to structures are less a new topic than a combination of two you already know — and that's exactly why they matter. Together they give you cheap access (8 bytes instead of hundreds), the ability to modify (which pass-by-value forbids), and heap-allocated data (which requires pointers by definition).

The syntax reduces to one rule: value gets a dot, pointer gets an arrow. Everything else follows — check for NULL, use const for read-only parameters, free inner members before the container, and reach for pointers whenever a struct is more than a couple of small fields.

And notice what you can now build: an array of records allocated at runtime, searched by moving a pointer instead of copying data. That's the shape of nearly every real C data structure — and it's one step away from linked lists, which are nothing more than structs that point to other structs.

Next in this cluster: nested structures, arrays of structures in depth, or self-referential structs and linked lists.

Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
    int id;
    char name[32];
    float score;
}
Student;
void printStudent(const Student *s) {
    printf("Student #%d: %s | Score: %.1f\
", s->id, s->name, s->score);
}
int main() {
    Student s1 = {
        1, "Emma Watson", 95.5f
    };
    printStudent(&s1);
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Pass structs larger than 16 bytes by pointer (const MyStruct *ptr) to avoid performance bottlenecks caused by stack copying.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: Why is `(*ptr).member` equivalent to `ptr->member`?

A: The arrow operator `->` was introduced into C as syntactic sugar to make dereferencing and member selection concise and readable.
Q

Q: What is the performance benefit of passing `const Struct *s` over `Struct s`?

A: Passing by pointer copies only 8 bytes of memory address on the CPU stack regardless of whether the struct is 100 bytes or 10 megabytes.

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations