Hi everyone ✋
In the previous post we sorted out NULL vs nullptr ─ how to say “this pointer leads nowhere” in a clean, type-safe way. Along the way we leaned on a little picture of address slips and a mailroom 📬.
Today we pick that picture right back up ─ because now we finally arrive at the house that address slip points to 🏠.
If we haven’t read the earlier posts in this series, it’s worth going through them first ─ today we combine pointers with struct and class objects, so the intro post and the dynamic memory post will feel especially handy.
Today’s topic is member access through the arrow operator ->. Once we hold a pointer to an object ─ a struct or a class ─ how do we reach the members inside it? If you’ve ever spotted that little arrow -> in real C++ code and wondered what it does, this is the post for it. By the end it’ll feel completely natural 😉
Let’s take a deep dive 🦴
A quick reminder ─ objects and their members 🧱
A struct or a class lets us bundle related things together into a single object. Those things ─ the variables and functions that live inside ─ are its members.
When we’re holding the object itself, we reach a member with the humble dot operator . ─
#include <iostream>
using namespace std;
struct Point
{
int x;
int y;
};
int main()
{
Point p = { 3, 7 };
cout << "x = " << p.x << ", y = " << p.y << endl; // dot on the object
return 0;
}
The output will be ─
x = 3, y = 7
Nothing new here ─ p.x and p.y reach straight into the object p. But everything in this series has been about pointers, so let’s ask the obvious question 👇
The problem ─ what if we only have a pointer? 🤔
Very often we don’t hold the object directly ─ we hold a pointer to it. This happens constantly: any object created with new comes back as a pointer, functions frequently pass objects around by pointer, and data structures like linked lists are built entirely from pointers to objects.
So the question is ─ if ptr points at a Point, how do we reach the x inside it? Writing ptr.x won’t do, because ptr isn’t a Point ─ it’s a pointer holding the address of a Point. We need a way to step from the address to the members it leads to.
Let’s picture it before we write it.
My little reference ─ the house and its rooms 🏠
Here’s the picture to keep in mind for the whole post, and it grows straight out of last week’s mailroom.
Think of an object as a house, and its members as the rooms inside ─ a Point house has an x room and a y room; a Student house has a name room, a marks room, and even a little workshop (a member function). The pointer is the address slip from last post ─ it doesn’t contain the house, it just tells us where to find it.
Now, reaching a room depends on what we’re holding ─
- If we’re standing inside the house (we have the object itself), we simply walk to the room ─ that’s the dot
.. - If we only hold the address slip (a pointer), we must first travel to the house, then step into the room.
Travelling to the house is exactly what dereferencing does ─ *ptr takes us from the slip to the house. Then a dot steps into a room. And the arrow -> is simply a shortcut that does both in one smooth motion ─ follow the address, walk inside, and reach the room. The little arrow even looks like it’s pointing from our slip toward the room 🏠➡️.
Let’s see both the long way and the shortcut.
The clunky way ─ (*ptr).member
Since travelling to the house is *ptr, and stepping into a room is .member, we can just combine them ─ dereference first, then dot ─
Point p = { 3, 7 };
Point *ptr = &p;
cout << (*ptr).x << endl; // travel to the house (*ptr), then enter room x
Those parentheses around *ptr are not optional ⚠️. They’re there because of operator precedence ─ the dot . binds tighter than the dereference *. So if we wrote it without the parentheses ─
cout << *ptr.x; // ❌ ERROR
the compiler reads it as *(ptr.x) ─ it tries to find a room x on the slip itself before travelling anywhere, which makes no sense. GCC even nudges us in the right direction ─
error: request for member 'x' in 'ptr', which is of pointer type 'Point*' (maybe you meant to use '->' ?)
That parenthesised (*ptr).x works perfectly, but honestly ─ it’s noisy. Typing it dozens of times, especially when chaining, gets old fast. C++ knew this, and gave us something better 🥳
The arrow operator ─ ptr->member 🎯
The arrow operator -> is pure shorthand. These two lines mean exactly the same thing ─
(*ptr).x // the long way ptr->x // the short way ─ identical, and much nicer
ptr->member is defined to be (*ptr).member. That’s the whole story. Follow the address, step inside, reach the room ─ all in one clean little arrow. So whenever we’re holding a pointer to an object, -> is the tool we reach for.
Let’s rewrite our Point example with it ─
#include <iostream>
using namespace std;
struct Point
{
int x;
int y;
};
int main()
{
Point p = { 3, 7 };
Point *ptr = &p;
cout << "x = " << ptr->x << endl; // ptr->x is (*ptr).x
cout << "y = " << ptr->y << endl;
return 0;
}
The output will be ─
x = 3 y = 7
Clean, readable, and it says exactly what we mean ─ “reach into the object this points at, and grab x.”
A class through a pointer 🎓
Everything so far used a struct, but the arrow works identically on a class, and this is where it gets more interesting ─ because a class usually has member functions too, and -> reaches those just as happily as it reaches data.
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
string name;
int marks;
void display()
{
cout << name << " scored " << marks << endl;
}
};
int main()
{
Student s;
s.name = "Imran";
s.marks = 95;
Student *ptr = &s;
ptr->name = "Imran Shah"; // reach a DATA member through the pointer
ptr->display(); // call a MEMBER FUNCTION through the pointer
return 0;
}
The output will be ─
Imran Shah scored 95
Look at those two arrow lines ─ ptr->name reads and writes a data member, and ptr->display() calls a method. Same arrow, whether the room holds a value or a whole little workshop 🛠️. That’s all there is to it: . when we hold the object, -> when we hold a pointer to it. Struct or class, data or function ─ the rule never changes.
Where -> truly shines ─ objects on the heap 🔥
We can go our whole lives using . on local objects ─ so why does -> matter so much in real code? Because of one word from the dynamic memory post ─ new.
When we create an object on the heap, new doesn’t hand us the object ─ it hands us a pointer to it. From that moment on, the only way to touch the object is through that pointer, and -> becomes our everyday tool.
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
string name;
int marks;
void display() { cout << name << " scored " << marks << endl; }
};
int main()
{
Student *s = new Student; // new gives us a POINTER, not the object
s->name = "Ada";
s->marks = 100;
s->display();
delete s; // done with the house ─ tear it down
return 0;
}
The output will be ─
Ada scored 100
This is the pattern we’ll see everywhere in real C++ ─ objects created with new, used through ->, and released with delete. In fact, the arrow reaches well beyond raw pointers ─ which is exactly where we’re headed next 🧵.
-> is everywhere ─ smart pointers and iterators 🧵
Here’s something that surprises people ─ the arrow isn’t just for raw pointers. Some of the most-used tools in modern C++ look like objects but behave like pointers, and they all speak the same arrow language.
Take a smart pointer from the smart pointers post. It manages a heap object for us and frees it automatically ─ but when we want to reach the object inside, we use ->, exactly as if it were a raw pointer ─
#include <iostream>
#include <string>
#include <memory>
#include <map>
using namespace std;
struct Student
{
string name;
int marks;
void display() { cout << name << " scored " << marks << endl; }
};
int main()
{
// a smart pointer ─ same arrow, but it cleans up after itself
unique_ptr<Student> s = make_unique<Student>();
s->name = "Grace";
s->marks = 99;
s->display();
// STL iterators use -> too ─ here it reaches into a map's key/value pair
map<string, int> scores = { { "Grace", 99 }, { "Alan", 88 } };
auto it = scores.find("Alan");
if (it != scores.end())
cout << it->first << " scored " << it->second << endl;
return 0;
}
The output will be ─
Grace scored 99 Alan scored 88
Notice it->first and it->second ─ if we’ve read the map and pair posts, that pattern is an old friend. A map iterator behaves like a pointer to a pair, so the arrow reaches its first and second just as it reaches any member.
How can a smart pointer or an iterator ─ which are really objects ─ use ->? Because -> is an overloadable operator: a type can define what its own arrow does. That’s the quiet trick behind smart pointers and iterators. We don’t need to write our own today, but it’s lovely to know the arrow we learned for raw pointers is the same arrow powering so much of the standard library 🥳
Chaining arrows ─ reaching deeper 🔗
Here’s where the arrow really earns its keep. Sometimes a member is itself a pointer to another object ─ this is the beating heart of structures like linked lists. When that happens, we can chain arrows to hop from object to object.
#include <iostream>
using namespace std;
struct Node
{
int data;
Node *next; // a pointer to the next Node
};
int main()
{
Node third = { 30, nullptr };
Node second = { 20, &third };
Node first = { 10, &second };
Node *head = &first;
cout << head->data << endl; // this node's data
cout << head->next->data << endl; // the next node's data
cout << head->next->next->data << endl; // and the one after that
return 0;
}
The output will be ─
10 20 30
Read head->next->next->data left to right like a little journey ─ start at head, follow next to the second house, follow next again to the third, then step into its data room. Each arrow is one hop down the chain. Notice third.next is nullptr ─ which is our gentle reminder that chaining only works while the addresses actually lead somewhere 👀.
A real problem ─ walking a linked list 🚶
Let’s put the arrow to work on the problem it was practically born for ─ walking a linked list. The static three-node chain above was a warm-up; real lists are built on the heap and traversed with a loop, and -> is the engine of both.
The idea is simple ─ each node holds some data and a pointer to the next node, and the last node points at nullptr to mark the end. We build the chain with new, then walk it by hopping along next until we fall off the end.
#include <iostream>
using namespace std;
struct Node
{
int data;
Node *next;
};
int main()
{
// build three nodes on the heap and link them together
Node *head = new Node{ 10, nullptr };
head->next = new Node{ 20, nullptr };
head->next->next = new Node{ 30, nullptr };
// walk the list until we reach the end (nullptr)
Node *current = head;
while (current != nullptr)
{
cout << current->data << " ";
current = current->next; // hop to the next node
}
cout << endl;
// free every node so we don't leak memory
while (head != nullptr)
{
Node *doomed = head;
head = head->next; // remember the next before we delete
delete doomed;
}
return 0;
}
The output will be ─
10 20 30
This tiny loop is the beating heart of every linked structure in C++. Look at the two arrows doing all the work ─ current->data reads the value inside the current node, and current = current->next re-aims our pointer at the next node. The while (current != nullptr) guard is what safely stops us at the end ─ the exact nullptr check from the last post, now earning its keep 🥳. Notice we also had to be careful when deleting ─ we grab head->next before we delete, because reaching through a node we’ve already destroyed would be a disaster (more on that in a moment ☠️).
Reaching objects through functions 🔁
The arrow shows up constantly at function boundaries too, and in two directions.
First, when we pass an object by pointer into a function, that function uses -> to reach back into our original object ─ just like the pass-by-pointer idea from the pointers and functions post. Second, a function can return a pointer to an object, and we can fire -> straight at whatever comes back.
#include <iostream>
#include <string>
using namespace std;
struct Student
{
string name;
int marks;
};
// takes a pointer ─ modifies the caller's real object through ->
void awardBonus(Student *s, int bonus)
{
s->marks += bonus;
}
// returns a pointer to whichever student scored higher
Student* topper(Student *a, Student *b)
{
return (a->marks >= b->marks) ? a : b;
}
int main()
{
Student alice = { "Alice", 82 };
Student bob = { "Bob", 90 };
awardBonus(&alice, 10); // alice's marks climb to 92
// call -> directly on the pointer the function hands back
cout << topper(&alice, &bob)->name
<< " leads with "
<< topper(&alice, &bob)->marks << endl;
return 0;
}
The output will be ─
Alice leads with 92
Inside awardBonus, s->marks += bonus reaches through the pointer and changes the real alice back in main ─ that’s the whole point of passing by pointer. And on the last line, topper(&alice, &bob)->name shows off something neat ─ we don’t need to store the returned pointer in a variable first; we can aim the arrow right at the function’s result. The arrow works on any expression that gives us a pointer to an object 👀.
-> meets const and nullptr 🛡️
The arrow doesn’t live in isolation ─ it carries along everything we’ve learned about pointers.
From the const pointers post ─ if our pointer is a pointer-to-const, the arrow lets us read members but not change them ─
Point p = { 3, 7 };
const Point *ptr = &p;
cout << ptr->x; // ✅ reading through the arrow is fine
// ptr->x = 99; // ❌ ERROR ─ can't modify through a pointer-to-const
The buttons are locked, just like our TV remote from that post ─ we can look into the rooms, but not rearrange the furniture.
And from the last post, the most important caution of all ⚠️ ─ using -> on a null pointer is undefined behaviour, because we’d be following an address that leads nowhere. ptr->member dereferences ptr, so a null ptr means a null dereference ☠️.
Student *ptr = nullptr; // ptr->display(); // ☠️ undefined behaviour ─ following a "no address" slip if (ptr) ptr->display(); // ✅ guard first, then follow
So before you send the courier down an address, make sure the slip actually points at a house. Guard with if (ptr) and the arrow stays perfectly safe.
A real-life bug ─ the dangling arrow ☠️
Here’s a mistake that bites almost every C++ beginner at least once, and the arrow sits right at the scene of the crime.
We saw that -> on a null pointer is trouble. But there’s a sneakier cousin ─ using -> on a pointer whose object has already been deleted. After delete, the pointer still holds the old address, but the house at that address has been torn down. The pointer is now a dangling pointer, and following it is undefined behaviour ─ a use-after-free bug.
Student *s = new Student; s->marks = 100; delete s; // the house is demolished... // s->marks = 50; // ☠️ use-after-free ─ the slip still points at rubble
The tragic part is that s->marks = 50; often looks like it works ─ the memory might not be reused immediately, so it may run without crashing today and blow up mysteriously next week. That’s the worst kind of bug 😱.
The fix is a simple, disciplined habit ─ after we delete a pointer, set it to nullptr ─
delete s; s = nullptr; // now the slip honestly says "no address" if (s) s->marks = 50; // the guard safely skips it ─ no crash
Once s is nullptr, our familiar if (s) guard can catch it. This is also exactly the headache that smart pointers were designed to erase ─ they delete the object and stop us from reaching a dangling one. Between the nullptr habit and smart pointers, the dangling arrow is very avoidable 🛡️.
Gotchas to keep us safe ⚠️
Let’s gather every tripwire in one place ─
.for objects,->for pointers. If we hold the object (or a reference), use the dot. If we hold a pointer to it, use the arrow. Mixing them up is the single most common beginner slip.ptr->memberis(*ptr).member. The arrow is exact shorthand ─ nothing magic, just tidier.- Mind the precedence in the long form.
*ptr.memberis read as*(ptr.member)and won’t compile. It must be(*ptr).member─ which is exactly why->exists. - The arrow works the same for struct and class, data and functions.
ptr->valueandptr->doSomething()follow the identical rule. ->on a null pointer is undefined behaviour. The arrow dereferences, so guard withif (ptr)before following it ☠️.->on a dangling pointer is just as deadly. Afterdelete, set the pointer tonullptrso a guard can catch it ─ reaching through freed memory is a use-after-free bug.- The arrow works on any expression that yields a pointer ─ including a function’s return value, as in
topper(&a, &b)->name. - Smart pointers and STL iterators use
->too, because->is an overloadable operator ─it->secondon a map iterator is the very same arrow. ->respectsconst. Through a pointer-to-const we can read members but not modify them ─ the compiler enforces it.
Keep these in mind and the arrow becomes second nature 🥳
So what should we remember? 🤔
Let’s wrap up the key takeaways ─
- Members of an object are reached with the dot
.when we hold the object, and the arrow->when we hold a pointer to it. - Picture the house and its rooms 🏠 ─ the object is a house, its members are rooms, the pointer is an address slip. The dot walks into a room we’re already standing beside; the arrow follows the address and steps inside, all in one motion.
ptr->memberis exactly(*ptr).member. It dereferences the pointer, then accesses the member ─ just cleaner to write and read.- In the long form, parentheses are mandatory ─
(*ptr).member, never*ptr.member, because.binds tighter than*. - The arrow works identically on
structandclass, and reaches data members and member functions the same way. - It’s the everyday tool for heap objects ─
newreturns a pointer, so we use the object through->and release it withdelete(and smart pointers use->too). - We can chain arrows ─
head->next->next->datahops from object to object, the backbone of linked structures. - Walking a linked list is the classic real use ─ build nodes with
new, then loop withcurrent = current->nextuntilnullptr, deleting as we go. - The arrow works at function boundaries in both directions ─ a function reaches into an object passed by pointer (
s->marks), and we can aim->straight at a pointer a function returns (topper(&a, &b)->name). - It’s not just for raw pointers ─ smart pointers and STL iterators use
->as well (it->second), because the arrow is an overloadable operator. - The arrow carries our other rules ─ a pointer-to-const allows reading but not writing through
->;->on a null pointer is undefined behaviour; and->on a dangling pointer (used afterdelete) is a use-after-free bug. Guard withif (ptr), and null out pointers after deleting ☠️.
Once this clicks, -> stops looking like cryptic punctuation and starts reading like a tiny set of directions ─ “go to this address, step inside, and grab that.” 👀✨
Congratulations 🥳🥳🥳
We now know how to reach into objects through a pointer ─ on the stack, on the heap, and all the way down a chain.
In the next post we’ll meet the this pointer ─ the hidden pointer every object secretly carries to itself, quietly working behind the scenes inside every member function we write 😉
Happy Coding 💻 🎵