Accessing Members Indirectly
Access struct members through pointers using the arrow operator (->).
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 aRover, 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 aRover, 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.
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.
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.wheelsis 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, soprobe->wheelsand(*probe).wheelsare 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).wheelsneeds 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 fornullptrbefore they follow the next link.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Accessing Members Indirectly - Quiz
Test your understanding of the lesson.
Practice Exercises
Database Record Access
Work with pointers to structs using the arrow operator. Implement functions that access struct members through pointers and references.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!