OOP

Inheritance and Polymorphism

Extend classes with inheritance and use polymorphism with virtual functions.

Inheritance

Inheritance lets a derived class inherit attributes and methods from a base class. Use : syntax.

Virtual Functions

Declare a function virtual to enable runtime polymorphism. Derived classes override virtual functions to provide specific implementations.

Pure Virtual Functions (Abstract Classes)

A function declared with = 0 is a pure virtual function. Classes containing them are abstract and cannot be instantiated — only used as base classes.

Virtual Destructors

Always make destructors virtual in base classes to ensure proper cleanup of derived objects.

Example

cpp
#include <iostream>
#include <vector>
#include <memory>
using namespace std;

// Abstract base class
class Shape {
public:
    virtual double area() const = 0;     // pure virtual
    virtual double perimeter() const = 0; // pure virtual
    virtual void draw() const {
        cout << "Drawing shape with area: " << area() << endl;
    }
    virtual ~Shape() {}  // virtual destructor!
};

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 {
        cout << "Drawing circle with radius " << radius << endl;
    }
};

class Rectangle : public Shape {
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() const override { return width * height; }
    double perimeter() const override { return 2 * (width + height); }
};

int main() {
    // Polymorphism via 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));

    for (const auto& shape : shapes) {
        shape->draw();
        cout << "Area: " << shape->area() << endl;
    }

    return 0;
}
Example — CPP
#include <iostream>
#include <vector>
#include <memory>
using namespace std;

// Abstract base class
class Shape {
public:
    virtual double area() const = 0;     // pure virtual
    virtual double perimeter() const = 0; // pure virtual
    virtual void draw() const {
        cout << "Drawing shape with area: " << area() << endl;
    }
    virtual ~Shape() {}  // virtual destructor!
};

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 {
        cout << "Drawing circle with radius " << radius << endl;
    }
};

class Rectangle : public Shape {
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() const override { return width * height; }
    double perimeter() const override { return 2 * (width + height); }
};

int main() {
    // Polymorphism via 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));

    for (const auto& shape : shapes) {
        shape->draw();
        cout << "Area: " << shape->area() << endl;
    }

    return 0;
}
Run it yourself — save as main.cpp
$ g++ main.cpp -o main && ./main

Where You'll See This in the Real World

Strip away the geometry and what this code demonstrates is the standard C++ recipe for "I have many different things that all speak the same interface, and I want to store them together and call them without knowing which is which at compile time." That exact recipe is everywhere in production code:

  • Physics engines in games. Balls, boxes, ragdoll capsules and terrain meshes all answer the same questions — where are you, what do you weigh, does this line hit you — and each answers differently. The engine keeps every object as one kind of collision shape and runs the whole list each frame, exactly like the loop in main(). Box2D and Bullet are built this way.
  • Ray tracers. Everything in a scene has to say whether a ray hits it: spheres, triangles, and the boxes that group them. The renderer holds them all as one kind of thing and asks each the same question.
  • Desktop and mobile apps. Every button, slider and text box has to draw itself and react to clicks. The window keeps them in one list and tells each to paint, without knowing which is which. Qt, JUCE and wxWidgets work like this, and the default draw() here is the same idea as a click handler you can override.
  • Audio effects and plugins. A reverb, an equalizer and a compressor each take a chunk of sound and hand back a changed chunk, so the program runs them in order as one chain. This is how plugins work generally: the host only knows the base class, and something loaded at runtime hands it a type the host has never seen.
  • Compilers and interpreters. Every piece of a program's syntax tree — a number, an addition, an if statement — has to be evaluated or turned into machine code, each in its own way. The compiler walks the tree calling the same function on every node.
  • Swappable backends. Code that logs messages, stores files or reads a sensor is written against a base class, so the concrete version can be console or file, cloud or local, one database or another. It is also why a test can plug in a fake without touching the rest of the program.

Three lines in the example are the ones that matter in real code. The virtual destructor is what lets unique_ptr<Shape> correctly destroy a Circle; without it, deleting through the base pointer is undefined behavior, and a derived class holding a file handle or GPU buffer leaks silently. draw() having a default body that calls area() is the Template Method pattern: the base defines the skeleton, derived classes fill in the steps. And vector<unique_ptr<Shape>> rather than vector<Shape> — which cannot hold an abstract type by value, and would slice even if it could — is the shape of every scene graph, widget tree and plugin registry.

When real code avoids it. If the set of types is closed and known at compile time, std::variant with std::visit gives the same dispatch with no heap allocation or vtable. Game engines lean toward data-oriented layouts because looping over scattered heap objects thrashes the cache. Virtual dispatch wins when the set of types is open-ended, the container is heterogeneous by nature, or dispatch cost is negligible next to the work being done.