Inheritance & Polymorphism

Scope: Inheritance lets one class build on another ("a Rectangle is-a Shape"); polymorphism lets one call behave differently per object type. Together they are the tools behind extensible, large-scale C++ designs. This lesson explains derivation, virtual functions, abstract classes, and the pitfalls to avoid.

Deriving a class

A derived class declares a base class after a colon. It inherits the base's members and can add its own state and behavior.

Base and derived

class Shape {
public:
    void setColor(const std::string &color) { this->color = color; }
protected:                     // visible to derived classes
    std::string color = "black";
};

class Rectangle : public Shape {   // Rectangle is-a Shape
public:
    double area() const { return width * height; }
private:
    double width = 1.0, height = 1.0;
};

public Shape means "is-a": a Rectangle can be used anywhere a Shape is expected. The protected members are invisible to the outside world but visible inside derived classes.

What is inherited

The derived class inherits all public and protected members of the base — its methods become available on the derived object:

Rectangle r;
r.setColor("red");        // inherited from Shape
std::cout << r.area();      // Rectangle's own method

What is not inherited: constructors, the destructor, and private members. Also, if the base and derived methods share a name, the derived version hides the base's — declare in the derived class with using Base::name; when you need both.

Virtual functions & polymorphism

The real power of inheritance comes from virtual functions. Mark a base method virtual; each derived class can provide its own implementation. When you call through a base pointer or reference, the derived version runs — the call is resolved at run time based on the object's actual type.

class Shape {
public:
    virtual double area() const { return 0.0; }   // overridable
};

class Circle : public Shape {
public:
    double area() const override {                // override: safer
        return 3.14159 * radius * radius;
    }
private:
    double radius = 1.0;
};

void printArea(const Shape &s) {    // accepts any Shape
    std::cout << s.area() << "\n";   // dispatch to the real type
}

override

Write override after every overriding method. It asks the compiler to verify the base really has a matching virtual — catching typos and signature mismatches at compile time. A base method should be virtual only when derived classes are expected to replace it; making everything virtual has a (small) cost and weakens the interface contract.

Pure virtual & abstract

A virtual function can declare no implementation: = 0 makes it pure virtual. A class with a pure virtual cannot be instantiated — it is an abstract interface that only shapes the derived classes:

class Shape {
public:
    virtual double area() const = 0;   // pure: no body here
    virtual ~Shape() = default;        // virtual destructor (essential!)
};

// Shape s;  // error: Shape is abstract
Circle c;
printArea(c);                          // works through the interface

Any class meant to be deleted through a base pointer must have a virtual destructor — otherwise only the base part is destroyed, leaking derived resources.

Class hierarchy: abstract Shape with derived Circle and Rectangle, pure virtual area reimplemented in each.

An abstract base class (Shape) with two concrete derived classes.

Slicing & references

Polymorphism works only through pointers and references. Copying a derived object into a base value slices off the derived part:

Circle c;
Shape s = c;          // SLICING: only the Shape part is copied; area() is Shape's
Shape &ref = c;       // OK: reference keeps the real object
Shape *ptr = &c;      // OK: pointer keeps the real object

Slicing is silent data loss. When you need polymorphic collections, store smart pointers to the base type — the memory lesson shows how.

final & best practices

A class or method marked final cannot be derived from or overridden — a deliberate lock that helps readers and the compiler:

class Circle final : public Shape {   // no class may derive from Circle
    double area() const override final { return 3.14159; }
};

Inheritance guidance in one line: prefer composition (a class contains another class) over inheritance unless a genuine "is-a" relationship with polymorphic behavior exists. Deep hierarchies (more than two or three levels) become hard to reason about.

Practice

  1. Build a small Animal hierarchy: base with virtual speak(), derived Dog and Cat overriding it.
  2. Store cats and dogs in a std::vector<Animal *> and call speak() on each — the virtual dispatch must produce the right sound.
  3. Add a pure virtual name() to Animal and implement it in both derived classes.
  4. Demonstrate slicing by copying a Dog into an Animal value and showing the wrong speak() runs.
  5. Explain why the base destructor must be virtual when destruction happens through a base pointer.