Chapter 10 - Notes

Inheritance

Inheritance is a solution to a problem where components being managed have similar attributes, differing only minutely in details and behaviour.

It allows us to abstract the common behaviour into a common base class, and have each component inherit from this base class.

Examples

Base Class

Example Derived Classes

Fish

Goldfish, Carp, Tuna

Mammal

Human, Elephant, Lion, Platypus

Bird

Crow, Parrot, Ostrich

Shape

Circle, Polygon

Polygon

Triangle, Octagon

The derived classes, in almost all instances, should be more specific than that of its base class.

Syntax

class Base 
{
   // implementation of Base 
};

class Derived: <access-specifier> Base 
{
   // implementation of Derived 
}

The access-specifier is either public (most common), private, or protected.

When reading about inheritance, you will come across different terminology that mean that same thing.

  • inherits from is the same as derives from

  • base class is the same as super class

  • derived class is the same as subclass

Using the `protected` access specifier

The protected access modifier allows one to define member variables and functions in a base class that are only accessible within the scope of the derived class.

Base Class Initialisation

If an overloaded constructor of the base class is defined, it can be invoked using the initialisation list syntax.

class Base {
public:
    Base(int someNumber) {
        // Use someNumber 
    }
}

class Derived: public Base {
public:
    Derived(): Base(25) // Instantiate Base with argument 25
    {
        // Derived class constructor code 
    }
}

This is useful for ensuring any required member variables are provided.

Overriding Base Class Methods

If a derived class implements a function with the same prototype to that of the base class that it inherits from, it effectively overrides the base class method.

class Fish {
public: 
    void speak() {
        std::cout << "blup blup" << std::endl;
    }
}

class Carp {
public:
    void speak() {
        std::cout << "swish swoosh" << std::endl;
    }
}

If .speak() was invoked using an instance of Carp, then the Carp::speak() method is invoked, overriding the Fish::speak() method.

Invoking Overridden Base Class Methods

Through an instance of a derived class

If you want to be able to invoke the Fish::speak() method via an instance of Carp, you need to use the scope resolution operator:

Carp carp;
carp.Fish::speak();

Within the scope of a derived class

If a derived class needs to re-use and invoke the implementation of an overridden method, you can also use the scope resolution operator:

class Carp: public Fish {
public:
    Carp(): Fish() {}
    
    void speak() {
        Fish::speak();
        std::cout << "swish swoosh" << std::endl;
    }
}

This would result in "blup blup" followed by "swish swoosh".

Hiding a Base Class' Methods

Overriding also allows derived classes to "hide" overloaded versions of a function in its base class.

class Fish {
public:
    void Swim();
    void Swim(bool quickly); // Overloaded version
}

class Tuna: public Fish {
public:
    void Swim();
}

int main() {
    Tuna tuna;
    Tuna.Swim(false);  // Compileation error: Tuna::Swim() hides Fish::Swim(bool)
}

If you want to be able to invoke Fish::Swim(bool), the following options are available:

  • Use the scope resolution operator

tuna.Fish::Swim(false);
  • Use keyword using in Tuna to unhide the Fish::Swim(bool) function:

class Tuna: public Fish {
public:
    using Fish::Swim;  // unhide all Fish::Swim methods 
    
    void Swim(); 
}
  • (Preferred) Override all overloaded variants of Fish::Swim in Tuna

class Tuna: public Fish {
public:
    void Swim();
    void Swim(bool quickly);  // calling tuna.Swim(false) invokes Tuna::Swim(bool)
}

Order of Construction

Base class objects are instantiated before the derived class. This ensures that any member attributes of the base class are ready to be used in the constructor of the derived class.

Order of Destruction

The order of destruction is opposite to that of construction. When an instance of a derived class foes out of scope, the destructor of the derived class is first called, then the base class.

Inheritance Types

Private Inheritance

class Base {
    // ...
}

class Derived: private Base {  // private inheritance 
    // ...
}

Private inheritance of a base class means that all public members and attributes of the base class are private to anyone with an instance of the derived class.

Public members and methods of Base can be consumed within the scope of Derived, but not by anyone else in possession of an instance of Derived.

Private inheritance does not imply an "is-a" relationship between Derived and Base, since none of the behaviours of Base can be invoked/accessed with an instance of Derived.

Private inheritance implies a "has-a" relationship, as it can invoke behaviours of Base within its scope.

Private inheritance is not a common pattern. The better way to express a "has-a" relationship is through composition; this is similar to the Library exercise in Exercise 9.

A Library contains (has-a) Bookshelves, which contains (has-a) Books. This composition is expressed through member variables of the owned type, with no inheritance required.

Protected Inheritance

class Base {
    // ...
}

class Derived: protected Base {  // private inheritance 
    // ...
}

Protected inheritance is similar to private inheritance in the following ways:

  • It also expresses a has-a relationship.

  • It also lets the derived class access all public and protected members of Base.

  • Those outside the inheritance hierarchy with an instance of Derived cannot access public members of Base

The difference is when the Derived class is further derived from:

class Derived2: protected Derived {
    // Can access public & protected members of Base 
}

Protected inheritance allows subclasses of the subclass to access public and protected members of the Base class. This is nor possible with private inheritance.

This is also not a common pattern for the same reasons as private inheritance. There are better ways to express "has-a" relationships between classes without inheritance.

Instance Slicing

Slicing occurs when an object of a derived class is copied into an object of a base class.

Derived derived;
Base base = derived; // Valid, will compile and run. 

What occurs is that the compiler copies only data of the Base part of Derived, in other words, not the complete object. The information contained in the Derived instance is lost in this process. This is known as slicing or narrowing.

To avoid this, don't pass parameters by value, pass them instead as pointers to the base class.

Multiple Inheritance

A derived class may inherit more than one base class.

class A {
    void foo();
}

class B {
    void bar();
}

class Derived: public A, public B {
    // ... 
}

int main() {
    Derived derived;
    derived.foo();  // Yes, from A
    derived.bar();  // Yes, from B
}

Preventing Inheritance

A class declaration suffixed with final ensures that the class cannot be inherited from.

class Foo {
    // ...
}

class Bar final: public Foo {
    // ...
}

class Baz: public Bar {  // Compile error: Bar is `final` class
    // ... 
}

Last updated

Was this helpful?