Skip to content
C++

Inheritance and Virtual Functions

Inheritance is a mechanism that lets one class — the derived class — build on another — the base class — by automatically gaining all of its members. You use inheritance to model an is-a relationship: a dog is an animal, a savings account is an account, a circle is a shape. Virtual functions take inheritance one step further: they let the same function call do different things depending on the actual type of the object, which is the foundation of polymorphism.

The is-a relationship — when to use inheritance

Inheritance is only appropriate when the derived type genuinely is the base type. A circle is a shape. A savings account is an account. A truck is a vehicle. In each case, every operation that makes sense for the base also makes sense for the derived class — you can draw any shape, deposit into any account, drive any vehicle. Contrast this with a car having an engine: a car is not an engine, so modeling that relationship with inheritance would be wrong. When you are not sure, apply the is-a test: can you honestly say that every object of the derived class is also a valid object of the base class? If yes, inheritance is the right tool; otherwise, prefer composition.

Getting this right matters because inheritance determines what you can do with the objects. If Circle inherits from Shape, you can store a circle in a Shape* pointer and call draw() on it — the compiler knows every shape can be drawn. If the is-a test fails, that logic breaks down.

Deriving a class — the syntax

You derive a class by adding a base class specifier after the class name, using a colon followed by the access level and the base class name. The most common form is public inheritance, which preserves the public interface of the base class. When you write class Circle : public Shape, the compiler knows that a Circle object always contains a complete Shape subobject inside it, and all of Shape's public members are accessible as public members of Circle as well.

class Shape {
public:
    double area() const { return 0.0; }     // default: zero area
    void describe() const {
        std::println("Area is {}", area());
    }
private:
    std::string m_color {"black"};
};

class Circle : public Shape {       // Circle "is a" Shape
public:
    explicit Circle(double radius) : m_radius{radius} {}
    double area() const { return 3.14159 * m_radius * m_radius; }
private:
    double m_radius;
};

Circle c {5.0};
c.describe();   // inherited from Shape — prints "Area is 0" (problem we fix later)
c.area();       // calls Circle::area() — 78.5

The derived class inherits every member function and member variable from the base — including describe() and even m_color (though m_color is private, so Circle cannot access it directly). The derived class can add new members and can also provide its own version of any base class function. The problem above — describe() calling the wrong area() — is exactly what virtual functions solve.

What gets inherited — and what stays private

Every non-private member of the base class is directly accessible in the derived class. Private members of the base class are still part of every derived object — they occupy memory and the base class functions use them — but the derived class cannot refer to them by name. This is intentional: it enforces encapsulation. If you need to give derived classes access to a member without making it fully public, you use the protected access level. Protected members are accessible from within the class itself and within any class derived from it, but not from external code.

class Animal {
public:
    void breathe() const { std::println("breathing"); }
protected:
    std::string m_name;   // derived classes can read/write this
private:
    int m_internal_id;    // derived classes cannot touch this
};

class Dog : public Animal {
public:
    explicit Dog(std::string name) { m_name = name; }   // OK: protected
    void bark() const { std::println("{} barks!", m_name); }  // OK: protected
    // void bad() { std::println("{}", m_internal_id); }  // ERROR: private
};

In practice, many C++ programmers keep base class member variables private rather than protected, and provide protected getter/setter functions instead. This keeps a single point of control over how those values change, even for derived classes.

Constructors in derived classes

When a derived class object is created, the base class portion must be initialized first — always, without exception. The base class constructor runs before any line of the derived class constructor body executes. If you do not explicitly specify which base constructor to call, the compiler calls the base class's default constructor. When you want to pass arguments to a specific base constructor, you call it in the derived class's initializer list, before your own member initializers:

class Shape {
public:
    Shape() { std::println("Shape() called"); }
    explicit Shape(std::string_view color) : m_color{color} {
        std::println("Shape(color) called");
    }
protected:
    std::string m_color {"black"};
};

class Circle : public Shape {
public:
    // Pass "color" to the base constructor, then initialize m_radius:
    Circle(double radius, std::string_view color)
        : Shape{color},       // ← base class constructor first
          m_radius{radius}    // ← then derived class members
    {
        std::println("Circle() called");
    }
private:
    double m_radius;
};

// Creating Circle c{5.0, "red"} prints:
// Shape(color) called
// Circle() called

You can never initialize base class member variables directly in a derived class initializer list — even if they are protected. Those members belong to the base class subobject and must be initialized by a base class constructor. The derived class initializer list can only mention base class constructors and direct members of the derived class itself.

Destruction is the reverse. When a derived object goes out of scope, the derived class destructor runs first, then the base class destructor. The rule is consistent: construction goes from most-base to most-derived; destruction goes from most-derived back to most-base.

The static binding problem

If you define the same function in both a base class and a derived class without using the virtual keyword, C++ uses static binding: the compiler decides at compile time which version to call, based on the declared type of the variable, not the actual runtime type of the object. This means that when you hold a derived object through a base class pointer or reference, you will always get the base class version of the function — even if the derived class has its own, more correct version. The example below shows why this matters:

class Shape {
public:
    double area() const { return 0.0; }        // base version
    void describe() const {
        std::println("Area is {}", area());    // which area() is called?
    }
};

class Circle : public Shape {
public:
    explicit Circle(double r) : m_radius{r} {}
    double area() const { return 3.14159 * m_radius * m_radius; }
private:
    double m_radius;
};

Circle c {5.0};
Shape* p {&c};          // base class pointer holds a Circle

p->area();              // calls Shape::area() → 0.0 (WRONG)
c.describe();           // calls describe() from Shape, which calls Shape::area() → 0.0 (WRONG)
c.area();               // calls Circle::area() → 78.5 (correct, direct call on concrete type)

The pointer p has static type Shape*, so the compiler binds every function call through p to Shape's member functions — regardless of what the pointer actually points to at runtime. This is the motivation for virtual functions.

Virtual functions — dynamic dispatch

When you mark a function virtual in the base class, you instruct the compiler to use dynamic binding: instead of deciding the call target at compile time, the correct version is selected at runtime based on the actual type of the object. The function in the derived class is said to override the base class function. Any function you declare virtual in a base class is automatically virtual in all classes derived from it — you do not have to repeat the keyword, though doing so is legal.

class Shape {
public:
    virtual double area() const { return 0.0; }  // virtual: enables dynamic dispatch
    void describe() const {
        std::println("Area is {}", area());       // now calls the right area()!
    }
    virtual ~Shape() = default;                  // virtual destructor (explained below)
};

class Circle : public Shape {
public:
    explicit Circle(double r) : m_radius{r} {}
    double area() const override { return 3.14159 * m_radius * m_radius; }
private:
    double m_radius;
};

class Square : public Shape {
public:
    explicit Square(double side) : m_side{side} {}
    double area() const override { return m_side * m_side; }
private:
    double m_side;
};

Circle c {5.0};
Square s {4.0};
Shape* p {&c};

p->area();      // dynamic: calls Circle::area() → 78.5 ✓
p = &s;
p->area();      // dynamic: calls Square::area() → 16.0 ✓
c.describe();   // calls Shape::describe(), which calls Circle::area() → 78.5 ✓

Dynamic dispatch only works through a pointer or reference to the base class. If you hold a derived object by value in a base class variable, object slicingoccurs: the derived portion is cut off, and you are left with a plain base class object. Polymorphism requires indirection.

// Object slicing — never store derived objects by value in a base container:
std::vector<Shape>  bad_shapes;              // copies lose the derived parts
bad_shapes.push_back(Circle{5.0});           // sliced to just Shape — bad!

// Correct: use pointers (ideally smart pointers):
std::vector<std::unique_ptr<Shape>>  shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Square>(4.0));

for (const auto& s : shapes)
    std::println("Area: {}", s->area());    // correct — 78.5, then 16.0

The override specifier — catching mistakes

The override specifier, placed after the function signature in a derived class, tells the compiler to verify that the function actually overrides a virtual function in the base class. If it does not — because you made a typo, forgot const, or slightly changed the parameter types — the compiler emits an error instead of silently creating a new unrelated function that hides the base class version. Always use overrideon every overriding function.

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

class Circle : public Shape {
public:
    // Correct — matches signature exactly:
    double area() const override { return 3.14159 * m_radius * m_radius; }

    // These would NOT override — compiler catches the mistake:
    // double Area() const override;       // ERROR: no virtual 'Area' in Shape
    // double area() override;             // ERROR: const mismatch — different signature
    // double area(int n) const override;  // ERROR: parameter list differs

private:
    double m_radius {1.0};
};

Note that override is not a keyword in the traditional sense — it is a context-sensitive identifier, so old code that used override as a variable name still compiles. You can also use final to prevent a virtual function from being overridden further down the hierarchy: double area() const override finalmeans “I override the base, and no class below me may override this again.”

Virtual destructors — the rule you must not forget

Because polymorphism involves base class pointers pointing to derived objects, deletion through a base class pointer is a common pattern. The C++ standard states that deleting a derived class object through a base class pointer is undefined behavior unless the base class destructor is virtual. If the destructor is not virtual, only the base class destructor will run, leaving the derived class's cleanup code unexecuted — which may cause resource leaks or worse.

// Dangerous — non-virtual destructor:
class BadBase {
public:
    ~BadBase() { std::println("BadBase destroyed"); }
};
class DangerousDerived : public BadBase {
    int* m_data = new int[100];
public:
    ~DangerousDerived() { delete[] m_data; }  // never called through BadBase*!
};
BadBase* p = new DangerousDerived{};
delete p;   // UB: only ~BadBase() runs, m_data leaks

// Correct — virtual destructor in base:
class Shape {
public:
    virtual ~Shape() = default;    // virtual destructor — always add this
};

class Circle : public Shape {
    // ~Circle() is implicitly virtual too (once base is virtual)
};

Shape* p = new Circle{5.0};
delete p;   // correct: ~Circle() runs, then ~Shape() runs

The rule is simple: if a class has any virtual functions, give it a virtual destructor. The idiomatic way is virtual ~Base() = default; — this marks the destructor virtual while letting the compiler generate the body. Once the base destructor is virtual, all derived class destructors are also virtual, even without the virtual keyword.

Putting it all together

Here is a complete, self-contained example that shows inheritance, virtual functions, override, and virtual destructors working together:

#include <memory>
#include <vector>
#include <print>
#include <numbers>

class Shape {
public:
    virtual double area() const = 0;         // pure virtual: Shape is abstract
    virtual std::string name() const = 0;
    virtual ~Shape() = default;
    void print() const {
        std::println("{}: area = {:.2f}", name(), area());
    }
};

class Circle : public Shape {
public:
    explicit Circle(double r) : m_radius{r} {}
    double area()        const override { return std::numbers::pi * m_radius * m_radius; }
    std::string name()   const override { return "Circle"; }
private:
    double m_radius;
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : m_w{w}, m_h{h} {}
    double area()        const override { return m_w * m_h; }
    std::string name()   const override { return "Rectangle"; }
private:
    double m_w, m_h;
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(3.0));
    shapes.push_back(std::make_unique<Rectangle>(4.0, 5.0));
    shapes.push_back(std::make_unique<Circle>(1.5));

    for (const auto& s : shapes)
        s->print();
}
// Output:
// Circle: area = 28.27
// Rectangle: area = 20.00
// Circle: area = 7.07

Key rules to remember

Use public inheritance only for true is-a relationships

If you cannot say that every derived object is also a valid base object, inheritance is the wrong tool. Model has-a relationships with member variables instead.

Derived constructors must initialize the base via the initializer list

Write Derived(args) : Base(base_args), m_own{val} {}. You cannot initialize base class member variables directly in a derived class initializer list — that is the base constructor's job.

Constructors run base-first; destructors run derived-first

A derived object is assembled from the bottom up (most-base constructor first) and torn down from the top down (most-derived destructor first). This ordering ensures objects are never in a half-constructed state.

Mark the base class function virtual to get dynamic dispatch

Without virtual, calls through a base class pointer always resolve to the base class version at compile time. With virtual, the correct version is selected at runtime based on the actual type of the object.

Always use override in derived classes

override instructs the compiler to verify that the function actually overrides a virtual in the base. This catches typos, const mismatches, and parameter-list differences that would otherwise silently create a new hiding function.

Any class with virtual functions needs a virtual destructor

Deleting a derived object through a base class pointer is undefined behavior unless the base has a virtual destructor. Use virtual ~Base() = default; in every polymorphic base class.

Sign in to track progress