Dataslope logoDataslope

Inheritance and Polymorphism

Building new classes from old ones and dispatching behaviour at runtime.

Sometimes two classes share most of their behaviour and differ in only a small way. A Square is a Rectangle where width equals height. A Dog, a Cat, and a Cow are all Animals that differ in how they make a sound. C++ offers two complementary features for these situations:

  • Inheritance lets one class extend another, taking its interface and adding to or refining it.
  • Polymorphism lets a single call site invoke different code depending on the actual type of the object at runtime.

Used carefully, this combination is powerful. Used carelessly, it produces tangled hierarchies that are hard to change. We'll lean toward small examples and clear rules.

A first hierarchy

class Animal {
public:
    Animal(std::string name) : name_(std::move(name)) {}
    const std::string& name() const { return name_; }

    virtual std::string sound() const { return "(generic sound)"; }
    virtual ~Animal() = default;     // important — see below

protected:
    std::string name_;
};

class Dog : public Animal {
public:
    Dog(std::string name) : Animal(std::move(name)) {}
    std::string sound() const override { return "woof"; }
};

class Cat : public Animal {
public:
    Cat(std::string name) : Animal(std::move(name)) {}
    std::string sound() const override { return "meow"; }
};

Three new keywords to absorb:

  • : public AnimalDog and Cat are derived from Animal. They inherit its public members.
  • virtual — marks sound as overridable; calls go through a runtime dispatch mechanism (the vtable) so the correct override runs even via a base-class pointer or reference.
  • override — tells the compiler "this is meant to override a virtual function." If the signature doesn't match a base virtual, you get a compile error instead of a silent mismatch.

Polymorphism in action

Once sound is virtual, code that holds an Animal* or Animal& will dispatch to the actual concrete class's version:

Code Block
C++ 20 (202002L)

Even though the loop variable's static type is "pointer to Animal", the call a->sound() runs Dog::sound or Cat::sound depending on what each pointer actually points to. That is runtime polymorphism.

How the compiler does this

For each class with virtual functions, the compiler builds a small hidden table — the vtable — listing the address of each virtual function for that class. Each object of that class stores a vptr to its class's vtable.

When you call a->sound(), the compiler emits "look up sound in a's vtable and call it." That's the one indirect call that gives you polymorphism — small but not free, which is why virtual is opt-in in C++.

Virtual destructors: don't forget

If you ever delete a derived object through a base-class pointer:

Animal* a = new Dog("Rex");
delete a;     // calls ~Animal — but Dog parts may leak unless dtor is virtual

A non-virtual destructor would only run ~Animal, leaving any Dog-specific resources unreleased. Any class that's intended to be a polymorphic base must give its destructor virtual. That's why we wrote virtual ~Animal() = default; above.

protected access

The base class above used protected: for name_. protected is like private but also accessible from derived classes. Use it sparingly — it weakens encapsulation. Often it's better to keep fields private and let derived classes call public methods.

Inheritance versus composition

It's tempting to use inheritance whenever two classes share fields or behaviour. Often the better tool is composition — one class has the other as a member.

// Inheritance: Car is-a Vehicle
class Car : public Vehicle { ... };

// Composition: Car has-an Engine
class Car {
    Engine engine_;
    ...
};

A useful rule: ask "is the relationship really 'is-a' in every sense?" If a Square is-a Rectangle, then you should be able to use a Square wherever a Rectangle is expected. (Famously, this fails in many ways, which is why "Square inherits from Rectangle" is a textbook OOP anti-example.)

When in doubt, compose.

Pure virtuals and abstract classes

You can declare a virtual function with no body, forcing every derived class to provide its own:

class Shape {
public:
    virtual double area() const = 0;     // pure virtual
    virtual ~Shape() = default;
};

A class with at least one pure virtual is abstract — you cannot create an instance of it directly. It serves as an interface that derived classes must implement.

Code Block
C++ 20 (202002L)

Challenge

Challenge
C++ 20 (202002L)
Shapes hierarchy

Add a Rectangle class derived from the abstract Shape that takes width and height and returns width * height from area(). main already creates a Rectangle(3.0, 4.0), calls area(), and expects 12 on stdout.

Test your understanding

QuestionSelect one

What does the virtual keyword on a member function do?

It marks the function as faster to call.

It makes the function dispatched at runtime through the vtable, so calls via a base pointer/reference run the most-derived override.

It hides the function from derived classes.

It is required for any inherited function.

QuestionSelect one

Why give a polymorphic base class a virtual destructor?

The compiler refuses to compile otherwise.

It makes destruction faster.

So that delete through a base-class pointer also calls the derived destructor and releases derived-class resources; otherwise you get a partial cleanup which is undefined behavior.

It is purely stylistic.

Next: how to own resources properly — the RAII pattern that underpins all robust C++ programs.

On this page