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.

class Person {
	private:
		string name;
		int age;
	public:
		// constructors
		// getters/setters
		void print () {
			cout << "Name: " << name << endl;
		}
}
 
class Student: public Person {
	private:
		int id;
	public:
		Student () { ID = 0; }
		void setNameID(string n, int d) {
			Person::setName(n);
			id = d;
		}
		void print() { // method overriding
			cout << "ID: " << id << endl;
			Person::print();
		}
}

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++.

class TA: public GradStudent, public Employee {
	// some code
}

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 an Engine.

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:

Polygon poly;
Rectangle rec;
poly = rec; // works fine
 
// see below
Polygon& operator=(Polygon& rhs) {
	// ...
}
r.operator=(p); // not rectangle

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.

rec = poly; // static cast

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).

Rectangle* r1;
Polygon* p1;
p1 = &r;
p1->set(3, 4); // calls set method in polygon
p1->area(); // error as area is a member of rectangle but not polygon
r1 = &p; // gives us an error

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:

class Circle {
	protected:
		int radius;
	public:
		Circle(int r) { radius = r; }
		void print() { cout << "Circle: " << radius << endl; }
};
 
class Wheel: public Circle {
	protected:
		int radius; // new variable, identical name
	public:
		Wheel(int rc, int rw): Circle(rc) {
			radius = rw;
		}
		void print() {
			Circle::print();
			cout << "Wheel: " << radius << endl;
		}
}
 
class Tire: public Wheel {
	protected:
		int radius; // yet another
	public:
		Tire(int rc, int rw, int rt): Wheel(rc, rw) {
			radius = rt;
		}
}

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++: