What Is Member Selection Through a Reference or a Pointer?

Member selection is the step that gets you from a whole struct object to one member inside it, and the member selection operator (.) is how you spell it. That operator has a single requirement: the expression on its left has to be an object of the struct type.

Everything in this lesson follows from how references and pointers meet that requirement.

  • A reference is a second name for an object that already exists. The expression on the left of the . is still a Rover, so member selection needs no new syntax.
  • A pointer is a separate object whose value is an address. The expression on the left is a Rover*, not a Rover, so . has nothing to select from. C++ supplies a second operator, the arrow, for that case.

The reference half looks exactly like ordinary member access:

#include <iostream>
#include <string>

struct Rover
{
    std::string callsign {};
    int wheels {};
    double batteryVolts {};
};

void reportStatus(const Rover& unit) // unit is a reference to a Rover
{
    std::cout << unit.callsign << ": " << unit.wheels << " wheels, "
              << unit.batteryVolts << " V" << '\n';
}

int main()
{
    Rover scout { "Kestrel", 6, 27.4 };

    Rover& crew { scout }; // crew is another name for scout
    crew.batteryVolts = 26.1; // writes straight into scout

    reportStatus(scout);

    return 0;
}

Output:

Kestrel: 6 wheels, 26.1 V

There is only one Rover in that program. crew and unit are names for it, so writing through crew changes what reportStatus later reads, and both of them select members with a plain dot.

Read the Left Operand, Then Pick the Operator

Every member access you will write comes down to one question: what is the type of the expression immediately to the left of the operator?

Expression on the left Its type Correct access Reason
scout Rover scout.wheels the object itself
crew Rover& crew.wheels a reference is that same object under another name
probe Rover* probe->wheels a pointer holds an address, so it needs the arrow
*probe Rover (*probe).wheels dereferencing has already produced the object

Only two answers appear in that table, which makes the rule short: if the left operand is a pointer, use ->, and otherwise use .. Note that the question is asked fresh at every operator in an expression, not once for the whole line. Later sections lean on that.

Each Operator Rejects the Other's Left Operand

The two operators are not interchangeable, and the compiler is blunt about it. The next program is broken: it tries to select a member straight out of a pointer.

#include <iostream>
#include <string>

struct Rover
{
    std::string callsign {};
    int wheels {};
    double batteryVolts {};
};

int main()
{
    Rover scout { "Kestrel", 6, 27.4 };
    Rover* probe { &scout }; // probe stores the address of scout

    std::cout << probe.wheels << '\n'; // broken: probe is a pointer, not a Rover

    return 0;
}
s.cpp:16:24: error: request for member 'wheels' in 'probe', which is of pointer type 'Rover*' (maybe you meant to use '->' ?)
   16 |     std::cout << probe.wheels << '\n'; // broken: probe is a pointer, not a Rover
      |                        ^~~~~~

A Rover* has no member called wheels. It has no members at all, because a pointer is just an address, so the request cannot be satisfied and the compiler names the operator it expected instead.

The mistake also runs the other way. This program is broken too: it aims the arrow at things that are not pointers.

#include <iostream>
#include <string>

struct Rover
{
    std::string callsign {};
    int wheels {};
    double batteryVolts {};
};

int main()
{
    Rover scout { "Kestrel", 6, 27.4 };
    Rover& crew { scout };

    std::cout << scout->wheels << '\n'; // broken: scout is an object
    std::cout << crew->wheels << '\n'; // broken: crew is a reference to an object

    return 0;
}
s.cpp:16:23: error: base operand of '->' has non-pointer type 'Rover'
   16 |     std::cout << scout->wheels << '\n'; // broken: scout is an object
      |                       ^~
s.cpp:17:22: error: base operand of '->' has non-pointer type 'Rover'
   17 |     std::cout << crew->wheels << '\n'; // broken: crew is a reference to an object
      |                      ^~

Both lines fail with the same complaint, and the reference line reports its type as Rover, not as a reference to one. The arrow is for pointers and nothing else.

Two Spellings of the Same Access

Because a pointer stores an address, reaching a member through one means visiting the object at that address first. The indirection operator (*) does that, and after it has run, an ordinary dot finishes the job:

#include <iostream>
#include <string>

struct Rover
{
    std::string callsign {};
    int wheels {};
    double batteryVolts {};
};

int main()
{
    Rover scout { "Kestrel", 6, 27.4 };
    Rover* probe { &scout };

    std::cout << (*probe).wheels << '\n'; // dereference first, then select
    std::cout << probe->wheels << '\n'; // same access, written the usual way

    probe->batteryVolts = 25.8; // the arrow works on the left of an assignment too
    std::cout << scout.batteryVolts << '\n';

    return 0;
}

Output:

6
6
25.8

The member selection from pointer operator (->), usually called the arrow operator, is defined as exactly that pair of steps: it dereferences its left operand and then selects a member from the result. probe->wheels is not merely similar to (*probe).wheels, nor is it faster: the two are the same operation, one of them written with tidier punctuation. Both forms yield the member itself, so either can appear on the left of an assignment, as the write to batteryVolts shows.

Best Practice
Reach for `->` whenever the left operand is a pointer. Save the `(*probe).wheels` form for explaining what the arrow does, and keep it out of working code.
Warning
The arrow dereferences, so it carries every risk that `*` does. Using `->` on a null pointer, or on one whose object has already gone away, is undefined behavior. The tidier syntax does not make the pointer any safer, so it still needs to point at a live object before you follow it.

Why the Parentheses in (*probe).wheels Are Not Optional

. and -> are postfix operators, and they bind more tightly than the unary *. Whenever both appear without parentheses, member selection happens first.

That is easiest to see where it is the behavior you want. The struct below stores a pointer as a member, so the dot has to run before the dereference for the expression to mean anything:

#include <iostream>

struct Beacon
{
    int* pulseCount {}; // a member that happens to be a pointer
};

int main()
{
    int pulses { 9 };
    Beacon marker { &pulses };

    // marker is an object, so . runs first and * acts on what it produces
    std::cout << *marker.pulseCount << '\n';

    return 0;
}

Output:

9

*marker.pulseCount reads as *(marker.pulseCount): fetch the member, then follow it. Apply that same grouping to a pointer on the left and the result is the error from earlier in the lesson. This program is broken:

#include <iostream>
#include <string>

struct Rover
{
    std::string callsign {};
    int wheels {};
    double batteryVolts {};
};

int main()
{
    Rover scout { "Kestrel", 6, 27.4 };
    Rover* probe { &scout };

    std::cout << *probe.wheels << '\n'; // broken: this is *(probe.wheels)

    return 0;
}
s.cpp:16:25: error: request for member 'wheels' in 'probe', which is of pointer type 'Rover*' (maybe you meant to use '->' ?)
   16 |     std::cout << *probe.wheels << '\n'; // broken: this is *(probe.wheels)
      |                         ^~~~~~

The * never gets a turn. The parentheses in (*probe).wheels exist purely to reverse that grouping and force the dereference to happen first. The arrow operator sidesteps the whole question by building both steps into one operator, which is the practical reason to prefer it.

Walking a Chain of Pointers

When the member you select is itself a pointer, the answer to "what is the type on the left?" is a pointer again, so the next operator is another arrow. Structs that link to one another are the usual place this shows up:

#include <iostream>

struct Waypoint
{
    double bearing {};
    int rangeMetres {};
    Waypoint* next {}; // the leg that follows this one, or nullptr at the end
};

int main()
{
    Waypoint crater { 275.0, 40, nullptr };
    Waypoint ridge { 190.5, 25, &crater };
    Waypoint start { 88.25, 12, &ridge };

    Waypoint* route { &start };

    std::cout << route->bearing << '\n'; // first leg
    std::cout << route->next->bearing << '\n'; // second leg
    std::cout << route->next->next->bearing << '\n'; // third leg

    // the third leg again, written without the arrow operator
    std::cout << (*(*(*route).next).next).bearing << '\n';

    return 0;
}

Output:

88.25
190.5
275
275

Read route->next->next->bearing from the left: route is a Waypoint*, so the first arrow lands on start; start.next is a Waypoint*, so the second arrow lands on ridge; ridge.next is a Waypoint*, so the third lands on crater, and bearing is a double that ends the chain. The final line of the program produces the same value through nested dereferences, and comparing the two is a fair summary of why the arrow exists.

Chains like this are usually walked in a loop rather than written out link by link, because the program does not know in advance how many links there are:

#include <iostream>

struct Waypoint
{
    double bearing {};
    int rangeMetres {};
    Waypoint* next {};
};

int main()
{
    Waypoint crater { 275.0, 40, nullptr };
    Waypoint ridge { 190.5, 25, &crater };
    Waypoint start { 88.25, 12, &ridge };

    int totalRange { 0 };

    // leg walks the chain: read through it with ->, then move it to the next link
    for (const Waypoint* leg { &start }; leg != nullptr; leg = leg->next)
    {
        std::cout << "leg bearing " << leg->bearing << ", range " << leg->rangeMetres << '\n';
        totalRange += leg->rangeMetres;
    }

    std::cout << "total range " << totalRange << '\n';

    return 0;
}

Output:

leg bearing 88.25, range 12
leg bearing 190.5, range 25
leg bearing 275, range 40
total range 77

The loop condition is doing real work here. crater.next is null, so the last arrow is never attempted, which is what keeps the walk inside live objects.

When Some Members Are Pointers and Some Are Not

A struct can hold some members by value and others by pointer, and then a single expression uses both operators. Ask the question again at each step and the answer falls out:

#include <iostream>
#include <string>

struct Antenna
{
    int gainDb {};
};

struct Rover
{
    std::string callsign {};
    int wheels {};
    double batteryVolts {};
};

struct Lander
{
    Antenna dish {}; // a member held by value
    Rover* pairedRover {}; // a member held by pointer
};

int main()
{
    Rover scout { "Kestrel", 6, 27.4 };
    Lander deck { { 34 }, &scout };

    Lander* site { &deck };
    const Lander& crew { deck };

    std::cout << deck.dish.gainDb << '\n'; // object, then value member
    std::cout << site->dish.gainDb << '\n'; // pointer, then value member
    std::cout << crew.pairedRover->wheels << '\n'; // reference, then pointer member
    std::cout << site->pairedRover->callsign << '\n'; // pointer, then pointer member

    return 0;
}

Output:

34
34
6
Kestrel

Each line picks its operators independently:

Expression First operator Second operator
deck.dish.gainDb . because deck is a Lander . because dish is an Antenna
site->dish.gainDb -> because site is a Lander* . because dish is an Antenna
crew.pairedRover->wheels . because crew is a const Lander& -> because pairedRover is a Rover*
site->pairedRover->callsign -> because site is a Lander* -> because pairedRover is a Rover*

Notice the third row. Reaching a struct through a reference does not change what its members are, so a pointer member behind a reference is still reached with an arrow.

Both operators have the same precedence and group left to right, so site->dish.gainDb already means what you want and needs no parentheses. Writing (site->dish).gainDb changes nothing about the result and is worth adding only if it makes a dense line easier to scan.

Summary

  • The member selection operator (.) requires an object of the struct type on its left. References satisfy that requirement because a reference is the object under another name, so references use . throughout.
  • A pointer does not satisfy it. probe.wheels is a compile error, because a pointer holds an address and has no members of its own.
  • The member selection from pointer operator (->) dereferences its left operand and then selects a member, so probe->wheels and (*probe).wheels are the same operation. The arrow is the form to write.
  • The arrow only accepts a pointer. Aiming it at an object or a reference is a compile error, just as aiming a dot at a pointer is.
  • (*probe).wheels needs its parentheses because . binds more tightly than *. Without them the expression groups as *(probe.wheels), which asks the pointer for a member.
  • When the member you selected is itself a pointer, use another arrow: route->next->next->bearing. Mixed structs mix the operators, and each operator is chosen from the type of the expression immediately to its left.
  • Following a null or dangling pointer with -> is undefined behavior, exactly as it is with *. Chain-walking loops normally test for nullptr before they follow the next link.