Skip to content
VLSI Mentor

C Part I · Module 10 · Structures, Unions, Enums and User-Defined Types

Pointers to Structures and the Arrow Operator

So far a structure has always been reached by name, as an object sitting in the function that declared it — and that is the situation real C code is least often in. Structures are big enough that programs pass their addresses rather than their contents, and a function that is supposed to change one has no choice in the matter. So most structure code you will ever read reaches its members through a pointer, and C gives that case an operator of its own. This chapter covers what that operator abbreviates, why the longer form it replaces needs parentheses you will get wrong at least once, the three increment expressions that all compile and all mean different things, and the one structure shape that cannot be built at all without a pointer.

Intermediate19 min readCPart IFundamentalsTypesPointers

Part I · Module 10 · Chapter 10.3 · Pointers to Structures and the Arrow Operator

1. A Pointer to a Structure Is an Ordinary Pointer

Nothing new is needed to make one. Chapter 7.2's address-of operator works on a structure object exactly as it works on an int:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct counter k = { .hits = 0, .tag = 'k' };
    struct counter *p = &k;

p has type struct counter *, it holds the address of k, and every rule from Module 7 applies unchanged — it can be null (7.4), it can be const-qualified (7.11), it can be passed to a function (7.5), and arithmetic on it moves by whole structures (7.8). Nothing about structures changes the pointer model.

What does need attention is reaching a member through it, and that is the whole of this chapter.

2. Why This Form Dominates

Two measurable reasons, and one that is not about measurement at all.

Azvya Education Pvt. Ltd.VLSI Mentor
a pointer is small; the thing it points at need not be
#include <stdio.h>

struct big { int a[16]; double d; char tag; };

int main(void)
{
    struct big v = { .tag = 'z' };
    struct big *p = &v;

    printf("sizeof(struct big) = %zu\n", sizeof(struct big));
    printf("sizeof p           = %zu   (a pointer)\n", sizeof p);
    printf("sizeof *p          = %zu   (what it points at)\n", sizeof *p);
    printf("(*p).tag = %c   p->tag = %c   same ? %d\n",
           (*p).tag, p->tag, (*p).tag == p->tag);
    return 0;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sizeof(struct big) = 80
sizeof p           = 8   (a pointer)
sizeof *p          = 80   (what it points at)
(*p).tag = z   p->tag = z   same ? 1

Size. Eighty bytes against eight. Passing this structure by value copies ten times as much as passing its address.

Mutation. A function that receives a structure by value receives a copy — Chapter 5.3's pass-by-value, which structures do not escape. Changing the copy changes nothing the caller can see. A function that is supposed to modify the caller's structure must receive a pointer, exactly as Chapter 7.5 established for int.

And the reason that is not about either: many structures are not copies of anything. A structure allocated with malloc is reached only through the pointer that malloc returned (Chapter 9.6), and a structure that refers to its own type — §7 below — cannot be built any other way.

Note sizeof p versus sizeof *p: the first is the pointer's size, the second the structure's. Chapter 7.3 made this distinction for int *; it matters far more here, because the two numbers differ by a factor of ten rather than a factor of two.

This chapter establishes what the pointer form can do. Choosing between by-value and by-pointer — on cost, on intent, on what the interface should promise — is Chapter 10.4's subject.

3. The Two Forms, and What -> Abbreviates

Given struct counter *p, there are two ways to reach a member:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    (*p).hits        /* dereference, then select */
    p->hits          /* identical meaning */

p->member is defined to mean exactly (*p).member. It is not an approximation and there is no case where they differ. The -> operator exists because the parenthesised form is tedious and, as §4 shows, easy to write wrongly.

The §2 output confirms the equivalence by comparison: (*p).tag and p->tag both gave z, and the test printed 1.

-> chains like . does, and the two mix freely:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    p->origin.x          /* member of a member */
    p->next->id          /* through two pointers */
    (&k)->hits           /* legal, and silly: write k.hits */

4. Why the Parenthesised Form Needs Its Parentheses

The parentheses in (*p).hits are not optional. . has higher precedence than unary *, so without them:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    *p.hits              /* means *(p.hits) — not what you want */

This one the compiler catches, and helpfully:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: member reference type 'struct s *' is a pointer; did you mean to use '->'?
error: indirection requires pointer operand ('int' invalid)

Two complaints from one mistake: p is a pointer so . is wrong on it, and even if that worked, the result is an int which cannot be dereferenced. The first message names the fix.

So *p.member is a nuisance rather than a hazard. The real precedence hazard is the increment forms, because all of them compile.

5. Three Expressions That All Compile and All Differ

Azvya Education Pvt. Ltd.VLSI Mentor
the precedence that actually bites
#include <stdio.h>

struct s { int x; };

int main(void)
{
    struct s a[3] = { {10}, {20}, {30} };

    struct s *p = a;
    int r1 = ++p->x;                 /* ++(p->x) : increments the MEMBER */
    printf("++p->x    -> value %d, a[0].x=%d, p now at index %ld\n",
           r1, a[0].x, (long)(p - a));

    struct s *q = a;
    int r2 = (++q)->x;               /* moves the POINTER, then reads */
    printf("(++q)->x  -> value %d, a[0].x=%d, q now at index %ld\n",
           r2, a[0].x, (long)(q - a));

    struct s *t = a;
    int r3 = t->x++;                 /* (t->x)++ : post-increment the member */
    printf("t->x++    -> value %d, a[0].x=%d\n", r3, a[0].x);

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
++p->x    -> value 11, a[0].x=11, p now at index 0
(++q)->x  -> value 20, a[0].x=11, q now at index 1
t->x++    -> value 11, a[0].x=12

Read it row by row. (a[0].x carries forward between rows — it is 11 on the second line because the first line incremented it, and 12 on the third for the same reason.)

++p->x incremented the member. a[0].x went from 10 to 11 and p is still at index 0. The expression parsed as ++(p->x).

(++q)->x moved the pointer. q is now at index 1 and the value read is 20 — a[1].x. Nothing was incremented except the pointer, and a[0].x is untouched by this line.

t->x++ post-incremented the member, so it yielded 11 (the old value) and left a[0].x at 12.

All three compiled with no warnings at -Wall -Wextra -Wpedantic. One fact explains every row:

-> binds tighter than unary ++, --, *, & and !. So ++p->x is ++(p->x) and *p->q is *(p->q). If you want the operator to apply to the pointer, you must say so with parentheses: (++p)->x.

The practical advice is not to memorise the table. It is this: when an increment and an arrow appear in one expression, parenthesise the one you mean — even when the default happens to be right — because the reader cannot tell a deliberate ++p->x from a mistyped (++p)->x.

6. Modifying Through a Pointer

This is the mechanical payoff, and it is Chapter 7.5's lesson with a structure in place of an int.

Azvya Education Pvt. Ltd.VLSI Mentor
by value changes nothing; through a pointer changes the caller's object
#include <stdio.h>

struct counter { int hits; char tag; };

static void bump_by_value(struct counter c)    { c.hits = c.hits + 1; }
static void bump_by_pointer(struct counter *c) { c->hits = c->hits + 1; }

/* reads only, and says so — Chapter 7.11 */
static void report(const struct counter *c)
{
    printf("  tag %c hits %d\n", c->tag, c->hits);
}

int main(void)
{
    struct counter k = { .hits = 0, .tag = 'k' };

    bump_by_value(k);
    printf("after bump_by_value:");    report(&k);

    bump_by_pointer(&k);
    printf("after bump_by_pointer:");  report(&k);

    bump_by_pointer(&k);
    bump_by_pointer(&k);
    printf("after two more:");         report(&k);

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
after bump_by_value:  tag k hits 0
after bump_by_pointer:  tag k hits 1
after two more:  tag k hits 3

bump_by_value incremented a member of its own copy, which then ceased to exist when the function returned — Chapter 9.2's automatic storage duration, doing exactly what it is supposed to. The caller's k never moved off zero. And nothing warned: the function is correct C that does nothing useful.

bump_by_pointer received an address and wrote through it, so the caller's object changed.

report takes a const struct counter *, which does two jobs at once. It is Chapter 7.11's promise not to modify, enforced:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static void f(const struct c *p) { p->h = 1; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: cannot assign to variable 'p' with const-qualified type 'const struct c *'

And it is Chapter 9.8 §7's ownership signal — a const T * parameter reads as "this function borrows and does not own". With structures the habit matters more than it did for scalars, because a structure parameter is nearly always a pointer, so const is the only thing in the signature that tells a reader whether the function is going to change their object.

Mark every structure pointer parameter const unless the function modifies through it. The signature then says what the function does, which is the only documentation that cannot drift.

7. The Shape That Needs a Pointer

Chapter 10.2 §8 said a structure cannot contain an object of its own type. Here is what that refusal looks like:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct bad { int id; struct bad inner; };
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: field has incomplete type 'struct bad'

The reason is size. To know how big struct bad is, the compiler would need to know how big inner is, which is how big struct bad is. There is no answer.

A pointer to the type has a known size — eight bytes here, whatever the structure turns out to be — so this is legal and is how C builds structures that refer to each other:

Azvya Education Pvt. Ltd.VLSI Mentor
a structure that points at its own type
#include <stdio.h>

struct stage {
    int           id;
    struct stage *next;        /* legal: a pointer has a known size */
};

int main(void)
{
    struct stage third  = { .id = 3, .next = NULL };
    struct stage second = { .id = 2, .next = &third };
    struct stage first  = { .id = 1, .next = &second };

    for (struct stage *p = &first; p != NULL; p = p->next) {
        printf("stage %d", p->id);
        printf(p->next != NULL ? " -> " : "\n");
    }

    printf("first.next->next->id = %d\n", first.next->next->id);
    printf("sizeof(struct stage) = %zu\n", sizeof(struct stage));

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
stage 1 -> stage 2 -> stage 3
first.next->next->id = 3
sizeof(struct stage) = 16

Notice that struct stage *next; appears inside the declaration of struct stage, at a point where the type is still incomplete. That is specifically allowed: a pointer to an incomplete structure type is fine, because its size does not depend on the structure's.

Three objects of type struct stage chained by their next members. The first holds id 1 and its next member points to the second. The second holds id 2 and its next points to the third. The third holds id 3 and its next is a null pointer, which ends the chain. Each arrow is one next member; traversal follows them with p equals p arrow next until p is null.firstid 1 · nextsecondid 2 · nextthirdid 3 · nextNULLthe chain endsnextnextnext12
Figure 1 — the three objects and the chain their next members form. Each arrow is one pointer member, and the NULL at the end is what makes the traversal terminable: the loop condition p != NULL is the only thing that tells the program where the chain stops, which is Chapter 7.4's null pointer being used as a deliberate sentinel rather than as a failure value. The objects themselves are three ordinary automatic variables — nothing here is allocated, and the chain is built purely out of addresses.

Two idioms in that program are worth naming, because you will see them constantly.

for (struct stage *p = &first; p != NULL; p = p->next) is the traversal. The advance step is p = p->next rather than p++ — the objects are not in an array and are not adjacent, so pointer arithmetic would be meaningless. This is the one traversal in C that is not an array walk.

first.next->next->id mixes both operators in one expression, correctly: first is an object so .; first.next is a pointer so ->; and again. Read left to right and the rule is simply dot for an object, arrow for a pointer.

8. The Rules That Still Apply

Nothing in this chapter suspends Module 7 or Module 9. Three reminders, because each has a structure-shaped version that looks new and is not.

A null structure pointer must not be dereferenced. p->hits where p is NULL is Chapter 7.4 §3's undefined behaviour. Check at the boundary, once.

A pointer to a structure whose lifetime ended is invalid. Returning &local_struct is Chapter 9.2's bug and Chapter 9.11's source 2 — the fact that the object is a structure changes nothing, and the compiler's -Wreturn-stack-address catches only some shapes of it, as Chapter 9.11 §7 measured.

An allocated structure follows Module 9 exactly. struct big *p = malloc(sizeof *p); is Chapter 9.6's idiom with no changes at all — note sizeof *p rather than sizeof(struct big), for the reason Chapter 9.6 §4 gave. It must be checked for NULL, freed exactly once, and have one documented owner.

9. What You Should Be Able to Say Now

  1. A pointer to a structure is an ordinary pointer; every rule from Module 7 applies unchanged.
  2. p->member means exactly (*p).member — verified identical.
  3. The pointer form dominates for two measurable reasons: size (80 bytes against 8) and mutation (by value changes a copy).
  4. sizeof p is the pointer's size; sizeof *p is the structure's.
  5. . binds tighter than unary *, so *p.m is wrong — and the compiler catches it with "did you mean to use '->'?".
  6. -> binds tighter than ++: measured, ++p->x incremented the member, (++q)->x moved the pointer, t->x++ post-incremented the member. All three compiled with no warnings.
  7. Parenthesise whenever an increment meets an arrow, even when the default is right.
  8. A function must take a pointer to modify the caller's structure — verified, by-value left it at zero.
  9. const struct T * is enforced ("cannot assign to variable ... with const-qualified type") and is also Chapter 9.8's borrow signal. Mark every read-only structure pointer parameter const.
  10. A structure cannot contain its own type ("field has incomplete type") but may point to it — the one shape that requires a pointer.
  11. Traversal is p = p->next, not p++: the objects are not adjacent.
  12. Dot for an object, arrow for a pointer — that is the whole rule for reading a chain.

10. Exercises

Exercise 1 — rewrite both ways. Given struct window *w, write each expression in the -> form and in the (*...) form.

  • the origin's y
  • the title's first character
  • the x of the window that w->parent points at

Exercise 2 — fix the parentheses. Each line is wrong or ambiguous. Say what it currently means and what it was probably meant to mean.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
*p.hits
++p->x
*p->next
&p->hits + 1

Exercise 3 — predict the three. Given struct s a[3] = { {5}, {6}, {7} }; and struct s *p = a;, give the value of each expression and the state of p and a afterwards. Treat each line as starting fresh.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
++p->x
(++p)->x
p++->x
(*p).x++

Exercise 4 — explain the no-op. bump_by_value in §6 is correct C and does nothing observable. Explain why using Chapter 5.3 and Chapter 9.2, and say why no warning flag reports it.

Exercise 5 — add the const. Mark each parameter const if it should be, and say what the signature then promises.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
void print_box(struct box *b);
void resize_box(struct box *b, int w);
int  area_of(struct box *b);
void copy_box(struct box *dst, struct box *src);

Exercise 6 — why it cannot work. Explain, in terms of size, why struct bad { int id; struct bad inner; }; is rejected but struct stage { int id; struct stage *next; }; is accepted — and say why the pointer member is legal even though the type is incomplete at that point.

Exercise 7 — write the traversal. Using §7's struct stage, write a function that counts the stages in a chain given a pointer to the first. State what it must do if handed NULL, and say which chapter's rule decides that.

Exercise 8 — allocate one. Write the three lines that allocate a struct stage, check the result, and set both members, using the idiom from Chapter 9.6 §4. Then say who owns it and which chapter made that your responsibility to answer.

11. Summary

  • A structure pointer is an ordinary pointer — Module 7 applies unchanged.
  • p->m is exactly (*p).m.
  • The pointer form dominates on size (80 against 8) and mutation (by value copies).
  • *p.m is caught by the compiler; the increment forms are not.
  • Measured, all with zero warnings: ++p->x → the member, (++q)->x → the pointer, t->x++ → post-increment the member.
  • -> binds tighter than ++, --, *, &. Parenthesise what you mean.
  • Only a pointer parameter lets a function modify the caller's structure.
  • const struct T * is enforced and signals a borrow (9.8).
  • A struct may point to its own type but not contain it — "field has incomplete type".
  • Traverse with p = p->next. Dot for an object, arrow for a pointer.

So the pointer form is smaller to pass and is the only form that can modify the caller's object — which reads like a verdict, and is not one. Passing by value has real advantages that this chapter never mentioned: the callee cannot damage anything, the caller needs no address, and the function's signature promises independence without needing a qualifier to say so. There is also a cost question with a genuinely surprising answer, because a small structure passed by value may never touch memory at all while the pointer version has to be followed. The next chapter is the decision rather than the mechanics: when a copy is right, when an address is right, what each one actually costs, and the one case where returning a structure by value is the clearest code you can write.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.