C++ for Beginners Part 8: Inheritance and Polymorphism
Inheritance lets you build new classes on top of existing ones, reusing and extending behaviour. Polymorphism lets a single interface work with many different types at runtime. Together they're the core of OOP in C++. This article covers base and derived classes, virtual functions, abstract classes, and when to use each pattern.
C++ for Beginners: A Complete Guide
An eight-part beginner-friendly journey through C++: from writing your first program and understanding variables, through control flow, functions, and arrays, to the heart of C++ — pointers, classes, inheritance, and polymorphism. Each article is self-contained and builds on the previous, giving you both a quick reference and a progressive path to mastery.
- 4 C++ for Beginners Part 4: Functions
- 5 C++ for Beginners Part 5: Arrays, Strings, and std::vector
- 6 C++ for Beginners Part 6: Pointers and Memory Management
- 7 C++ for Beginners Part 7: Classes and Object-Oriented Programming
- 8 C++ for Beginners Part 8: Inheritance and Polymorphism
Table of Contents
- What is inheritance?
- Basic inheritance syntax
- Inheritance and constructors
- Method overriding
- Virtual functions — runtime polymorphism
- The override keyword
- Virtual destructors — essential!
- Abstract classes and pure virtual functions
- Polymorphism in action: the Shape hierarchy
- Composition vs inheritance
- Series wrap-up
- Where to go next
What is inheritance?
Inheritance allows a derived class to extend a base class, reusing its data and methods while adding or overriding behaviour.
Think of it in real-world terms:
Animalis a base classDogandCatare derived classes — they're both animals, but each has unique behaviour
Animal
├── Dog (inherits from Animal)
└── Cat (inherits from Animal)
Basic inheritance syntax
class Animal {
public:
std::string name;
Animal(const std::string& name) : name(name) {}
void eat() {
std::cout << name << " is eating.
";
}
void sleep() {
std::cout << name << " is sleeping.
";
}
};
class Dog : public Animal {
public:
Dog(const std::string& name) : Animal(name) {} // call base constructor
void bark() {
std::cout << name << " says: Woof!
";
}
};
class Cat : public Animal {
public:
Cat(const std::string& name) : Animal(name) {}
void meow() {
std::cout << name << " says: Meow!
";
}
};
Usage:
Dog d("Rex");
d.eat(); // inherited from Animal
d.sleep(); // inherited from Animal
d.bark(); // Dog-specific
Cat c("Whiskers");
c.eat();
c.meow();
The public keyword in : public Animal means the public interface of Animal remains public in Dog. (There's also protected and private inheritance, but public is almost always what you want.)
Inheritance and constructors
Derived class constructors must call the base class constructor in the initializer list:
class Shape {
protected:
std::string color;
public:
Shape(const std::string& color) : color(color) {}
};
class Circle : public Shape {
double radius;
public:
Circle(const std::string& color, double radius)
: Shape(color), radius(radius) {} // Shape(color) called here
};
If you don't call the base constructor explicitly, the base's default constructor is called. If there's no default constructor, it's a compile error.
Method overriding
A derived class can override a base class method by defining a method with the same name:
class Animal {
public:
void speak() {
std::cout << "...
";
}
};
class Dog : public Animal {
public:
void speak() { // overrides Animal::speak
std::cout << "Woof!
";
}
};
Dog d;
d.speak(); // Woof!
But there's a problem — what if you use a base class pointer?
Animal* a = new Dog();
a->speak(); // "..." ← calls Animal::speak, not Dog::speak !
This is because without virtual, C++ does static dispatch — the function is chosen at compile time based on the pointer type (Animal*), not the actual runtime type (Dog).
Virtual functions — runtime polymorphism
Mark the base function virtual to enable runtime dispatch:
class Animal {
public:
virtual void speak() {
std::cout << "...
";
}
};
class Dog : public Animal {
public:
void speak() override { // 'override' is optional but recommended
std::cout << "Woof!
";
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Meow!
";
}
};
Animal* animals[] = { new Dog(), new Cat(), new Dog() };
for (Animal* a : animals) {
a->speak(); // calls the correct derived-class version!
}
// Woof!
// Meow!
// Woof!
With virtual, C++ uses the vtable — a lookup table per class — to decide at runtime which function to call based on the actual object type.
The override keyword
override tells the compiler "I intend to override a virtual function". If the signature doesn't match a base virtual, the compiler errors out — catching typos:
class Dog : public Animal {
void speek() override {} // compile error: 'Animal::speek' not virtual
};
Virtual destructors — essential!
If you delete a derived object through a base pointer, the derived destructor only runs if the base destructor is virtual:
class Base {
public:
~Base() { std::cout << "Base destroyed
"; }
};
class Derived : public Base {
public:
~Derived() { std::cout << "Derived destroyed
"; }
};
Base* b = new Derived();
delete b;
// Only prints: "Base destroyed" — Derived's destructor never called!
// This is a resource leak if Derived owns memory/files/sockets
Fix: always make the base destructor virtual:
class Base {
public:
virtual ~Base() { std::cout << "Base destroyed
"; }
};
Base* b = new Derived();
delete b;
// Derived destroyed
// Base destroyed ← correct order
Rule: If a class has any virtual functions, its destructor must be virtual.
Abstract classes and pure virtual functions
A pure virtual function is declared with = 0. It has no implementation in the base class:
class Shape {
public:
virtual double area() const = 0; // pure virtual
virtual double perimeter() const = 0; // pure virtual
virtual void draw() const = 0; // pure virtual
virtual ~Shape() {}
};
A class with at least one pure virtual function is an abstract class — you cannot instantiate it directly:
Shape s; // compile error: cannot instantiate abstract class
Derived classes must implement all pure virtual functions or they too become abstract:
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.14159 * radius * radius; }
double perimeter() const override { return 2 * 3.14159 * radius; }
void draw() const override { std::cout << "Drawing circle
"; }
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double w, double h) : w(w), h(h) {}
double area() const override { return w * h; }
double perimeter() const override { return 2 * (w + h); }
void draw() const override { std::cout << "Drawing rectangle
"; }
};
Polymorphism in action: the Shape hierarchy
#include <iostream>
#include <vector>
#include <memory>
#include <cmath>
class Shape {
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual void describe() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
double r;
public:
Circle(double r) : r(r) {}
double area() const override { return M_PI * r * r; }
double perimeter() const override { return 2 * M_PI * r; }
void describe() const override {
std::cout << "Circle r=" << r
<< " area=" << area()
<< " perim=" << perimeter() << "
";
}
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double w, double h) : w(w), h(h) {}
double area() const override { return w * h; }
double perimeter() const override { return 2*(w+h); }
void describe() const override {
std::cout << "Rect " << w << "×" << h
<< " area=" << area()
<< " perim=" << perimeter() << "
";
}
};
class Triangle : public Shape {
double a, b, c; // three sides
public:
Triangle(double a, double b, double c) : a(a), b(b), c(c) {}
double perimeter() const override { return a + b + c; }
double area() const override {
double s = perimeter() / 2;
return std::sqrt(s * (s-a) * (s-b) * (s-c)); // Heron's formula
}
void describe() const override {
std::cout << "Triangle " << a << "+" << b << "+" << c
<< " area=" << area()
<< " perim=" << perimeter() << "
";
}
};
double totalArea(const std::vector<std::unique_ptr<Shape>>& shapes) {
double total = 0;
for (const auto& s : shapes) total += s->area();
return total;
}
int main() {
std::vector<std::unique_ptr<Shape>> canvas;
canvas.push_back(std::make_unique<Circle>(5));
canvas.push_back(std::make_unique<Rectangle>(4, 6));
canvas.push_back(std::make_unique<Triangle>(3, 4, 5));
for (const auto& shape : canvas) {
shape->describe();
}
std::cout << "Total area: " << totalArea(canvas) << "
";
}
Output:
Circle r=5 area=78.5398 perim=31.4159
Rect 4×6 area=24 perim=20
Triangle 3+4+5 area=6 perim=12
Total area: 108.54
The totalArea function works with any Shape — it doesn't need to know whether it's a Circle, Rectangle, or Triangle. That's polymorphism: one interface, many implementations.
Composition vs inheritance
Inheritance is powerful but often overused. Before reaching for it, ask:
- Is the relationship truly "is-a"? (A Dog is an Animal — ✓)
- Or is it "has-a"? (A Car has an Engine — use composition instead)
// Inheritance (is-a)
class Dog : public Animal { ... };
// Composition (has-a)
class Car {
Engine engine; // Car HAS an Engine
Transmission trans;
};
Composition is often more flexible and easier to change. Inheritance creates tight coupling between base and derived classes. In modern C++, prefer composition over inheritance unless you explicitly need polymorphic dispatch.
Series wrap-up
You now have a solid foundation in C++:
| Part | Topic |
|---|---|
| 1 | Hello World, compiler, build flags |
| 2 | Variables, types, operators |
| 3 | Control flow: if/switch/loops |
| 4 | Functions, references, overloading |
| 5 | Arrays, strings, std::vector |
| 6 | Pointers, heap memory, smart pointers |
| 7 | Classes, constructors, encapsulation |
| 8 | Inheritance, virtual functions, polymorphism |
Where to go next
- Templates — write type-generic algorithms and containers
- The STL in depth —
std::map,std::set,std::unordered_map, iterators,<algorithm> - Move semantics and rvalue references — understand the performance model of modern C++
- Concurrency —
std::thread,std::mutex,std::atomic - Practice — solve problems on LeetCode or Codeforces using C++; building small projects (a CLI game, a file parser, a simple interpreter) is the fastest way to cement these concepts.
C++ rewards patience. It's a language that takes time to master, but the understanding it builds about how computers work transfers to everything else you'll ever program.