Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 1 of 9

Inheritance says one type is a kind of another. A Circle is a Shape; anywhere a Shape is expected, a Circle will do.

That relationship is the only reason to use it. If you find yourself inheriting to reuse an implementation rather than to express is-a, lesson 16.8 has the argument for why that is usually a mistake.

The mechanics

#include <iostream>
#include <string>

class Shape {
protected:
    std::string name_;          // accessible to derived classes, not to outsiders

public:
    explicit Shape(std::string n) : name_(std::move(n)) {
        std::cout << "  Shape(" << name_ << ")\n";
    }
    ~Shape() { std::cout << "  ~Shape(" << name_ << ")\n"; }

    const std::string& name() const { return name_; }
    double area() const { return 0.0; }
};

class Circle : public Shape {
    double radius_;

public:
    Circle(double r) : Shape("circle"), radius_(r) {
        std::cout << "  Circle(" << radius_ << ")\n";
    }
    ~Circle() { std::cout << "  ~Circle\n"; }

    double area() const { return 3.14159 * radius_ * radius_; }
};

class Rectangle : public Shape {
    double w_, h_;

public:
    Rectangle(double w, double h) : Shape("rectangle"), w_(w), h_(h) {
        std::cout << "  Rectangle(" << w_ << "x" << h_ << ")\n";
    }
    ~Rectangle() { std::cout << "  ~Rectangle\n"; }

    double area() const { return w_ * h_; }
};

int main() {
    std::cout << "constructing a Circle:\n";
    {
        Circle c(2.0);
        std::cout << "  name: " << c.name() << ", area: " << c.area() << "\n";
        std::cout << "destroying:\n";
    }

    std::cout << "\nconstructing a Rectangle:\n";
    {
        Rectangle r(3.0, 4.0);
        std::cout << "  area: " << r.area() << "\n";

        // A derived object IS-A base object: this reference binds.
        Shape& as_shape = r;
        std::cout << "  through Shape&: name " << as_shape.name()
                  << ", area " << as_shape.area() << "  <-- calls Shape::area\n";
        std::cout << "destroying:\n";
    }
    return 0;
}
constructing a Circle:
  Shape(circle)
  Circle(2)
  name: circle, area: 12.5664
destroying:
  ~Circle
  ~Shape(circle)

constructing a Rectangle:
  Shape(rectangle)
  Rectangle(3x4)
  area: 12
  through Shape&: name rectangle, area 0  <-- calls Shape::area
destroying:
  ~Rectangle
  ~Shape(rectangle)

Three things in that output, and the last one is the whole reason Module 16 continues.

The base is constructed first, the derived second. Shape(circle) prints before Circle(2). That ordering is not arbitrary: the derived constructor's body may use base members, so the base must be complete before it runs.

Destruction is exactly reversed. ~Circle then ~Shape. The derived part is torn down while the base is still intact, which is the only order that makes sense.

as_shape.area() returned 0, not 12. The reference refers to a Rectangle, and Rectangle::area() would have returned 12 — but the call went to Shape::area().

That third result is the problem lesson 16.2 solves with virtual. For now, understand why it happens: area() is not virtual, so the compiler picks which function to call based on the static type of the expression — the type written in the source, Shape&. It does not look at what the object actually is.

Access specifiers

// class Shape {
// public:      accessible to everyone
// protected:   accessible to Shape and to classes derived from Shape
// private:     accessible only to Shape (the default for `class`)
// };

protected exists specifically for inheritance: it is the interface a base offers to its derived classes but not to the outside world.

Use it sparingly. A protected member is part of your public API as far as derived classes are concerned — changing it breaks them. Many well-designed hierarchies have no protected data at all, only protected functions.

The three kinds of inheritance

// class Derived : public Base     — is-a. Public members stay public.
// class Derived : protected Base  — implemented-in-terms-of; rare.
// class Derived : private Base    — implemented-in-terms-of; also rare.

public is what you want in essentially every case. It is the only one that expresses is-a, and the only one that lets a Derived* convert to a Base*.

private inheritance says "I use Base's implementation but I am not a Base". That is composition with extra steps, and lesson 16.8 argues you should just use a member.

Note that class defaults to private inheritance and struct defaults to public — which is why class Derived : Base almost never means what someone new to C++ expects. Write public explicitly, always.

Constructors are not inherited by default

A derived class must pass arguments to its base explicitly:

// Circle(double r) : Shape("circle"), radius_(r) {}
//                    ^^^^^^^^^^^^^^^ the base is initialised first, in the
//                                     member initialiser list

If the base has no default constructor and you forget, the compiler rejects it. That is the compiler enforcing the rule that a base must be fully constructed before the derived body runs.

Since C++11 you can inherit constructors wholesale:

// class Circle : public Shape {
// public:
//     using Shape::Shape;      // Circle now has all of Shape's constructors
// };

Useful for a thin derived class that adds no state. Dangerous for one that does, because an inherited constructor does not initialise the derived members.

Initialisation order is fixed

This one surprises people and -Wall warns about it:

// class Derived : public Base {
//     int a;
//     int b;
// public:
//     Derived() : b(1), a(b) {}     // WRONG: a is initialised FIRST
// };

Members are initialised in declaration order, not in the order written in the initialiser list. a is declared before b, so a(b) reads b before b exists.

The full order for any construction:

  1. Base class (or bases, left to right)
  2. Members, in declaration order
  3. The constructor body

GCC reports the mismatch as -Wreorder, which lesson 11.1 listed among the warnings worth having.

What inheritance gives you, and what it does not

It gives you:

  • A Derived& or Derived* converts to Base& or Base* implicitly.
  • Derived objects inherit the base's members and non-private functions.
  • One function can accept any type in the hierarchy.

It does not yet give you:

  • Calling the right function through a base reference. That needs virtual (lesson 16.2).
  • Safe deletion through a base pointer. That needs a virtual destructor (lesson 16.3), and getting it wrong is the module's most dangerous bug.
  • Copying a derived object through a base reference. That is slicing (lesson 16.6).

Those three gaps are the next three lessons, and none of them is optional.

Try it

Build a three-level hierarchy — Animal, Dog : Animal, Puppy : Dog — with a message printed in every constructor and destructor. Predict the full construction and destruction order before running it. Then add a member to each class and use -Wall to catch a deliberately mis-ordered initialiser list.

Checkpoint

  • Inheritance expresses is-a. Use it for that and not for implementation reuse.
  • Construction runs base first, then members in declaration order, then the body.
  • Destruction is the exact reverse: body, members, base.
  • protected is the interface a base offers derived classes; prefer functions over data.
  • Always write public inheritance explicitly — class defaults to private.
  • A derived constructor must initialise its base in the member initialiser list.
  • using Base::Base; inherits constructors; safe only when the derived class adds no state.
  • Members initialise in declaration order, not initialiser-list order — -Wreorder catches it.
  • A Derived& converts to Base& implicitly; that is the point of public inheritance.
  • Calling a non-virtual function through a Base& calls the base version — measured above.
  • Three gaps remain: virtual dispatch, virtual destructors, and slicing.

Questions about this lesson

Log in to ask a question.