Intuition What this page is for
The parent note taught you what . and -> mean. This page hunts down every situation a struct member-access can put you in — plain value, pointer, nested, array-of-structs, pointer-to-struct-inside-array, a struct field that is itself a pointer, degenerate (empty/zero/null) cases, and the exam traps — and works each one from the ground up. By the end you should never meet an access pattern you haven't already seen solved.
Before anything: two words we will lean on constantly.
Definition "Object" vs "address" — the only fork that matters
You hold the object when a variable is the struct itself (e.g. struct Point a;). Getting a member: use ==the dot .==.
You hold the address when a variable stores where the struct lives — a pointer (e.g. struct Point *p;). Getting a member: use ==the arrow ->==, because you must first travel to the object.
Every example below is just this one fork, applied over and over.
Below is the full list of case-classes a member access can fall into. Each later example is tagged with the cell(s) it covers, so you can see the grid fill up.
#
Case class
What's tricky about it
Covered by
A
Plain value, single-level
baseline — just .
Ex 1
B
Pointer to value, single-level
must use ->, not .
Ex 2
C
. and -> giving the same result
proving p->m == (*p).m
Ex 3
D
Nested struct, all values
chain of dots e.when.d
Ex 4
E
Nested struct via a pointer
first hop ->, then .
Ex 4
F
Array of structs (value access)
index first, then dot
Ex 5
G
Pointer walking an array of structs
-> while the pointer moves
Ex 6
H
Function modifying caller's struct
why &a + -> inside
Ex 7
I
Degenerate: zero-initialised / "empty" struct
what a struct looks like at value 0
Ex 8
J
Real-world word problem
translate a story into structs
Ex 9
K
Exam twist: *p.m vs (*p).m precedence trap
operator binding
Ex 10
L
Field that is itself a pointer-to-struct
. then -> chained
Ex 11
M
Degenerate: -> through a null / uninitialised pointer
undefined behaviour / crash
Ex 12
Worked example Example 1 — plain value, single level
(cell A)
struct Point { int x, y; };
struct Point a;
a.x = 3 ;
a.y = 4 ;
printf ( "( %d , %d )" , a.x, a.y);
Forecast: guess the printed line before reading on.
a is declared as a struct value — the object itself lives in a.
Why this step? We need to know which side of the fork we're on. a is an object → dot.
a.x = 3 writes into the x slot; a.y = 4 writes into the independent y slot.
Why this step? Members are separate boxes — touching one never touches the other.
printf reads both back with ..
Why this step? Same fork: still holding the object.
Verify: x=3, y=4 → output is (3,4). Independent slots, no interference.
The figure below draws exactly this: the object a is the outer red box, and x and y are the two separate inner black slots. Notice the two slots never overlap — that is why setting a.x leaves a.y alone. Trace each arrow from a slot down to the label a.x / a.y you would write to reach it.
Worked example Example 2 — pointer to value
(cell B)
struct Point a = { 3 , 4 };
struct Point * p = & a; // p stores the ADDRESS of a
p -> x = 10 ;
printf ( "( %d , %d )" , a.x, a.y);
Forecast: does changing p->x change a? What prints?
&a produces the address of a; p stores it.
Why this step? Now we hold an address, not the object → the fork sends us to ->.
p->x = 10 means "travel to the object p points at, then set its x". That object is a.
Why this step? There is only one Point here. p doesn't make a copy — it points at the same bytes as a.
a.x reads the (now changed) value.
Why this step? Confirms the write reached the real object.
Verify: a.x became 10, a.y untouched → output (10,4).
In the figure, the red box on the left is p — it does not hold a Point, it holds &a. The red arrow is the "travel" step p->x performs before touching any member; follow it across to the real object a on the right, where x has become 10.
p.x here
p is an address, not a struct. p.x gives "request for member 'x' in something not a structure" . Fork rule: address → arrow.
Worked example Example 3 — proving
p->m equals (*p).m (cell C)
struct Point a = { 3 , 4 };
struct Point * p = & a;
int viaArrow = p -> y;
int viaStar = ( * p).y;
printf ( " %d %d " , viaArrow, viaStar);
Forecast: are the two numbers the same? Why must they be?
*p dereferences — it is the object a.
Why this step? Following the address gives us the object back.
(*p).y then dots into that object. The parentheses force "dereference first".
Why this step? . binds tighter than *, so without parens the compiler reads *(p.y) — nonsense.
p->y is defined by the language to mean exactly step 2, just prettier.
Why this step? Shows the arrow is pure shorthand, not new behaviour.
Verify: both read a.y = 4 → output 4 4. Equal, as guaranteed.
Worked example Example 4 — nested struct, by value and via pointer
(cells D, E)
struct Date { int d, m, y; };
struct Event { char title [ 10 ]; struct Date when; };
struct Event e;
strcpy (e.title, "Exam" );
e.when.d = 25 ; // (cell D)
e.when.m = 12 ;
struct Event * pe = & e;
pe -> when.y = 2025 ; // (cell E)
printf ( " %s %d - %d - %d " , e.title, e.when.d, e.when.m, e.when.y);
Forecast: what date prints?
e is a value → e.when is also a value (an inner Date object living inside e).
Why this step? Nesting doesn't change the fork; each . lands on another object.
e.when.d = 25: first dot reaches the inner Date, second dot reaches its field d.
Why this step? Chain the operator that matches what you currently hold. Both hops are values → both dots.
pe is a pointer → first hop is -> to reach e. But pe->when is again a value (the inner Date), so we continue with ..
Why this step? The fork is re-evaluated at every hop. Pointer → arrow once; the inner struct is held by value → dot after.
Verify: title Exam, date 25-12-2025 → output Exam 25-12-2025.
Re-ask the fork at every dot. "Do I hold an object or an address right now ?" Answer per-hop, not once for the whole line.
Worked example Example 5 — array of structs
(cell F)
struct Point pts [ 3 ] = { { 1 , 1 }, { 2 , 4 }, { 3 , 9 } };
int sum = 0 ;
for ( int i = 0 ; i < 3 ; i ++ )
sum += pts [i].y; // index FIRST, then dot
printf ( " %d " , sum);
Forecast: guess the sum of the y values.
pts[i] selects the i-th struct value in the array.
Why this step? Indexing an array of structs yields a struct object (not a pointer). Object → dot.
pts[i].y reads that object's y.
Why this step? [] binds tighter than ., so pts[i].y correctly means (pts[i]).y — no parens needed.
Accumulate 1 + 4 + 9.
Why this step? Sanity target for the verify.
Verify: 1 + 4 + 9 = 14 → output 14.
The figure lays the three struct values side by side. The red y field in each box is the one the loop pulls out; follow the red arrow under each box to the exact access expression pts[i].y. The running total in red at the top is what sum accumulates to.
Worked example Example 6 — walking with a pointer
(cell G)
struct Point pts [ 3 ] = { { 1 , 1 }, { 2 , 4 }, { 3 , 9 } };
struct Point * q = pts; // q points at pts[0]
int sum = 0 ;
for ( int i = 0 ; i < 3 ; i ++ ) {
sum += q->x; // q is a pointer → arrow
q ++ ; // step to the next struct
}
printf ( " %d " , sum);
Forecast: what does the sum of the x values come to?
q = pts makes q hold the address of the first element.
Why this step? An array name decays to a pointer to its first element. So now we hold an address → arrow.
q->x reads the x of whatever struct q currently points at.
Why this step? Fork: address → ->.
q++ advances the pointer by one whole struct (not one byte). The compiler scales the step by sizeof(struct Point).
Why this step? This is the crucial pointer-arithmetic rule that makes the loop visit each element exactly once.
Verify: 1 + 2 + 3 = 6 → output 6.
The figure shows q as a red marker that hops along the top of the array. Each red q++ arrow is a jump of one whole struct — never a single byte — which is why the marker lands cleanly on the start of the next box every time.
See Pointers in C for why q++ jumps by sizeof and not by 1.
Worked example Example 7 — function modifies the caller's struct
(cell H)
void grow ( struct Point * r ) {
r->x += 1 ;
r->y += 1 ;
}
struct Point a = { 3 , 4 };
grow ( & a );
printf ( "( %d , %d )" , a.x, a.y);
Forecast: does a change? To what?
grow(&a) passes the address of a, not a copy of it.
Why this step? If we passed a by value, grow would edit a throw-away copy and main's a would stay (3,4).
Inside, r holds that address → we use -> to reach the real object.
Why this step? Fork: address → arrow. r->x += 1 edits the same bytes a lives in.
Back in main, a reflects the change.
Why this step? Confirms the edit crossed the function boundary.
Verify: a becomes (4,5) → output (4,5).
See Passing Structures to Functions for by-value vs by-pointer trade-offs.
Worked example Example 8 — zero-initialised (empty) struct
(cell I)
struct Student { char name [ 20 ]; int roll; float marks; };
struct Student blank = { 0 }; // every member zeroed
printf ( "[ %s ] %d %.1f " , blank.name, blank.roll, blank.marks);
Forecast: what does an "empty" struct print? Is name a crash?
= {0} initialises the first member to 0 and, by the C rule, zeros all the rest too.
Why this step? This is the safe, defined way to get a "blank" struct — no garbage.
name becomes an array whose first byte is '\0' (0). Printed with %s, that is the empty string — safe, not a crash.
Why this step? A zero first byte is a valid, empty C string; %s stops immediately.
roll is 0, marks is 0.0.
Why this step? Confirms every slot is a defined zero.
Verify: output [] 0 0.0. The [] shows an empty (not garbage) name.
Common mistake Leaving a struct
uninitialised
struct Student x; (no = {0}) as a local variable contains garbage , not zeros. Printing x.name with %s may crash. Always initialise, or memset(&x, 0, sizeof x).
Worked example Example 9 — real-world: a wallet of coins
(cell J)
Story: A wallet holds a count of ₹1, ₹5 and ₹10 coins. Model it as a struct and compute the total money.
struct Wallet { int ones, fives, tens; };
struct Wallet w = { 7 , 3 , 2 }; // 7×1, 3×5, 2×10
int total = w.ones * 1 + w.fives * 5 + w.tens * 10 ;
printf ( "Rs %d " , total);
Forecast: guess the rupee total before computing.
Identify the entity (a wallet) and its fields (counts per coin) → one struct, three int members.
Why this step? Structs shine when several values describe the same thing; a wallet is exactly that.
w is a value → each field read with ..
Why this step? Fork: we hold the object.
Weight each count by its coin value and sum: 7 ⋅ 1 + 3 ⋅ 5 + 2 ⋅ 10 .
Why this step? Turns the word "total money" into arithmetic on members.
Verify: 7 + 15 + 20 = 42 → output Rs 42. Units: coins × rupees/coin = rupees. ✓
Worked example Example 10 —
*p.m vs (*p).m (cell K)
struct Box { int * ptr; };
int value = 99 ;
struct Box b = { & value };
struct Box * p = & b;
// Trick: what does *p.ptr try to do?
printf ( " %d " , * ( * p).ptr); // the CORRECT way
Forecast: why does *p.ptr (without inner parens) fail to compile, and what does the correct line print?
. binds tighter than *. So the compiler reads *p.ptr as *(p.ptr).
Why this step? Precedence decides grouping before meaning. This is the whole trap.
But p is a pointer , so p.ptr is illegal (p is not a struct) → compile error. You must write (*p).ptr to dereference p first. Equivalently p->ptr.
Why this step? Fork rule again: address → travel first.
(*p).ptr is b.ptr, which is &value. The outer * then reads through it to get value.
Why this step? Two levels of indirection: one to reach the box, one to reach the int the box points to.
Verify: (*p).ptr = &value, *(&value) = 99 → output 99.
Worked example Example 11 — dot then arrow, chained
(cell L)
struct Date { int d, m, y; };
struct Container { struct Date * when; }; // field is a POINTER to a Date
struct Date today = { 25 , 12 , 2025 };
struct Container c;
c.when = & today; // the field stores an ADDRESS
printf ( " %d " , c.when -> d ); // dot to the field, THEN arrow through it
Forecast: what number prints, and why does the operator change halfway through the line?
c is a value → reach its field with . : c.when.
Why this step? Fork, first hop: we hold the object c, so dot.
But c.when is not a Date — it is a pointer to a Date (an address). So the fork flips: to reach its member we now need ->.
Why this step? Re-ask the fork after each hop. The first hop gave us an address, so the next hop travels with ->.
c.when->d = "the field when of c, then travel through that pointer to the d inside today".
Why this step? This is the mirror image of Example 4: there the inner struct was a value (dot after arrow); here it is a pointer (arrow after dot).
Verify: c.when points at today, whose d is 25 → output 25.
The figure shows the flip in action: c is a plain box (reached by .), but its when slot holds a red arrow (an address) that you must follow with -> to land on the separate today object.
Mnemonic Dot-then-arrow vs arrow-then-dot
The operator depends only on what the field itself is : a nested struct value → keep dotting; a nested struct pointer → switch to ->. Never guess from the line's start — inspect each hop.
Worked example Example 12 — arrow through a null / uninitialised pointer
(cell M)
struct Date { int d, m, y; };
struct Date * p = NULL ; // points at nothing
int day = p -> d; // UNDEFINED BEHAVIOUR — likely crash
struct Date * q; // uninitialised: holds GARBAGE address
int bad = q -> d; // UNDEFINED BEHAVIOUR — reads a random address
Forecast: the fork rule says "address → arrow", so p->d looks legal. Why is it still catastrophic?
-> always dereferences first (Example 3: p->d ≡ (*p).d). Dereferencing means "read the bytes at this address".
Why this step? The operator's meaning is fixed; it never checks whether the address is valid .
NULL is the address "nowhere" (numerically 0). Reading the object at nowhere is not allowed — the OS usually kills the program with a segmentation fault.
Why this step? Passing the fork rule ("hold an address → arrow") is necessary but not sufficient : the address must also point at a real object.
q was never assigned, so it holds a leftover garbage address. q->d reads some random memory — sometimes it "works", sometimes it corrupts data, sometimes it crashes. That unpredictability is undefined behaviour.
Why this step? Shows that "no crash this time" does not mean the code is correct.
Verify (mental, not a value): there is no defined answer to check — the program's behaviour is undefined. The correct fix is to guard first:
if (p != NULL ) day = p -> d; // only travel when the address is real
Common mistake "It compiled and ran once, so it's fine"
-> on NULL or an uninitialised pointer compiles cleanly and may even appear to work. Always ensure a struct pointer is set to a real object (via &obj, a successful malloc, or an array element) before you follow it. See Pointers in C and Linked Lists — traversing a list means checking node != NULL at every node->next.
Recall Fast self-test
Given struct Node *n; how do you read member data? ::: n->data (n is a pointer → arrow)
Given struct Node arr[5]; how do you read data of element 2? ::: arr[2].data (indexing gives a value → dot)
Why is q++ on a struct Point *q NOT a 1-byte step? ::: It scales by sizeof(struct Point) so it lands on the next whole struct.
What does = {0} guarantee for a struct? ::: Every member is zero-initialised (empty string, 0, 0.0).
If c.when is a struct Date *, how do you read its d? ::: c.when->d — dot to the field, then arrow through the pointer.
Why is p->d catastrophic when p is NULL? ::: -> dereferences first; reading the object at address "nowhere" is undefined behaviour (usually a crash).
Pointers in C — the address side of every fork, q++ scaling, and null/uninitialised pointer dangers.
Arrays vs Structures — Ex 5 & 6 mix both: an array of structs .
Linked Lists — Ex 11 & 12's pointer-inside-struct is the seed of a node; traversal guards against null.
typedef — drop struct from these type names.
Memory Alignment & Padding — why q++ may skip padding bytes too.
Passing Structures to Functions — Ex 7 in full.
Parent: 5.1.22 Structures — declaration, accessing members (. and - - ) (Hinglish)
function edits caller via arrow
check not null before travel