8.5 Inheritance and Abstract Classes
Inheritance expresses an "is a" relationship. A Food is a Product. A Circle is a Shape. C++ inheritance is powerful, but it is not the default solution for sharing code. Use it when callers should be able to treat several more specific types through one more general type.
This section focuses on the class design side: base classes, derived classes, access, construction order, override, pure virtual functions, and abstract interfaces. The next section focuses on runtime polymorphism.
Base classes and derived classes
A base class stores shared behavior. A derived class adds or specializes behavior.
class Product {
private:
std::string name_;
double price_;
public:
Product(std::string name, double price)
: name_(name), price_(price) {}
std::string name() const { return name_; }
double price() const { return price_; }
virtual std::string label() const {
return name_ + " $" + std::to_string(price_);
}
};
class Food : public Product {
private:
int calories_;
public:
Food(std::string name, double price, int calories)
: Product(name, price), calories_(calories) {}
std::string label() const override {
return "Food: " + name() + ", " + std::to_string(calories_) + " kcal";
}
};public Product means public members of Product stay public when accessed through Food. The derived constructor calls the base constructor in its initialization list. Base-class construction happens before derived-class construction, because the base part of the object must exist before the derived part can build on it.
The keyword override is not required, but you should use it. It asks the compiler to check that the function really overrides a virtual function from the base class. If you misspell the function name or change a parameter by accident, the compiler catches it.
Pure virtual functions and abstract classes
Sometimes a base class is not meant to create objects. It is meant to define an interface.
class Shape {
public:
virtual double area() const = 0;
virtual std::string name() const = 0;
virtual ~Shape() = default;
};The = 0 makes a function pure virtual. A class with at least one pure virtual function is abstract, so this is illegal:
Shape shape; // Error: Shape is abstract.A derived class becomes concrete only after it implements all pure virtual functions.
class Circle : public Shape {
private:
double radius_;
public:
explicit Circle(double radius) : radius_(radius) {}
double area() const override {
return 3.1415926 * radius_ * radius_;
}
std::string name() const override {
return "circle";
}
};Abstract classes are useful when you want to promise behavior without committing to one implementation. The base class says "every shape can report an area"; the derived classes decide how.
Designing inheritance carefully
Inheritance exposes a long-term relationship between classes, so design it deliberately.
Use inheritance when:
- the derived class truly is a specialized version of the base class;
- callers benefit from a shared interface;
- the base class is stable enough for derived classes to depend on it.
Avoid inheritance when:
- you only want to reuse a few helper functions;
- the derived class is only "implemented using" the base class;
- changing the base class would constantly break derived classes.
Composition is often simpler:
class EmailNotification {
private:
Message message_;
};But when several notification types must all provide the same operation, an abstract base class is a clean fit:
class Notification {
public:
virtual ~Notification() = default;
virtual std::string preview() const = 0;
};The interface stays small, and every derived class is forced to provide the required behavior.