C / STRUCTS, UNIONS AND ENUMS
Structs, pointers and the arrow operator
Use p->x to read and write struct members through a pointer, know why (*p).x needs its parentheses, and chain arrows safely through nested pointers.
What you will learn
- Read p->x as (*p).x: dereference the pointer, then select the member.
- Keep the parentheses in (*p).x because . binds tighter than unary *.
- Know that p->x = 5 changes the pointee while p = &b changes only the pointer.
- Chain a->b->c left to right and check each intermediate pointer for NULL.
Understanding Structs, pointers and the arrow operator
A pointer to a struct holds only an address, so reaching a member takes two steps: dereference the pointer to name the struct object, then select the member from it. Written out that is (*p).x, and the inner parentheses are not decoration, because the member selector . binds tighter than unary *, so *p.x would parse as *(p.x) and ask for a member named x inside the pointer itself, which no pointer has. Since that two-step pattern appears constantly in C, the language supplies -> as one operator meaning exactly the same thing: p->x and (*p).x are interchangeable everywhere, including as assignment targets.
The useful mental model is that p->x names a location, not a value copied out of one. It designates the member sitting at a fixed offset inside the object p currently points at, so p->x = 5 writes into that object and every other pointer aimed at it sees the new value, while &p->x is a perfectly good address you can pass around. That makes p->x = 5 and p = &b completely different acts: the first changes the pointee, the second changes which object later arrow expressions talk about and leaves both structs untouched.
The arrow is a postfix operator, in the same precedence tier as [], function calls and postfix ++, all of which bind tighter than unary * and & and than anything arithmetic. So *p->q means *(p->q), p->a[2] means (p->a)[2], and p->n++ increments the member rather than the pointer, while chains like a->b->c simply associate left to right, each arrow consuming one level of indirection. Nothing is checked at run time, since the compiler turns p->x into a load at the pointer's value plus the member's offset, so every pointer in a chain must really point at a live object or the program is undefined rather than merely wrong.
<stdio.h>
struct Point {
int x;
int y;
};
struct Segment {
struct Point *start;
struct Point *end;
};
int main(void)
{
struct Point a = { 1, 2 };
struct Point b = { 7, 5 };
struct Point *p = &a;
struct Segment s = { &a, &b };
printf("(*p).x = %d, p->y = %d\n", (*p).x, p->y);
p->x += 10; /* writes into a, not into p */
printf("a.x is now %d\n", a.x);
printf("dx = %d, dy = %d\n", s.end->x - s.start->x,
s.end->y - s.start->y);
p = &b; /* changes p, leaves a alone */
printf("p->x = %d, a.x = %d\n", p->x, a.x);
return 0;
}
p->x is not new machinery: it is exactly (*p).x, a dereference followed by a member selection, given its own operator because . binds tighter than unary *.
Worked examples
Arrow binds tighter than *
Shows how * , [] and postfix -- combine with -> when a member is itself a pointer.
<stdio.h>
struct Box {
int *data;
int n;
};
int main(void)
{
int nums[3] = { 4, 9, 16 };
struct Box b = { nums, 3 };
struct Box *bp = &b;
printf("*bp->data = %d\n", *bp->data);
printf("bp->data[1] = %d\n", bp->data[1]);
*bp->data = 100;
printf("nums[0] = %d\n", nums[0]);
bp->n--;
printf("b.n = %d\n", b.n);
return 0;
}
Example explained
Line 1*bp->data is *(bp->data): the arrow runs first because it is postfix, then * dereferences the int pointer it produced.
Line 2bp->data[1] is (bp->data)[1], so the subscript indexes nums, not some array of Box objects.
Line 3*bp->data = 100 stores through the member pointer, so nums[0] changes while b.data still points at nums.
Line 4bp->n-- decrements the member inside b; bp is untouched because the postfix -- applies to bp->n.
Walking an array with a struct pointer
Demonstrates the same arrow expression reading different elements as a pointer advances.
<stdio.h>
struct Item {
const char *name;
int qty;
};
int main(void)
{
struct Item cart[3] = { { "bolt", 4 }, { "nut", 8 }, { "washer", 2 } };
struct Item *it;
int total = 0;
for (it = cart; it != cart + 3; it++) {
printf("%s x%d\n", it->name, it->qty);
total += it->qty;
}
printf("total %d\n", total);
return 0;
}
Example explained
Line 1it = cart aims the loop pointer at the first element, since an array name yields a pointer to element zero.
Line 2it++ advances by sizeof(struct Item) bytes, so it always lands on the start of the next element.
Line 3it->qty loads the member at a fixed offset from wherever it points, which is why one expression serves every element.
Line 4it != cart + 3 stops one past the last element; that address may be formed and compared but never dereferenced.
Chained arrows through a self-referential struct
Shows a->b->c evaluating left to right and how a NULL member ends the chain.
<stdio.h>
struct Node {
int value;
struct Node *next;
};
int main(void)
{
struct Node third = { 3, NULL };
struct Node second = { 2, &third };
struct Node first = { 1, &second };
struct Node *cur = &first;
printf("%d %d %d\n", cur->value, cur->next->value,
cur->next->next->value);
while (cur != NULL) {
printf("node %d, next %s\n", cur->value,
cur->next ? "yes" : "no");
cur = cur->next;
}
return 0;
}
Example explained
Line 1cur->next->next->value evaluates left to right: each arrow yields a struct Node * that the next arrow dereferences.
Line 2cur = cur->next copies a member pointer into cur, moving the traversal along without modifying any node.
Line 3third.next is NULL, so the loop test fails after the third node instead of dereferencing a null pointer.
Line 4cur->next ? ... only reads the member as a truth value, so it stays safe even when that member is NULL.
Important notes
The arrow performs no run-time check; it compiles to a load at pointer value plus member offset, so an uninitialized, NULL or freed pointer makes p->x undefined behaviour.
p->x designates the member itself rather than a copy, so &p->x is a usable address and writes through it are visible to every other pointer aimed at that object.
Common mistakes
Writing *p.x instead of p->x: it parses as *(p.x), and the compiler rejects it with a message about requesting a member in something that is not a structure or union.
Using p.x when p is a pointer (or v->x when v is a value): this is a type error, and silencing it with a cast hides the real problem instead of picking the right operator.
Chaining through a pointer that may be NULL, as in node->next->value: the arrow just adds an offset to that null address and loads from it, giving a crash or silent garbage rather than a diagnostic.
Try it yourself
Change, predict, then run
In a browser editor define struct Point { int x, y; }; and struct Rect { struct Point *tl, *br; };, build a Rect from two Point objects, take a struct Rect *r, and print width and height using only arrow expressions such as r->br->x - r->tl->x. Then change one Point through r (r->tl->x = ...) and print again to confirm the Rect reports the new size.
Open the C workspaceCheck your understanding
Given struct Box { int *data; int n; }; and struct Box *bp = &b;, what does the statement *bp->data = 7; modify?
- The data pointer stored inside the struct that bp points at
- The int that bp->data points at
- The pointer variable bp itself
- Nothing: it is a compile error, because unary * cannot be applied to the result of an arrow expression
Show answer
-> is a postfix operator and binds tighter than unary *, so the statement is *(bp->data) = 7: it fetches the member pointer first and then writes into the int that pointer addresses. Option 0 is the tempting one, but changing the member would be written bp->data = ... with no leading *; and the parentheses in (*bp).data are needed only when * applies to bp before a dot, which is not the situation here.