25. Classes – Inheritance and Polymorphism – Modern C++ for Absolute Beginners: A Friendly Introduction to C++ Programming Language and C++11 to C++20 Standards

© Slobodan Dmitrović 2020
S. DmitrovićModern C++ for Absolute Beginnershttps://doi.org/10.1007/978-1-4842-6047-0_25

25. Classes – Inheritance and Polymorphism

Slobodan Dmitrović1 
(1)
Belgrade, Serbia
 

In this chapter, we discuss some of the fundamental building blocks of object-oriented programming, such as inheritance and polymorphism.

25.1 Inheritance

We can build a class from an existing class. It is said that a class can be derived from an existing class. This is known as inheritance and is one of the pillars of object-oriented programming, abbreviated as OOP. To derive a class from an existing class, we write:
class MyDerivedClass : public MyBaseClass {};
A simple example would be:
class MyBaseClass
{
};
class MyDerivedClass : public MyBaseClass
{
};
int main()
{
}

In this example, MyDerivedClass inherits the MyBaseClass.

Let us get the terminology out of the way. It is said that MyDerivedClass is derived from MyBaseClass, or MyBaseClass is a base class for MyDerivedClass. It is also said that MyDerivedClass is MyBaseClass. They all mean the same thing.

Now the two classes have some sort of relationship. This relationship can be expressed through different naming conventions, but the most important one is inheritance. Derived class and objects of a derived class can access public members of a base class:
class MyBaseClass
{
public:
    char c;
    int x;
};
class MyDerivedClass : public MyBaseClass
{
    // c and x also accessible here
};
int main()
{
    MyDerivedClass o;
    o.c = 'a';
    o.x = 123;
}
The following example introduces the new access specifier called protected:. The derived class itself can access protected members of a base class. The protected access specifier allows access to the base class and derived class, but not to objects:
class MyBaseClass
{
protected:
    char c;
    int x;
};
class MyDerivedClass : public MyBaseClass
{
    // c and x also accessible here
};
int main()
{
    MyDerivedClass o;
    o.c = 'a';    // Error, not accessible to object
    o.x = 123;    // error, not accessible to object
}
The derived class cannot access private members of a base class:
class MyBaseClass
{
private:
    char c;
    int x;
};
class MyDerivedClass : public MyBaseClass
{
    // c and x NOT accessible here
};
int main()
{
    MyDerivedClass o;
    o.c = 'a';    // Error, not accessible to object
    o.x = 123;    // error, not accessible to object
}
The derived class inherits public and protected members of a base class and can introduce its own members. A simple example:
class MyBaseClass
{
public:
    char c;
    int x;
};
class MyDerivedClass : public MyBaseClass
{
public:
    double d;
};
int main()
{
    MyDerivedClass o;
    o.c = 'a';
    o.x = 123;
    o.d = 456.789;
}

Here we inherited everything from the MyBaseClass class and introduced a new member field in MyDerivedClass called d. So, with MyDerivedClass, we are extending the capability of MyBaseClass. The field d only exists in MyDerivedClass and is accessible to derived class and its objects. It is not accessible to MyBaseClass class as it does not exist there.

Please note that there are other ways of inheriting a class such as through protected and private inheritance, but the public inheritance such as class MyDerivedClass : public MyBaseClass is the most widely used, and we will stick to that one for now.

A derived class itself can be a base class. Example:
class MyBaseClass
{
public:
    char c;
    int x;
};
class MyDerivedClass : public MyBaseClass
{
public:
    double d;
};
class MySecondDerivedClass : public MyDerivedClass
{
public:
    bool b;
};
int main()
{
    MySecondDerivedClass o;
    o.c = 'a';
    o.x = 123;
    o.d = 456.789;
    o.b = true;
}

Now our class has everything MyDerivedClass has, which includes everything MyBaseClass has, plus an additional bool field. It is said the inheritance produces a particular hierarchy of classes.

This approach is widely used when we want to extend the functionality of our classes.

The derived class is compatible with a base class. A pointer to a derived class is compatible with a pointer to a base class. This allows us to utilize polymorphism, which we will talk about in the next chapter.

25.2 Polymorphism

It is said that the derived class is a base class. Its type is compatible with the base class type. Also, a pointer to a derived class is compatible with a pointer to the base class. This is important, so let’s repeat this: a pointer to a derived class is compatible with a pointer to a base class. Together with inheritance, this is used to achieve the functionality known as polymorphism. Polymorphism means the object can morph into different types. Polymorphism in C++ is achieved through an interface known as virtual functions. A virtual function is a function whose behavior can be overridden in subsequent derived classes. And our pointer/object morphs into different types to invoke the appropriate function. Example:
#include <iostream>
class MyBaseClass
{
public:
    virtual void dowork()
    {
        std::cout << "Hello from a base class." << '\n';
    }
};
class MyDerivedClass : public MyBaseClass
{
public:
    void dowork()
    {
        std::cout << "Hello from a derived class." << '\n';
    }
};
int main()
{
    MyBaseClass* o = new MyDerivedClass;
    o->dowork();
    delete o;
}

In this example, we have a simple inheritance where MyDerivedClass is derived from MyBaseClass.

The MyBaseClass class has a function called dowork() with a virtual specifier. Virtual means this function can be overridden/redefined in subsequent derived classes, and the appropriate version will be invoked through a polymorphic object. The derived class has a function with the same name and same type of arguments (none in our case) in the derived class.

In our main program, we create an instance of a MyDerivedClass class through a base class pointer. Using the arrow operator -> we invoke the appropriate version of the function. Here the o object morphs into different types to invoke the appropriate function. Here it invokes the derived version. That is why the concept is called polymorphism.

If there were no dowork() function in the derived class, it would invoke the base class version:
#include <iostream>
class MyBaseClass
{
public:
    virtual void dowork()
    {
        std::cout << "Hello from a base class." << '\n';
    }
};
class MyDerivedClass : public MyBaseClass
{
public:
};
int main()
{
    MyBaseClass* o = new MyDerivedClass;
    o->dowork();
delete o;
}
Functions can be pure virtual by specifying the = 0; at the end of the function declaration. Pure virtual functions do not have definitions and are also called interfaces. Pure virtual functions must be re-defined in the derived class. Classes having at least one pure virtual function are called abstract classes and cannot be instantiated. They can only be used as base classes. Example:
#include <iostream>
class MyAbstractClass
{
public:
    virtual void dowork() = 0;
};
class MyDerivedClass : public MyAbstractClass
{
public:
    void dowork()
    {
        std::cout << "Hello from a derived class." << '\n';
    }
};
int main()
{
    MyAbstractClass* o = new MyDerivedClass;
    o->dowork();
delete o;
}
One important thing to add is that a base class must have a virtual destructor if it is to be used in a polymorphic scenario. This ensures the proper deallocation of objects accessed through a base class pointer via the inheritance chain:
class MyBaseClass
{
public:
    virtual void dowork() = 0;
    virtual ~MyBaseClass() {};
};

Please remember that the use of operator new and raw pointers is discouraged in modern C++. We should use smart pointers instead. More on this, later in the book.

So, three pillars of object-oriented programming are:
  • Encapsulation

  • Inheritance

  • Polymorphism

Encapsulation is grouping the fields into different visibility zones, hiding implementation from the user, and exposing the interface, for example.

Inheritance is a mechanism where we can create classes by inheriting from a base class. Inheritance creates a certain class hierarchy and relationship between them.

Polymorphism is an ability of an object to morph into different types during runtime, ensuring the proper function is invoked. This is achieved through inheritance, virtual and overridden functions, and base and derived class pointers.