C++ Inheritance & Polymorphism: All Types, Virtual Functions, vtable, override, final & DSA (2025)
⚡ Quick Answer: Inheritance & Polymorphism in C++
- Inheritance:
class Derived : public Base— derived class gets all public/protected members - 5 types: single, multiple, multilevel, hierarchical, hybrid
- virtual: enables runtime dispatch via vtable — always call the correct derived version
- pure virtual:
= 0— forces derived class to implement; makes class abstract - override: compile-time check that you're actually overriding a virtual function
- virtual destructor: mandatory in any polymorphic base class
- diamond problem: solved with
virtual publicinheritance
Inheritance and polymorphism are the two pillars of C++ OOP — they let you model real-world
hierarchies, write reusable algorithms that work on any derived type, and build extensible
systems without modifying existing code. This guide covers everything from the 5 inheritance
types through vtable internals, the diamond problem, dynamic_cast, and five
complete DSA examples.
5 Inheritance Types
Single → Hybrid
Access Specifiers
public/protected/private
Virtual Functions
vtable explained
Pure Virtual & Abstract
Interfaces in C++
override & final
C++11 safety keywords
Diamond Problem
Virtual inheritance
dynamic_cast
Safe runtime cast
DSA Examples
Graph, Tree, Iterator
5 Inheritance Types With Examples
class Dog : public AnimalMost common — use for is-a.
class Amphibian : public Land, public WaterUse carefully — can cause diamond.
class C : public B where B : public AEach level specializes further.
class Dog : public Animalclass Cat : public AnimalOften causes diamond problem.
Needs virtual inheritance.
#include <iostream>
#include <string>
using namespace std;
// ── Single Inheritance ─────────────────────────────────────────
class Animal {
protected:
string name;
public:
explicit Animal(string n) : name(move(n)) {}
void breathe() const { cout << name << " breathes\n"; }
};
class Dog : public Animal { // Single inheritance
public:
explicit Dog(string n) : Animal(move(n)) {}
void bark() const { cout << name << " barks!\n"; }
};
// ── Multilevel Inheritance ─────────────────────────────────────
class GuideDog : public Dog { // Dog → Animal → GuideDog
public:
explicit GuideDog(string n) : Dog(move(n)) {}
void guide() const { cout << name << " guides owner\n"; }
};
// ── Hierarchical Inheritance ───────────────────────────────────
class Cat : public Animal { // Cat and Dog both inherit Animal
public:
explicit Cat(string n) : Animal(move(n)) {}
void meow() const { cout << name << " meows!\n"; }
};
int main() {
GuideDog gd("Buddy");
gd.breathe(); // from Animal
gd.bark(); // from Dog
gd.guide(); // own method
Dog d("Rex"); Cat c("Whiskers");
d.breathe(); c.breathe(); // both inherit breathe()
d.bark(); c.meow();
return 0;
}
Buddy breathes
Buddy barks!
Buddy guides owner
Rex breathes
Whiskers breathes
Rex barks!
Whiskers meows!Access Specifiers in Inheritance
| Base member | public inheritance | protected inheritance | private inheritance |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | inaccessible | inaccessible | inaccessible |
Virtual Functions & vtable Internals
vptr → → Circle vtable
[0]: Circle::draw
[1]: Circle::area → Circle::draw()
"Drawing circle"
vptr → → Square vtable
[0]: Square::draw
[1]: Square::area → Square::draw()
"Drawing square"
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class Shape {
public:
// virtual → runtime dispatch via vtable
virtual void draw() const { cout << "Shape::draw\n"; }
virtual double area() const noexcept { return 0; }
virtual ~Shape() = default; // MUST be virtual
};
class Circle : public Shape {
double r;
public:
explicit Circle(double radius) : r(radius) {}
void draw() const override { cout << "Circle(r="<<r<<")\n"; }
double area() const noexcept override{ return 3.14159*r*r; }
};
class Square : public Shape {
double s;
public:
explicit Square(double side) : s(side) {}
void draw() const override { cout << "Square(s="<<s<<")\n"; }
double area() const noexcept override{ return s*s; }
};
// Works for ANY Shape — current or future derived types
void printInfo(const Shape& s) {
s.draw(); // virtual → calls correct derived version
cout << " area=" << s.area() << "\n";
}
int main() {
// Store heterogeneous shapes — unique_ptr for RAII
vector<unique_ptr<Shape>> shapes;
shapes.emplace_back(make_unique<Circle>(5.0));
shapes.emplace_back(make_unique<Square>(4.0));
shapes.emplace_back(make_unique<Circle>(3.0));
for (const auto& s : shapes) printInfo(*s);
// Without virtual: WRONG — always calls Shape::draw
Shape* p = new Circle(2.0);
p->draw(); // WITH virtual: Circle::draw ✓
delete p; // virtual dtor: ~Circle then ~Shape ✓
return 0;
}
Circle(r=5)
area=78.5398
Square(s=4)
area=16
Circle(r=3)
area=28.2743
Circle(r=2)Pure Virtual Functions & Abstract Classes
#include <iostream>
#include <memory>
using namespace std;
// Abstract class — cannot be instantiated directly
class Serializable {
public:
virtual string serialize() const = 0; // pure virtual — MUST override
virtual void deserialize(const string&) = 0;
virtual ~Serializable() = default;
// Non-pure virtual: has default, can override
virtual string format() const { return "json"; }
};
// Concrete class — implements all pure virtuals → instantiable
class UserRecord : public Serializable {
string name; int id;
public:
UserRecord(string n, int i) : name(move(n)), id(i) {}
string serialize() const override {
return "{\"id\":" + to_string(id) + ",\"name\":\"" + name + "\"}";
}
void deserialize(const string& data) override {
cout << "Deserializing: " << data << "\n";
}
};
// ⚠ This would be a COMPILE ERROR:
// Serializable s; // Cannot instantiate abstract class
void save(const Serializable& obj) {
cout << obj.format() << ": " << obj.serialize() << "\n";
}
int main() {
UserRecord u("Alice", 42);
save(u); // json: {"id":42,"name":"Alice"}
u.deserialize("{\"id\":43}");
return 0;
}
json: {"id":42,"name":"Alice"}
Deserializing: {"id":43}override, final & Virtual Destructors
#include <iostream>
using namespace std;
class Base {
public:
virtual void process() { cout << "Base::process\n"; }
virtual void compute(int x) { cout << "Base::compute("<<x<<")\n"; }
virtual ~Base() { cout << "~Base\n"; } // MUST be virtual
};
class Derived : public Base {
public:
// override: compile error if Base::process doesn't exist/is not virtual
void process() override { cout << "Derived::process\n"; }
// ⚠ Without override, this silently creates a NEW function (not override)
// void compute(double x) { } ← different signature — not an override!
void compute(int x) override { cout << "Derived::compute("<<x<<")\n"; }
// final: GrandChild cannot override this
virtual void locked() final { cout << "Derived::locked\n"; }
~Derived() override { cout << "~Derived\n"; }
};
// final class — cannot be subclassed
class LeafNode final : public Derived { };
// class Bad : public LeafNode { }; ← COMPILE ERROR
int main() {
Base* p = new Derived();
p->process(); // Derived::process ← virtual dispatch
p->compute(42); // Derived::compute ← virtual dispatch
delete p; // ~Derived then ~Base ← virtual destructor
return 0;
}
Derived::process
Derived::compute(42)
~Derived
~Baseoverride on every function that overrides a virtual. (3) final on classes and functions you explicitly don't want further overridden. These three together eliminate the most common inheritance bugs.
Diamond Problem & Virtual Inheritance
#include <iostream>
using namespace std;
// ── Without virtual inheritance — PROBLEM ─────────────────────
class A { public: int x = 10; };
class B : public A {};
class C : public A {};
class D : public B, public C {
// D has TWO copies of A::x — ambiguous!
// D::x is a compile error — use B::x or C::x
};
// ── With virtual inheritance — SOLUTION ───────────────────────
class AV { public: int val = 42; void show() { cout << "val="<<val<<"\n"; } };
class BV : virtual public AV {}; // virtual inheritance
class CV : virtual public AV {}; // virtual inheritance
class DV : public BV, public CV {
public:
// DV must directly initialize AV (the shared base)
DV() : AV(), BV(), CV() {}
};
int main() {
// Without virtual
D d;
cout << d.B::x << " " << d.C::x << endl; // 10 10 — two separate copies
d.B::x = 99;
cout << d.B::x << " " << d.C::x << endl; // 99 10 — B and C are independent
// With virtual inheritance — ONE shared AV
DV dv;
dv.show(); // no ambiguity — only one AV
dv.val = 100;
dv.show(); // val=100 — single copy ✓
return 0;
}
10 10
99 10
val=42
val=100dynamic_cast & Object Slicing
#include <iostream>
#include <memory>
using namespace std;
class Animal { public: virtual ~Animal()=default; virtual void speak()=0; };
class Dog : public Animal {
public:
void speak() override { cout << "Woof!\n"; }
void fetch() const { cout << "Fetching!\n"; } // Dog-specific
};
class Cat : public Animal { public: void speak() override { cout << "Meow!\n"; } };
int main() {
// ── dynamic_cast — safe runtime downcast ─────────────────
unique_ptr<Animal> a = make_unique<Dog>();
a->speak(); // Woof! — virtual dispatch
// Downcast to Dog* — safe (returns nullptr if not Dog)
Dog* d = dynamic_cast<Dog*>(a.get());
if (d) d->fetch(); // Fetching! — Dog-specific method
// Downcast to Cat* — returns nullptr (not a Cat)
Cat* c = dynamic_cast<Cat*>(a.get());
if (!c) cout << "Not a Cat\n";
// ── Object slicing — DANGER with pass-by-value ────────────
Dog dog;
Animal sliced = dog; // SLICED — only Animal part copied!
// sliced.speak() — would call Animal::speak, not Dog::speak
// (speak is pure virtual, so this actually won't compile)
// Fix: always use pointers or references for polymorphism
Animal& ref = dog;
ref.speak(); // Woof! — reference, no slicing
return 0;
}
Woof!
Fetching!
Not a Cat
Woof!DSA Examples: Graph, Tree, Strategy
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class Graph { // Abstract graph interface
protected:
int V;
vector<vector<int>> adj;
public:
explicit Graph(int v) : V(v), adj(v) {}
virtual void addEdge(int u, int v) = 0; // pure virtual
virtual void printGraph() const = 0;
virtual ~Graph() = default;
int vertices() const noexcept { return V; }
};
class Directed : public Graph {
public:
using Graph::Graph;
void addEdge(int u, int v) override { adj[u].push_back(v); }
void printGraph() const override {
cout << "Directed:\n";
for (int i=0; i<V; i++) {
cout << " " << i << " → ";
for (int nb : adj[i]) cout << nb << " ";
cout << endl;
}
}
};
class Undirected : public Graph {
public:
using Graph::Graph;
void addEdge(int u, int v) override {
adj[u].push_back(v); adj[v].push_back(u);
}
void printGraph() const override {
cout << "Undirected:\n";
for (int i=0; i<V; i++) {
cout << " " << i << " — ";
for (int nb : adj[i]) cout << nb << " ";
cout << endl;
}
}
};
// Works with any Graph — current or future
void buildAndPrint(Graph& g) {
g.addEdge(0,1); g.addEdge(1,2); g.addEdge(0,2);
g.printGraph();
}
int main() {
Directed dg(3); buildAndPrint(dg);
Undirected ug(3); buildAndPrint(ug);
}
Directed:
0 → 1 2
1 → 2
2 →
Undirected:
0 — 1 2
1 — 0 2
2 — 1 0 #include <iostream>
#include <memory>
using namespace std;
struct Node { int val; unique_ptr<Node> left, right; explicit Node(int v):val(v){} };
class BinaryTree {
protected:
unique_ptr<Node> root;
void inorder_(const Node* n) const {
if (!n) return;
inorder_(n->left.get());
cout << n->val << " ";
inorder_(n->right.get());
}
public:
virtual void insert(int) = 0; // pure virtual — BST vs Random tree differ
void inorder() const { inorder_(root.get()); cout << endl; }
virtual ~BinaryTree() = default;
};
class BST : public BinaryTree {
void ins_(Node*& n, int v) {
if (!n) { n = new Node(v); return; }
v < n->val ? ins_((Node*&)n->left, v) : ins_((Node*&)n->right, v);
}
public:
void insert(int v) override { ins_((Node*&)root, v); }
};
int main() {
BST bst;
for (int x : {5,2,7,1,3}) bst.insert(x);
bst.inorder(); // 1 2 3 5 7
BinaryTree* t = &bst; // polymorphic use
t->insert(6);
t->inorder(); // 1 2 3 5 6 7
}
1 2 3 5 7
1 2 3 5 6 7 #include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class SortStrategy {
public:
virtual void sort(vector<int>& v) = 0;
virtual string name() const = 0;
virtual ~SortStrategy() = default;
};
class QuickSort : public SortStrategy {
public:
void sort(vector<int>& v) override { sort(v.begin(), v.end()); }
string name() const override { return "QuickSort"; }
};
class DescSort : public SortStrategy {
public:
void sort(vector<int>& v) override { sort(v.rbegin(), v.rend()); }
string name() const override { return "DescSort"; }
};
void run(SortStrategy& s, vector<int> v) {
s.sort(v);
cout << s.name() << ": ";
for (int x : v) cout << x << " ";
cout << endl;
}
int main() {
vector<int> data = {5,3,8,1,9};
QuickSort qs; run(qs, data);
DescSort ds; run(ds, data);
}
QuickSort: 1 3 5 8 9
DescSort: 9 8 5 3 1 Best Practices & Common Mistakes
✅ Virtual destructor in every polymorphic base
Any class with a virtual function needs virtual ~Base() = default;. Without it, deleting through a base pointer leaks resources.
✅ Always use override
Catches typos, signature mismatches, and overriding non-virtual functions — all silent bugs without override.
✅ Public inheritance for is-a only
If the derived class isn't truly a subtype of the base (Liskov Substitution Principle), use composition or private inheritance instead.
✅ Use pure virtual for interfaces
Force derived classes to implement required behavior. Documents the contract clearly and prevents incomplete implementations.
✅ Prefer unique_ptr for polymorphic collections
vector<unique_ptr<Base>> stores heterogeneous objects safely with automatic cleanup — better than raw pointer arrays.
✅ Mark non-extensible classes final
Signals design intent, prevents accidental subclassing, and enables compiler optimizations (devirtualization).
❌ Never call virtual in constructor/destructor
During construction, the vtable is not fully set up — virtual calls resolve to the base version. Use template method pattern if you need this.
❌ Avoid deep hierarchies (>3 levels)
Deep hierarchies are fragile — a base change breaks all descendants. Favor composition or flat hierarchies with multiple interfaces.
FAQ / Interview Questions
Base* p = new Derived(); p->method() — without virtual, always calls Base::method even though p points to a Derived object.class B : virtual public A and class C : virtual public A. This ensures D contains only one copy of A's data. D's constructor must directly initialize A. Virtual inheritance adds a small overhead (extra pointer) — use it only when you genuinely have the diamond structure.Base* p = new Derived(); delete p; — only ~Base() runs; ~Derived() is skipped, leaking any resources the derived class owns. With virtual destructor: ~Derived() runs first, then ~Base(). Rule: any class with virtual functions (polymorphic) must have a virtual destructor. Write virtual ~Base() = default;. This is one of the most common C++ bugs when forgotten.Animal a = dog; — a is now a pure Animal. Prevention: always use pointers or references for polymorphism: Animal& a = dog; or Animal* a = &dog;. Function parameters: void f(Animal a) slices; void f(Animal& a) or void f(Animal* a) does not. The C++ Core Guidelines recommend marking polymorphic base classes with a deleted copy constructor to catch accidental slicing.dynamic_cast: safe runtime downcast in a polymorphic hierarchy (requires virtual functions). Returns nullptr for pointers, throws std::bad_cast for references on failure. Use when you genuinely need to call derived-specific methods not in the base. static_cast: compile-time cast, no runtime check. For upcasting (derived to base — always safe), or when you are certain of the type (performance-critical code). Never use static_cast for downcasting in a polymorphic hierarchy — if you're wrong, it's undefined behavior. Frequent use of dynamic_cast often signals a design problem — prefer pure virtual interface design.Master C++ OOP — from inheritance to design patterns
Structured lessons, 200+ exercises, completion certificate. Join 50,000+ students on CoodeVerse.