Skip to content
Lesson 14 of 15

Step 1 of 7 · Reading · ~4 min

Learn

Classes, Inheritance, and Templates

Inheritance and Virtual Functions

By the end of this lesson you can build a small family of classes that share one interface, and write a single function that works on any member of that family and still calls the right code for each one.

A derived class and its base

class Hourly : public Employee makes Hourly a derived class of the base class Employee: every Hourly is also an Employee, and a const Employee& can refer to one. The derived constructor builds its base part first, in its member initializer list, exactly like initializing a member:

class Salaried : public Employee {
    double monthly;
public:
    explicit Salaried(double m) : monthly{m} {}
    double pay() const override { return monthly; }
    std::string title() const override { return "salaried"; }
};

class Lead : public Salaried {
public:
    explicit Lead(double m) : Salaried{m} {}   // hand m to the base constructor
};

Lead has no monthly of its own; it inherits the one inside Salaried, along with both functions. Inheritance can go several levels deep.

Virtual and pure virtual functions

A virtual function is a member function whose call is decided at run time by the object's real type, not by the type of the reference you call it through. = 0 makes it pure virtual: the base declares it but gives no body, so every concrete derived class must supply one. A class with a pure virtual function is abstract: you cannot create an Employee object, only objects of derived classes.

class Employee {
public:
    virtual double pay() const = 0;
    virtual std::string title() const = 0;
    virtual ~Employee() = default;
};

class Hourly : public Employee {
    double rate, hours;
public:
    Hourly(double r, double h) : rate{r}, hours{h} {}
    double pay() const override { return rate * hours; }
    std::string title() const override { return "hourly"; }
};

void payslip(const Employee& e) {
    std::cout << e.title() << ": " << e.pay() << "\n";
}

Hourly h{20, 7.5};
payslip(h);        // hourly: 150

override asks the compiler to check that you really are replacing a base virtual function; a misspelt name or a missing const becomes a compile error instead of a silent new function. The virtual destructor matters when you delete a derived object through a base pointer (Employee* p = new Hourly{20, 8}; delete p;): without virtual, only the base part is destroyed.

Overriding only part of the interface

A class two levels down may override just one function and inherit the rest:

class Lead : public Salaried {
public:
    explicit Lead(double m) : Salaried{m} {}
    std::string title() const override { return "lead"; }
};
// Lead{4000}: title() is "lead", pay() comes from Salaried

Leave out that override and payslip prints salaried: 4000 for a lead. The code compiles; the output is just wrong.

The trap: no virtual, no dispatch

If the base gives title() a body but no virtual, the call is fixed at compile time by the reference type:

class Staff {
public:
    std::string title() const { return "staff"; }   // not virtual
};
class Intern : public Staff {
public:
    std::string title() const { return "intern"; }
};
void show(const Staff& s) { std::cout << s.title() << "\n"; }

Intern i;
show(i);    // staff   (no virtual: static dispatch)
            // intern  (with virtual in Staff)

Common mistakes

  • Leaving out a factor in one class's formula. A formula with a factor of one half is easy to drop; the program compiles and every result from that class comes out exactly double.
  • Not overriding the name in a class derived from a concrete class. The inherited version runs, so the object reports its parent's name, like salaried above.
  • Bypassing the shared function. Printing inside each branch instead of through one function taking const Base& lets the formats drift apart, and one branch ends up with a missing space or label.
  • Forgetting the empty case. When the count is 0 the loop never runs, but the summary line must still print.

Your exercise

Build the Shape hierarchy described in the exercise: an abstract base, three shapes (one derived from another), and the report function that prints any of them through a const Shape&. The starter already parses every line and prints the total; the most likely slip is a square that calls itself a rectangle.

Up nextTemplates IntroductionClasses, Inheritance, and Templates

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…