Lesson 109 min read

Inheritance & Polymorphism

Inheritance is like a family tree β€” children inherit traits from parents, but can develop their own unique ones too.

Building on What Exists

Say you're building a drawing app. You have shapes β€” circles, rectangles, triangles. They all share some traits: a color, a position, an area() method. But each shape calculates area differently.

You could copy-paste the shared code into every shape class. Or you could write it once in a base class called Shape and let each specific shape inherit from it, adding only what makes it unique.

That's inheritance β€” a "is-a" relationship. A Circle is a Shape. A Rectangle is a Shape. They share the Shape interface but each has its own implementation.

Basic Inheritance β€” Shape Hierarchy

#include <iostream>
#include <string>
using namespace std;
class Shape {
protected:
string color;
public:
Shape(const string& c) : color(c) {}
void printColor() const {
cout << "Color: " << color << endl;
}
double area() const {
return 0.0; // Default β€” not very useful
}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(const string& c, double r) : Shape(c), radius(r) {}
double area() const {
return 3.14159 * radius * radius;
}
};
int main() {
Circle c("red", 5.0);
c.printColor(); // Inherited from Shape
cout << "Area: " << c.area() << endl; // Circle's own method
return 0;
}
Output
Color: red
Area: 78.5397

The Problem: What Happens Through a Base Pointer?

Here's where things get interesting β€” and tricky. If you store a Circle through a Shape* pointer and call area(), which version runs? Without the virtual keyword, it calls Shape's version β€” even though the actual object is a Circle. This is called static binding, and it's almost never what you want.

The fix? Make the base class method virtual. This tells the compiler: "Don't decide at compile time β€” check the actual object type at runtime." This is polymorphism.

virtual Functions β€” Polymorphism in Action

#include <iostream>
#include <cmath>
#include <vector>
#include <memory>
using namespace std;
class Shape {
protected:
string name;
public:
Shape(const string& n) : name(n) {}
// virtual = "let the actual object type decide"
virtual double area() const {
return 0.0;
}
// Virtual destructor β€” essential! (explained below)
virtual ~Shape() = default;
string getName() const { return name; }
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : Shape("Circle"), radius(r) {}
double area() const override { // override = safety check
return M_PI * radius * radius;
}
};
class Rectangle : public Shape {
double width, height;
public:
Rectangle(double w, double h)
: Shape("Rectangle"), width(w), height(h) {}
double area() const override {
return width * height;
}
};
class Triangle : public Shape {
double base, height;
public:
Triangle(double b, double h)
: Shape("Triangle"), base(b), height(h) {}
double area() const override {
return 0.5 * base * height;
}
};
int main() {
// Store different shapes through base class pointers
vector<unique_ptr<Shape>> shapes;
shapes.push_back(make_unique<Circle>(5.0));
shapes.push_back(make_unique<Rectangle>(4.0, 6.0));
shapes.push_back(make_unique<Triangle>(3.0, 8.0));
// Polymorphism: each shape calculates its own area
for (const auto& s : shapes) {
cout << s->getName() << " area: " << s->area() << endl;
}
return 0;
}
Output
Circle area: 78.5398
Rectangle area: 24
Triangle area: 12

The override Keyword β€” Your Safety Net

Always use override when you intend to override a virtual function. Without it, a typo in the function signature silently creates a new function instead of overriding the base one. With override, the compiler catches the mistake.

// Without override β€” silent bug if signature doesn't match
double area() const { ... }  // Might not actually override!

// With override β€” compiler error if it doesn't match a base virtual
double area() const override { ... }  // Safe!

Pure Virtual Functions & Abstract Classes

Sometimes a base class method has no reasonable default implementation. Every shape must have an area, but "Shape" by itself doesn't know how to calculate one. This is where pure virtual functions come in.

By writing = 0, you're saying: "I'm not implementing this β€” every derived class MUST." A class with at least one pure virtual function is abstract β€” you can't create instances of it directly.

Abstract Classes β€” Enforcing the Contract

#include <iostream>
#include <memory>
using namespace std;
class Animal {
public:
// Pure virtual β€” every animal MUST implement these
virtual void speak() const = 0;
virtual string type() const = 0;
// Regular method β€” shared by all animals
void describe() const {
cout << "I'm a " << type() << " and I say: ";
speak();
}
virtual ~Animal() = default;
};
// Animal a; // ERROR! Can't instantiate abstract class
class Dog : public Animal {
public:
void speak() const override { cout << "Woof!" << endl; }
string type() const override { return "Dog"; }
};
class Cat : public Animal {
public:
void speak() const override { cout << "Meow!" << endl; }
string type() const override { return "Cat"; }
};
int main() {
unique_ptr<Animal> pets[] = {
make_unique<Dog>(),
make_unique<Cat>()
};
for (const auto& pet : pets) {
pet->describe();
}
return 0;
}
Output
I'm a Dog and I say: Woof!
I'm a Cat and I say: Meow!

Why Virtual Destructors Matter

This is one of the most important rules in C++, and ignoring it causes silent memory leaks.

When you delete an object through a base class pointer (delete basePtr;), the compiler needs to know which destructor to call. Without virtual, it only calls the base class destructor, skipping the derived class cleanup entirely.

Virtual Destructor β€” Preventing Memory Leaks

#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "Base constructed" << endl; }
virtual ~Base() { cout << "Base destroyed" << endl; }
// Without virtual: ~Base() { ... } β€” DANGEROUS!
};
class Derived : public Base {
int* data;
public:
Derived() : data(new int[100]) {
cout << "Derived constructed (allocated memory)" << endl;
}
~Derived() override {
delete[] data;
cout << "Derived destroyed (freed memory)" << endl;
}
};
int main() {
cout << "--- Creating through base pointer ---" << endl;
Base* ptr = new Derived();
cout << "--- Deleting through base pointer ---" << endl;
delete ptr; // With virtual: calls Derived then Base destructor
// Without virtual: only calls Base β€” memory leak!
return 0;
}
Output
--- Creating through base pointer ---
Base constructed
Derived constructed (allocated memory)
--- Deleting through base pointer ---
Derived destroyed (freed memory)
Base destroyed
Note: If a class has ANY virtual function, its destructor MUST be virtual too. Otherwise, deleting through a base pointer won't call the derived destructor β€” causing memory leaks. The simplest fix: virtual ~Base() = default;

The Slicing Problem

One last gotcha. When you assign a derived object to a base object by value, the derived parts get sliced off. Only the base portion remains. This is called object slicing, and it's a sneaky bug.

Object Slicing β€” A Subtle Bug

#include <iostream>
using namespace std;
class Base {
public:
virtual void whoAmI() const {
cout << "I am Base" << endl;
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void whoAmI() const override {
cout << "I am Derived" << endl;
}
};
void byValue(Base b) { // COPIES β€” slicing happens!
b.whoAmI();
}
void byReference(const Base& b) { // Reference β€” no slicing
b.whoAmI();
}
int main() {
Derived d;
cout << "By value: ";
byValue(d); // Sliced! Calls Base::whoAmI
cout << "By reference: ";
byReference(d); // No slicing! Calls Derived::whoAmI
return 0;
}
Output
By value:     I am Base
By reference: I am Derived

Key Takeaways

  • Use virtual on base class methods that derived classes should override
  • Always mark overrides with override for compile-time safety
  • Use = 0 for pure virtual functions when there's no sensible default
  • If you have virtual functions, make the destructor virtual too
  • Use pointers or references (not value types) to avoid object slicing
  • Prefer unique_ptr for owning polymorphic objects
Challenge

Quick check

What happens when you call a non-virtual method through a base class pointer pointing to a derived object?
← Classes & ObjectsPointers & References β†’