In object-oriented programming, inheritance is the idea that a “derived” class can “inherit” the public methods and attributes of a “base” class (and also implement its own specific methods/attributes on top of that). For example, an ordered list could inherit everything from a linked list class, but redefine insert()
. In Java we use the extends
keyword.
The derived class is an instance of the base class. What this means is when we declare a Student
object, we call the Person
constructor then the Student
constructor below the hood.
Inheritance allows us to implement large class hierarchies. Take, for instance, IO streams in C++:
The basics
Our motivation is that it’s pretty tedious to re-implement a class from scratch if it’s similar to another class. This also forces us to understand fully the specifics of the class we’re “copying” from. So we inherit methods/attributes instead. This allows us to reuse code (and all changes in the original class will be reflected in sub-classes), and doesn’t require us to understand the specifics of what happens under the hood.
We can also embed the base class in the derived class (as an attribute) to represent a “has a” relationship. Inheritance itself represents an “is a” relationship.
If members are private, we can’t access them from the derived class. We also don’t inherit the constructors of Person
. If we re-define an inherited method, we override it. We can instead use the protected
access modifier to denote otherwise private data except for derived classes.
We can also inherit from more than one base class in C++.
But Java can only inherit one class, i.e., it only allows for single inheritance.
Redefining or overloading
We never inherit constructors, copy constructors, operator=, and destructors — we must implement our own. This is the same thing we do with single classes that use dynamic memory allocation.
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.
Mixing types
We also run into a problem when using operator=
. Take the following example:
Because there’s mismatched types, we can’t use operator=
. If rectangle is a derived class of polygon, then we can say that p=r
, because rectangle is also a polygon class. But the other way around doesn’t work.
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.