Hi everyone ✋
We’ve spent this whole series getting to know pointers from every angle. In the previous post we finished mastering smart pointers ─ cycles, weak_ptr, custom deleters, and ownership. Today we reach what is, for many people, the single most powerful thing pointers do in C++ ─ they make polymorphism possible 🎭.
If we haven’t read the earlier posts in this series, it’s worth going through them first ─ today builds on the arrow operator, this pointer, and smart pointers posts especially.
Today’s topic is base and derived pointers ─ polymorphism through pointers. We’ll point a base-class pointer at a derived object, watch a single function call do different things depending on the real object behind it, peek at the clever machinery that makes it work, and learn the two safety rules ─ virtual destructors and avoiding slicing ─ that keep it all correct. This is where object-oriented C++ truly comes alive 🥳.
Let’s take a deep dive 🦴
A quick recap ─ base and derived 🧱
C++ lets one class inherit from another. The base class holds what’s common, and a derived class extends it with extras ─ the classic “is-a” relationship. A Restaurant is a Building; a Library is a Building. Each derived type is a Building plus its own special parts.
That “is-a” link is the key that unlocks today’s magic ─ because if a Restaurant really is a Building, then surely a pointer to a Building should be able to point at one 👇.
A base pointer can hold a derived object ⬆️
Here’s the foundation of everything today ─ a base-class pointer can point at a derived object.
Restaurant r; Building *b = &r; // a Building pointer aimed at a Restaurant ✅
This is perfectly legal, and it’s called upcasting ─ viewing a derived object through a base-class pointer. The pointer’s type is Building*, but the object it points at is really a Restaurant. That mismatch ─ base-typed pointer, derived object ─ is the whole game. The interesting question is ─ when we call a function through b, which version runs? The Building one, or the Restaurant one? Let’s build a picture, then find out.
My little reference ─ one blueprint, many buildings 🏙️
Let’s keep our houses, and zoom out to a whole town 🏙️. Think of the base class as a general blueprint ─ “Building.” Every specific kind ─ a Restaurant, a Library, a Clinic ─ is a specialised building raised from that shared blueprint, each with its own extra rooms and its own way of doing things.
Now, a base-class pointer is a slip that just says “there’s a building at this address.” It deliberately doesn’t record which kind of building ─ only that it’s some building.
Every building can open up for the day, but each does it differently ─ a restaurant fires up its ovens, a library unlocks its reading rooms. So here’s the question that decides everything ─ when we walk up with our generic “a building” slip and tell it to open up, does it run the generic opening routine, or does the actual building run its own?
The answer depends on one keyword, virtual, and it’s the difference between a slip that only knows “a building” and one that reaches the real building’s own procedures directory. Let’s see both.
The surprise ─ without virtual 😯
Let’s try it with plain, ordinary functions first ─ no virtual anywhere.
#include <iostream>
using namespace std;
struct Building
{
void openUp() { cout << "A building opens its doors" << endl; }
};
struct Restaurant : Building
{
void openUp() { cout << "The restaurant fires up its ovens" << endl; }
};
int main()
{
Restaurant r;
Building *b = &r; // Building pointer, Restaurant object
b->openUp(); // which one runs?
return 0;
}
The output will be ─
A building opens its doors
That catches almost everyone off guard 😯. You’d expect the Restaurant version ─ but even though b points at a Restaurant, the Building version ran. Why? Because without virtual, C++ decides which function to call based on the pointer’s type (Building*) at compile time ─ it never looks at the real object. The slip only says “a building,” so the generic routine runs, and the restaurant’s ovens stay cold. That’s almost never what we want.
virtual ─ dispatch to the real type 🎯
The fix is one keyword. Mark the base function virtual, and C++ switches to deciding at runtime, based on the actual object ─ this is dynamic dispatch, the heart of polymorphism.
#include <iostream>
using namespace std;
struct Building
{
virtual void openUp() { cout << "A building opens its doors" << endl; }
};
struct Restaurant : Building
{
void openUp() override { cout << "The restaurant fires up its ovens" << endl; }
};
int main()
{
Restaurant r;
Building *b = &r;
b->openUp(); // now it dispatches to the REAL type
return 0;
}
The output will be ─
The restaurant fires up its ovens
That’s the magic 🎯. The same line, b->openUp(), now runs the Restaurant version, because the actual object is a Restaurant. Notice the override keyword on the derived function ─ it’s optional but wise: it asks the compiler to double-check that we really are overriding a base virtual, catching typos in the signature. So the rule is simple ─ mark a function virtual in the base, and calls through a base pointer follow the real object.
How it works ─ a peek at the vtable 🔎
How does a Building* know to run Restaurant::openUp? The mechanism is beautiful, and ─ fittingly for this series ─ it’s pointers all the way down 🧵.
When a class has any virtual functions, the compiler builds a hidden table for it called a vtable (virtual table) ─ essentially an array of function pointers, one per virtual function, holding the addresses of that type’s versions. Restaurant‘s vtable points openUp at Restaurant::openUp; Library‘s points it at Library::openUp. Then every object of such a class secretly carries one extra hidden pointer ─ the vptr ─ aimed at its own type’s vtable.
So b->openUp() really means “follow b to the object, read its hidden vptr to find its vtable, look up openUp there, and call whatever it points at.” That’s our procedures directory from the analogy, made real ─ each building carries a directory of its own procedures, and the base pointer simply consults it. The whole feature is built from the very function pointers and member access we studied earlier in the series 🥳.
The real payoff ─ one loop, many behaviours 🏙️
Here’s why all this matters. Because a Building* can point at any kind of building, we can keep a whole mixed collection of them together and treat them uniformly ─ yet each still behaves as its true self. This is the superpower polymorphism gives us.
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
struct Building
{
virtual void openUp() { cout << "A building opens its doors" << endl; }
virtual ~Building() = default; // a virtual destructor ─ more on this next
};
struct Restaurant : Building
{
void openUp() override { cout << "The restaurant fires up its ovens" << endl; }
};
struct Library : Building
{
void openUp() override { cout << "The library unlocks its reading rooms" << endl; }
};
int main()
{
vector<unique_ptr<Building>> town;
town.push_back(make_unique<Restaurant>());
town.push_back(make_unique<Library>());
town.push_back(make_unique<Building>());
for (auto& b : town)
b->openUp(); // each one opens up its own way
return 0;
}
The output will be ─
The restaurant fires up its ovens The library unlocks its reading rooms A building opens its doors
Look at that loop ─ one line, b->openUp(), produced three different behaviours 🏙️. We stored a restaurant, a library, and a plain building all together as Buildings (owned safely by unique_ptr, straight from the smart-pointers posts), and each ran its own version. This is the beating heart of real C++ design ─ a common interface, many implementations, all handled through base pointers. Adding a new kind of building later doesn’t change this loop at all.
Virtual destructors ─ tearing down cleanly 🧹
Now the safety rule that flows straight from our cleanup story ─ and it’s the reason that virtual ~Building() appeared above. When we delete a derived object through a base pointer, the destructor must be virtual, or the derived part is never cleaned up.
Watch what happens without a virtual destructor ─
#include <iostream>
using namespace std;
struct Building
{
~Building() { cout << "Building torn down" << endl; } // NOT virtual
};
struct Restaurant : Building
{
~Restaurant() { cout << "Restaurant kitchen dismantled" << endl; }
};
int main()
{
Building *b = new Restaurant;
delete b; // deleting a Restaurant through a Building*
return 0;
}
The output will be ─
Building torn down
The Restaurant‘s destructor never ran 😱 ─ only the Building part was torn down, and the restaurant’s kitchen (any resources the derived part owned) leaked. Formally, deleting a derived object through a non-virtual base destructor is undefined behaviour. The fix is one word ─ make the base destructor virtual ─
struct Building
{
virtual ~Building() { cout << "Building torn down" << endl; }
};
With that change, the output becomes ─
Restaurant kitchen dismantled Building torn down
Now both destructors run, in the right order ─ derived first, then base ─ and everything is cleaned up. The rule is iron-clad ─ if a class has any virtual function (or is ever deleted through a base pointer), give it a virtual destructor. In our picture, demolishing a building through the generic slip must still tear down its specialised rooms, not just the shell 🧹.
Object slicing ─ why we use pointers and references ✂️
Here’s the flip side, and it explains why polymorphism lives on pointers and references rather than plain values. If we copy a derived object into a base by value, the derived part is quietly sliced off.
#include <iostream>
using namespace std;
struct Building
{
virtual void openUp() { cout << "A building opens its doors" << endl; }
};
struct Restaurant : Building
{
void openUp() override { cout << "The restaurant fires up its ovens" << endl; }
};
int main()
{
Restaurant r;
Building b = r; // ☠️ SLICING ─ copy by value keeps only the Building part
b.openUp(); // runs Building's version ─ the "restaurant" is gone
Building &ref = r; // a reference keeps the whole object
ref.openUp(); // dispatches to Restaurant
return 0;
}
The output will be ─
A building opens its doors The restaurant fires up its ovens
When we wrote Building b = r;, we asked for a plain Building, so C++ copied only the building-shaped part and left the restaurant’s kitchen behind ─ the object was sliced. Calling openUp() on it runs the base version, because there’s no restaurant left to dispatch to. But the reference ref still refers to the whole Restaurant, so it dispatches correctly. This is the core reason polymorphism is always done through pointers or references ─ they refer to the real, complete object, while copying into a base value throws the derived part away ✂️.
dynamic_cast ─ “which kind is it, really?” 🔍
Sometimes we hold a Building* and need to ask ─ “is this actually a Restaurant?” ─ so we can use restaurant-only features. That’s what dynamic_cast is for. It safely converts a base pointer to a derived pointer if the object really is that type, and gives back nullptr if it isn’t.
#include <iostream>
using namespace std;
struct Building { virtual ~Building() = default; };
struct Restaurant : Building { void serveFood() { cout << "Serving food" << endl; } };
struct Library : Building {};
void tryServe(Building *b)
{
// is this building really a Restaurant?
if (Restaurant *r = dynamic_cast<Restaurant*>(b))
r->serveFood(); // yes ─ safe to use Restaurant features
else
cout << "Not a restaurant ─ can't serve food" << endl;
}
int main()
{
Restaurant rest;
Library lib;
tryServe(&rest); // it's a Restaurant
tryServe(&lib); // it's not
return 0;
}
The output will be ─
Serving food Not a restaurant ─ can't serve food
dynamic_cast<Restaurant*>(b) handed back a real Restaurant* for the restaurant, and nullptr for the library ─ which our if neatly catches. This is the safe way to recover a derived type, checked at runtime; it only works on polymorphic types (ones with a virtual function). Its reckless cousin static_cast would perform the conversion with no check, so whenever you’re unsure of the real type, reach for dynamic_cast 🔍.
Abstract base classes ─ a pure interface 📜
One last idea completes the picture. Often the base is a pure concept that should never be built on its own ─ there’s no such thing as a generic “building,” only real kinds. We can express that by making a function pure virtual, with = 0 and no body ─
struct Building
{
virtual void openUp() = 0; // pure virtual ─ no implementation
virtual ~Building() = default;
};
A class with a pure virtual function is abstract ─ it defines an interface every derived type must fulfil, and it cannot be instantiated on its own. Try to create one directly and the compiler stops us ─
error: cannot declare variable 'b' to be of abstract type 'Building'
We can still use Building* and Building& freely ─ that’s the point ─ but every real object must be a concrete derived type that implements openUp. This is how C++ expresses “here is the contract; go implement it,” and it’s the foundation of clean, extensible design 📜.
Gotchas to keep us safe ⚠️
Let’s gather every tripwire in one place ─
- Without
virtual, calls bind to the pointer’s type.b->openUp()runs the base version unless the function is virtual ─ mark base functionsvirtualfor polymorphism. - Always give a polymorphic base a virtual destructor. Deleting a derived object through a base pointer without one skips the derived cleanup and is undefined behaviour.
- Beware slicing. Copying a derived object into a base value discards the derived part ─ use a pointer or reference to keep polymorphism alive.
- Use
overrideon derived functions. It’s free insurance that the signature really matches a base virtual. - Prefer
dynamic_castoverstatic_castfor downcasting. It checks at runtime and returnsnullptron a mismatch;static_castperforms no check at all. - A pure virtual (
= 0) makes a class abstract ─ it can’t be instantiated, only pointed at, and every concrete derived type must implement it.
Keep these in mind and polymorphism is both powerful and safe 🥳.
So what should we remember? 🤔
Let’s wrap up the key takeaways ─
- A base-class pointer can point at a derived object (upcasting) ─ base-typed pointer, derived object. This is the setup for all polymorphism.
- Picture one blueprint, many buildings 🏙️ ─ a base pointer is a slip that only says “a building,” and
virtualis what lets it reach the real building’s own procedures. - Without
virtual, a call binds to the pointer’s type at compile time (the surprise). Withvirtual, it dispatches to the real object’s type at runtime ─ that’s polymorphism. - Under the hood, each polymorphic object carries a hidden vptr to its type’s vtable (a table of function pointers) ─ dynamic dispatch is pointers all the way down.
- The payoff ─ a single collection of base pointers can hold many derived types, and one loop makes each behave as itself.
- Virtual destructors are essential ─ deleting a derived object through a base pointer needs one, or the derived part leaks (undefined behaviour).
- Object slicing ─ copying a derived object into a base value discards the derived part, which is exactly why polymorphism uses pointers and references.
dynamic_castsafely recovers a derived type at runtime (or givesnullptr), and a pure virtual= 0makes an abstract base ─ a pure interface.
Once this clicks, pointers stop being just “addresses” and become the mechanism that lets one line of code do the right thing for a hundred different types 👀✨.
Congratulations 🥳🥳🥳
We’ve now seen the most powerful thing pointers do ─ giving us runtime polymorphism, clean interfaces, and code that bends gracefully to new types.
And with that, we’re almost at the end of our journey. In the next post ─ the final one in this series ─ we’ll set pointers side by side with their close cousin, the reference, compare exactly how they differ and when to reach for each, and tie the whole series together 😉
Happy Coding 💻 🎵