typedef struct {
int x;
int y;
}
Point;
Point p = {
10, 20
}
;
Point *ptr = &p;
// pointer to a PointPointers to Structures in C: Accessing Members Efficiently
What is Pointers to Structures in C? Core Concept & Direct Summary
How do structure pointers and the arrow operator `->` provide zero-copy access to composite records?
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:
- 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.
- Modification. Functions need to change a struct's members, and C is pass-by-value. Pointers are the only way.
- 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
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):
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:
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:
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
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:
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:
Output:
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:
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:
By value:
By pointer:
Now put it in a loop:
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:
(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:
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:
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.
Output:
Several important details:
malloc(sizeof(Person))— allocates the struct's bytes. Notesizeof(Person), the whole struct, not a member.- Every access is
p->member— becausepis a pointer. There is no "variable" here at all. - Nested allocation —
nameis itself a pointer, so it needs its ownmalloc. A struct containing pointers means multiple allocations. - Partial failure cleanup — if the second
mallocfails, we free the first. Otherwise the struct allocation leaks. - Reverse-order freeing — free the inner blocks before the container. If you free
pfirst, you've lost the pointer top->nameand that memory leaks. - Nulling after free — the habit from the NULL pointers page.
The alternative — a fixed array member:
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:
Sample Run:
Three access styles in one program, all equivalent:
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
- Using
.on a struct pointer —ptr.xdoesn't compile. Pointers need->. *ptr.xinstead of(*ptr).x— precedence bites. Useptr->xand sidestep it entirely.- Passing a struct by value and expecting modification — the copy is discarded. Pass a pointer.
- Forgetting
&at the call site —movePoint(p, ...)where the parameter isPoint *pwon't compile cleanly; you needmovePoint(&p, ...). - Allocating the struct but not its pointer members —
p->nameis a pointer; it needs its ownmalloc(or should be an inline array member). - Freeing the struct before its members — free inner blocks first, container last. Otherwise the inner pointers are lost and leak.
- Not checking
mallocforNULL— the struct pointer may be null; dereferencingp->memberthen crashes. - Assuming
sizeof(struct)equals the sum of members — padding. Usesizeofon the struct type, never add up members by hand. - Using an uninitialized struct pointer —
Person *p; p->age = 5;writes to a random address. Initialize toNULLor allocate. - Passing large structs by value in loops — the silent performance cost. Pass
constpointers. - Modifying through a
constpointer — good news, actually: the compiler catches it. But if you need to modify, drop theconstdeliberately 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
#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
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 questionsQ: Why is `(*ptr).member` equivalent to `ptr->member`?
Q: What is the performance benefit of passing `const Struct *s` over `Struct s`?
Related Tools, Tutorials & Authoritative References
Recommended Interactive Tools
- Online C Compiler & Runner — Interactive online GCC sandbox to execute C code in real time.
- Online C Code Editor & IDE — Multi-file C programming environment with code formatting.
- C++ Programming Tutorial — Transition from procedural C to Object-Oriented C++ with STL.
- JavaScript Programming Tutorial — Master modern JavaScript fundamentals, DOM APIs, and async patterns.
Authoritative Standards & Citations
- ISO/IEC 9899 (C Language Standard) — Official international C specification by ISO/IEC JTC 1/SC 22/WG 14.
- GNU GCC Documentation — Official reference manual for the GNU Compiler Collection and C libraries.
- The C Programming Language (K&R) — Definitive classic work by Brian Kernighan & Dennis Ritchie at Bell Labs.
- C Reference Documentation (cppreference.com) — Verified reference covering C89 through C23 language features.