Inheritance and Polymorphism in C++
Understand virtual functions, vtable mechanics, pure virtual functions, abstract classes, and multiple inheritance.
Inheritance and Polymorphism in C++
Inheritance lets you build new types from existing ones, reusing interface and implementation. Polymorphism lets you write code against a base class that works correctly for any derived class determined only at runtime. Together they form the backbone of classic object-oriented design and enable patterns like plugins, strategy objects, and scene graphs where the concrete types are not known at compile time.
Base and Derived Classes
A derived class inherits all the members of its base class. It can extend the base with new members and, if the base has virtual functions, override them. The “is-a” relationship that justifies inheritance: a Dog is an Animal, so everywhere an Animal is expected, a Dog should work.
#include <string>
#include <iostream>
class Animal {
protected:
std::string name_; // protected: visible to derived classes
public:
explicit Animal(const std::string& name) : name_(name) {}
std::string name() const { return name_; }
void breathe() const { std::cout << name_ << " breathes\n"; }
};
class Dog : public Animal {
public:
explicit Dog(const std::string& name) : Animal(name) {} // delegate to base constructor
void bark() const { std::cout << name_ << " says: Woof!\n"; }
};
int main() {
Dog d("Rex");
d.breathe(); // inherited from Animal — no code duplication
d.bark(); // Dog-specific behavior
}
Inheritance Access Modes
The access mode controls how the inherited members appear to the outside world. public inheritance is by far the most common because it preserves the base class’s interface — it is the correct choice whenever the derived class truly “is a” base class.
| Mode | public members become | protected members become |
|---|---|---|
public | public | protected |
protected | protected | protected |
private | private | private |
public inheritance models an “is-a” relationship and is by far the most common. Prefer it unless you have a specific reason for the others.
Virtual Functions and Runtime Polymorphism
Without virtual, calling a method through a base pointer always calls the base version — the call is resolved at compile time based on the pointer type. With virtual, the call is resolved at runtime based on the actual object type. This is what enables a container of Shape* pointers to correctly call each shape’s draw() even though the container doesn’t know the concrete types.
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
virtual double area() const = 0; // pure virtual — must be overridden
virtual void draw() const { std::cout << "Drawing shape\n"; }
virtual ~Shape() = default; // must be virtual — see section below
};
class Circle : public Shape {
double radius_;
public:
explicit Circle(double r) : radius_(r) {}
double area() const override { return 3.14159265 * radius_ * radius_; }
void draw() const override { std::cout << "Drawing circle r=" << radius_ << "\n"; }
};
class Rectangle : public Shape {
double w_, h_;
public:
Rectangle(double w, double h) : w_(w), h_(h) {}
double area() const override { return w_ * h_; }
void draw() const override { std::cout << "Drawing rect " << w_ << "x" << h_ << "\n"; }
};
int main() {
// A heterogeneous collection of shapes — the key payoff of polymorphism
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(4.0, 6.0));
for (const auto& s : shapes) {
s->draw(); // calls the right draw()
std::cout << " area = " << s->area() << "\n"; // calls the right area()
}
}
How the vtable Works
Understanding the vtable demystifies virtual dispatch and explains its cost. Every class with at least one virtual function gets a vtable — a static array of function pointers, one per virtual function. Each object of that class carries a hidden vptr (typically 8 bytes on 64-bit systems) pointing to the vtable.
Circle object layout:
[ vptr ] ──────► Circle vtable
[ radius_ ] [ &Circle::area ]
[ &Circle::draw ]
[ &Circle::~Circle ]
When you call shape->area(), the CPU follows the vptr to the vtable, then calls the pointer at the area slot. This one level of indirection is the entire overhead of virtual dispatch — typically 1–3 nanoseconds, negligible except in tight inner loops.
Pure Virtual Functions and Abstract Classes
A function declared = 0 is pure virtual. Any class with at least one pure virtual function is abstract and cannot be instantiated directly. Abstract classes are the C++ mechanism for declaring interfaces: you define the contract (the pure virtual functions) without providing an implementation, and derived classes provide concrete implementations.
class Serializable {
public:
virtual std::string serialize() const = 0;
virtual void deserialize(const std::string& data) = 0;
virtual ~Serializable() = default;
};
// Must override both pure virtuals to be concrete and instantiable
class Config : public Serializable {
public:
std::string serialize() const override { return "{}"; }
void deserialize(const std::string&) override { /* ... */ }
};
Abstract classes define interfaces. A pure virtual function can still have a body, providing a default implementation that derived classes can call via Base::method().
override and final
Always use override when you intend to override a virtual function. Without it, a typo in the function signature silently creates a new non-virtual function instead of overriding the intended one — a common, hard-to-debug mistake. override makes the compiler catch this immediately.
class Base {
public:
virtual void process(int x) const {}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void process(int x) const override {} // OK — compiler verifies this overrides Base::process
// void process(float x) const override {} // compile error — no such virtual in Base
};
class Sealed final : public Base {
// No class can inherit from Sealed — useful for preventing misuse
void process(int x) const override final {}
};
Virtual Destructor
If you ever delete a derived object through a base pointer — which is the normal pattern with polymorphism — the base destructor must be virtual. Without it, only the base destructor runs, the derived destructor is skipped, and any resources owned by the derived class leak.
class Base {
public:
virtual ~Base() { /* base cleanup */ }
};
class Derived : public Base {
char* buffer_;
public:
Derived() : buffer_(new char[1024]) {}
~Derived() override { delete[] buffer_; } // runs correctly when Base::~Base is virtual
};
int main() {
Base* p = new Derived();
delete p; // calls ~Derived() then ~Base() — correct and leak-free
}
Multiple Inheritance and the Diamond Problem
C++ allows a class to inherit from multiple bases, which is useful for combining independent interfaces. The complication arises when two bases share a common grandparent — without virtual inheritance, the grandparent’s data appears twice, making member access ambiguous.
struct Animal { std::string name_; };
// Without virtual: Dog gets two copies of Animal — ambiguous name_
struct Flyable : Animal {};
struct Swimmable : Animal {};
struct Duck : Flyable, Swimmable {}; // Duck::name_ is ambiguous
// With virtual inheritance: only one shared Animal subobject
struct VFlyable : virtual Animal {};
struct VSwimmable : virtual Animal {};
struct VDuck : VFlyable, VSwimmable {}; // one Animal — unambiguous
Virtual base classes are the correct solution, though they add overhead. Prefer composition or interfaces (pure-virtual-only classes with no data) to avoid the diamond entirely.
dynamic_cast and typeid
dynamic_cast safely downcasts through an inheritance hierarchy at runtime. It returns nullptr (for pointer casts) or throws std::bad_cast (for reference casts) if the cast fails. This is safer than static_cast when you’re not certain of the actual type.
#include <typeinfo>
#include <iostream>
void processShape(Shape* s) {
if (auto* c = dynamic_cast<Circle*>(s)) {
// s is actually a Circle — safe to use Circle-specific API
std::cout << "Got a circle\n";
} else {
std::cout << "Not a circle — type: " << typeid(*s).name() << "\n";
}
}
typeid returns a std::type_info object whose .name() is implementation-defined. Prefer virtual dispatch over dynamic_cast/typeid in production code — if you need to switch on a type, the design usually needs a new virtual function instead.
Design Guidelines
- Always declare the base class destructor
virtualwhen the class is used polymorphically. - Mark overrides with
override— never omit it. - Prefer pure-virtual interfaces (no data, all virtual) to minimize coupling.
- Prefer composition over inheritance when the “is-a” relationship is unclear.
- Avoid deep inheritance hierarchies (more than 2-3 levels); they become hard to reason about.