Inheritance is a core pillar of object-oriented programming. It refers to the idea that we can define classes derived from base classes, where the derived classes can inherit the public methods and attributes of the base class. The derived class can also extend the base class’s definition as they see fit (implement their own specific methods/attributes on top of that).
The derived class is an instance of the base class. This means that under the hood, when we declare a Derived
object, we call the Base
constructor, then the Derived
constructor.
Design-wise: the base class guarantees the preconditions and the post-conditions of an operation, and the derived class should respect these guarantees. An override can ask for less and provide more, but it should never require more or promise less.
The basics
Our motivation: it’s tedious to define a class from scratch if it’s similar to another class, since it also forces us to fully understand the specifics of the class we’re copying from. Inheritance allows us to abstract this reuse of code, all changes in the original class will be reflected in sub-classes, and we don’t need to understand what happens under the hood.
If members in the base class are private
, we can’t access them from the derived class. We must instead use the protected
access modifier, to denote data that is private except for derived classes.
We can inherit from more than one base class in C++.
But Java can only inherit one class, i.e., it only allows for single inheritance.
Class relationships
There are two types of relationships we can think about:
- Simple inheritance represents a “is a” relationship.
- If we use the base class within the derived class (as a data member), we can represent a “has a” relationship, i.e., a
Car
has anEngine
.
When we use operator=
, we have two types of operations. The first is upcasting, when we set a derived class to a base class. This is always safe, and an object of a derived class is always substitutable when a base object is required. For example, say Rectangle
inherits from Polygon
. Since a rectangle is a polygon, then we can say:
This is because rectangle is also a polygon class.
The other way around is downcasting, where we cast a base class pointer to a derived class pointer. This may work, but the compiler cannot verify if it’s true, and it’ll lead to undefined behaviour if we’re incorrect.
If , then intuitively it’s fine if: the types match, or we say that a “rectangle” is a “polygon” (p = r
). It’s not fine if we say that is a sub-class of (i.e., we can’t say that a polygon is a rectangle r = p
).
The r1
pointer can’t access all rectangle members as not all of them exist in the base class. Our problem is that we can’t access members of a derived class if the pointer to it is of the base class. The solution is using virtual functions, which facilitates dynamic dispatch.
Inheriting functions
We don’t inherit constructors, copy constructors, operator=, and destructors. We must instead implement our own.
If we want to implement constructors that make use of base classes or methods that call base methods, we can:
Constructors are invoked from the highest base class to derived classes. Destructors are invoked from the lowest-level up to the highest base-class (i.e., last-in, first-out/LIFO).
Think about it like forging a chain link by link from a hook. You have to create the higher links before the lower ones, and to destroy it you have to destroy the lower links before the higher ones.
Language-specific
In these languages:
- In Python, we can inherit from a base class with parentheses:
class Derived(Base)
. - In Java, we can use the
extends
keyword:class Derived extends Base
.
Applications
Inheritance allows us to implement large class hierarchies. Consider streams in C++: