Last updated: December 2025

C++ Inheritance & Polymorphism: All Types, Virtual Functions, vtable, override, final & DSA (2025)

By CoodeVerse Editorial Team ✓ 2025 Verified ⏱ 24 min read 🎯 Beginner–Intermediate 📦 C++11/17/20
Difficulty:
Beginner–Intermediate — Prerequisites: Classes & Objects

⚡ Quick Answer: Inheritance & Polymorphism in C++

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

🌳 Section 1

5 Inheritance Types With Examples

1. Single
One derived, one base.
class Dog : public Animal
Most common — use for is-a.
2. Multiple
One derived, multiple bases.
class Amphibian : public Land, public Water
Use carefully — can cause diamond.
3. Multilevel
Chain: A → B → C.
class C : public B where B : public A
Each level specializes further.
4. Hierarchical
Multiple derived from one base.
class Dog : public Animal
class Cat : public Animal
5. Hybrid
Mix of multiple + multilevel.
Often causes diamond problem.
Needs virtual inheritance.
inheritance_types.cpp — single + multilevel + hierarchicalC++
#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;
}
Output
Buddy breathes
Buddy barks!
Buddy guides owner
Rex breathes
Whiskers breathes
Rex barks!
Whiskers meows!
🔐 Section 2

Access Specifiers in Inheritance

Base memberpublic inheritanceprotected inheritanceprivate inheritance
publicpublicprotectedprivate
protectedprotectedprotectedprivate
privateinaccessibleinaccessibleinaccessible
Use public inheritance for "is-a" relationships (Dog is-a Animal). Use private inheritance for "implemented-in-terms-of" — you want the implementation but don't want to expose the base class interface (a Stack built using a vector, where you don't want vector's push_back visible). Protected inheritance is rare in practice.
⚡ Section 3

Virtual Functions & vtable Internals

How virtual dispatch works
Circle object
vptr →
Circle vtable
[0]: Circle::draw
[1]: Circle::area
Circle::draw()
"Drawing circle"
Square object
vptr →
Square vtable
[0]: Square::draw
[1]: Square::area
Square::draw()
"Drawing square"
Shape* p = new Circle(); p→draw() → follows p's vptr → Circle's vtable → Circle::draw()
virtual_functions.cpp — runtime polymorphism via vtableC++
#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;
}
Output
Circle(r=5)
area=78.5398
Square(s=4)
area=16
Circle(r=3)
area=28.2743
Circle(r=2)
🎭 Section 4

Pure Virtual Functions & Abstract Classes

abstract_class.cpp — interface design with pure virtualC++
#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;
}
Output
json: {"id":42,"name":"Alice"}
Deserializing: {"id":43}
✅ Section 5

override, final & Virtual Destructors

override_final.cpp — C++11 safety + correct destructor chainC++11
#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;
}
Output
Derived::process
Derived::compute(42)
~Derived
~Base
Three rules — always apply: (1) Virtual destructor in every class that has virtual functions. (2) override 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.
💎 Section 6

Diamond Problem & Virtual Inheritance

diamond_problem.cpp — problem and solutionC++
#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;
}
Output
10 10
99 10
val=42
val=100
🔄 Section 7

dynamic_cast & Object Slicing

dynamic_cast.cpp — safe runtime cast + slicing demoC++
#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;
}
Output
Woof!
Fetching!
Not a Cat
Woof!
🏆 Section 8

DSA Examples: Graph, Tree, Strategy

graph_polymorphism.cpp — abstract Graph base + directed/undirectedC++
#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);
}
Output
Directed:
0 → 1 2
1 → 2
2 →
Undirected:
0 — 1 2
1 — 0 2
2 — 1 0
tree_hierarchy.cpp — BinaryTree → BST with inheritanceC++
#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
}
Output
1 2 3 5 7
1 2 3 5 6 7
sort_strategy.cpp — Strategy pattern with inheritanceC++
#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);
}
Output
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

Non-virtual: the compiler decides which function to call based on the declared type of the pointer/reference — compile-time, zero overhead. Even if a derived version exists, the base version runs. Virtual: the correct function is determined at runtime based on the actual object type — follows the vptr to the vtable. This is why forgetting virtual is a critical bug: Base* p = new Derived(); p->method() — without virtual, always calls Base::method even though p points to a Derived object.
The diamond problem occurs when class D inherits from B and C, both of which inherit from A — D ends up with two copies of A's data and ambiguous access to A's members. Fix: use virtual inheritance — 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.
Without a virtual destructor: 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.
Object slicing occurs when a derived object is assigned or copied to a base class variable (by value) — the derived-specific data and vtable are lost. 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.
LSP states: a derived class object must be substitutable for a base class object without breaking the program's correctness. If code works correctly with a Shape*, it must work correctly when that pointer actually points to a Circle or Square. Violations: Circle::area() throwing an exception when the base Shape::area() never throws. Square::setWidth() silently changing height (if inheriting from Rectangle). C++ test: can you replace every use of Base with Derived and get correct results? If not, the inheritance is wrong — use composition instead.
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.

Related C++ Topics on CoodeVerse

Classes & Objects Advanced OOP Design Patterns Constructors & Destructors Templates Smart Pointers Debugging & Optimization 📚 Full C++ Course

CoodeVerse Editorial Team

Senior C++ architects and CS educators. All code tested with GCC 13 and Clang 16, -Wall -Wextra -std=c++17.