C++ Inheritance, Virtual Functions & Runtime Polymorphism Masterclass
Welcome to Phase 11: Inheritance & Polymorphism! Inheritance lets a derived class reuse, extend, and specialize a base class. Polymorphism (runtime dispatch through virtual functions) allows a base-class pointer/reference to call the correct overridden method at runtime โ the foundation of object-oriented design in C++.
A derived class inherits members from a base class. Access specifier in the inheritance declaration controls how base members are seen in the derived class and outside world.
| Inheritance Type | public in Base | protected in Base | private in Base |
|---|---|---|---|
public inheritance | public | protected | inaccessible |
protected inheritance | protected | protected | inaccessible |
private inheritance | private | private | inaccessible |
Inheritance Hierarchy Types:
โข Single: One derived from one base.
โข Multilevel: A โ B โ C chain.
โข Multiple: Derived inherits from two or more bases.
โข Hierarchical: Multiple derived classes from one base.
Declaring a method virtual in the base class tells the compiler to use the vtable (virtual dispatch table) mechanism. At runtime, the actual object type determines which override is called โ this is runtime polymorphism.
Key Rules:
โข Always mark the destructor virtual in a polymorphic base class to prevent undefined behaviour on delete.
โข Use override keyword in derived classes to catch typos at compile time.
โข Use final to prevent further overriding.
โข A pure virtual function (= 0) makes the class abstract โ it cannot be instantiated directly.
#include <iostream>
#include <memory>
#include <vector>
class Animal {
public:
virtual void sound() const {
std::cout << "Generic animal sound
";
}
virtual std::string name() const = 0; // pure virtual โ abstract class
virtual ~Animal() = default; // virtual destructor ESSENTIAL!
};
class Dog : public Animal {
public:
void sound() const override {
std::cout << "Dog: Woof! Woof!
";
}
std::string name() const override { return "Dog"; }
};
class Cat : public Animal {
public:
void sound() const override {
std::cout << "Cat: Meow!
";
}
std::string name() const override { return "Cat"; }
};
class GuideDog : public Dog { // multilevel inheritance
public:
void guide() const {
std::cout << "GuideDog: Leading the way!
";
}
};
int main() {
// Runtime polymorphism via base-class pointer
std::vector<std::unique_ptr<Animal>> animals;
animals.push_back(std::make_unique<Dog>());
animals.push_back(std::make_unique<Cat>());
animals.push_back(std::make_unique<GuideDog>());
for (const auto& a : animals) {
std::cout << a->name() << ": ";
a->sound();
}
// dynamic_cast for safe downcasting
Animal* ptr = new GuideDog();
if (auto* gd = dynamic_cast<GuideDog*>(ptr)) {
gd->guide();
}
delete ptr;
return 0;
}
| Cast Type | Direction | Safety | Example |
|---|---|---|---|
| Upcasting | Derived โ Base | Always safe (implicit) | Animal* a = &dog; |
| Downcasting (static) | Base โ Derived | Unsafe โ programmer must verify type | static_cast<Dog*>(a) |
| Downcasting (dynamic) | Base โ Derived | Runtime-checked; returns nullptr if wrong | dynamic_cast<Dog*>(a) |
Composition vs Inheritance:
โข Inheritance expresses an IS-A relationship (Dog IS-A Animal).
โข Composition expresses a HAS-A relationship (Car HAS-A Engine). Prefer composition for flexibility โ it avoids deep inheritance hierarchies and fragile base-class coupling.
In C++, an abstract class has at least one pure virtual function. It defines a contract that all derived classes must fulfill. This mimics Java/C# interfaces without a separate interface keyword.
#include <iostream>
class ILogger { // Interface-style abstract base
public:
virtual void log(const std::string& msg) const = 0;
virtual void setLevel(int level) = 0;
virtual ~ILogger() = default;
};
class ConsoleLogger : public ILogger {
int level_{0};
public:
void log(const std::string& msg) const override {
std::cout << "[LOG-" << level_ << "] " << msg << "
";
}
void setLevel(int level) override { level_ = level; }
};
void processWithLogger(ILogger& logger) {
logger.setLevel(2);
logger.log("System started successfully");
}
int main() {
ConsoleLogger clog;
processWithLogger(clog);
return 0;
}
Q1: Why is virtual destructor important?
Without a virtual destructor, deleting a Derived object through a Base pointer calls only the Base destructor โ the Derived destructor is skipped, causing resource leaks.
Q2: What is the diamond problem in multiple inheritance?
When two base classes share a common ancestor, the derived class gets two copies of that ancestor. Solved with virtual inheritance: class B : virtual public A.
Q3: Can a constructor be virtual?
No. Constructors cannot be virtual because the vtable is set up during construction โ the object type isn't fully known yet.
Q4: What is the overhead of virtual functions?
Each polymorphic object carries a hidden vptr (8 bytes on 64-bit). Each virtual call does one extra pointer dereference โ negligible in almost all applications.
Q5: What does override keyword do?
override instructs the compiler to verify the function actually overrides a virtual function in the base. It catches typos and signature mismatches at compile time.