Road to C++ Programmer #4 - Inheritance

Last Edited: 9/10/2024

The blog post introduces the concept of inheritance in C++.

C++ References

Inheritance

One unique feature of a class object is how we can inherit attributes and methods from another class, which helps reduce code duplication. The following is an example of how inheritance can be used.

class MenuItem {
    public:
        string name;
        float calories;
    
        void print () {
            cout << name << " has " << calories << " kcals." << endl;
        }
};
 
class Drink : public MenuItem {
    public:
        float ml;
 
        void kcals_per_ml () {
            cout << "kcals/ml: " << calories/ml << "." << endl;
        }
};
 
int main () {
 
    Drink lemonade;
    lemonade.name = "Lemonade";
    lemonade.calories = 105;
    lemonade.ml = 250;
 
    lemonade.print(); // => Lemonade has 105 kcals.
    lemonade.kcals_per_ml(); // => kcals/ml: 0.42.
 
    return 0;
}

Without defining the attributes like name, calories, and the method print, a Drink object can access them by inheriting from MenuItem. The inherited class is called the base class or parent class, and the class inherited from the base class is called the derived class or child class. You can also create classes that inherit from a derived class, as shown below.

class HotDrink : public Drink {
    public:
        float temperature;
 
        void serving_temperature () {
            cout << "Temperature: " << temperature << "." << endl;
        }
};
 
int main () {
    HotDrink hot_chocolate;
    hot_chocolate.name = "Hot Chocolate";
    hot_chocolate.carolies = 154;
    hot_chocolate.ml = 200;
    hot_chocolate.temperature = 70;
 
    hot_chocolate.print(); // => Hot Chocolate has 154 kcals.
    hot_chocolate.kcals_per_ml(); // => kcals/ml: 0.77.
    hot_chocolate.serving_temperature(); // => Temperature: 70.
    return 0;
}

The HotDrink class inherits from the Drink class, which inherits from MenuItem. Hence, the HotDrink class object has access to all the member variables and functions of both Drink and MenuItem.

Constructor & Deconstructor

When defining constructors in the derived class, the default constructor of the base class is implicitly run if no constructors in the base class are specified within the derived class constructor.

class BaseClass {
    BaseClass () {
        cout << "BaseClass Constructor" << endl;
    }
};
 
class DerviedClass : public BaseClass {
    DerivedClass () {
        cout << "DerivedClass Constructor" << endl;
    }
};
 
int main () {
    DerviedClass derived;
    // => BaseClass Constructor
    //    DerivedClass Constructor
    return 0
}

When there is no default constructor set up for the base class, compilation will fail unless the derived class explicitly uses a parameterized constructor of the base class.

class BaseClass {
    public:
        string name;
    BaseClass (string name) {
        cout << "BaseClass Param Constructor" << endl;
    }
};
 
class DerviedClass : public BaseClass {
    DerivedClass () : BaseClass("default") {
        cout << "DerivedClass Constructor" << endl;
    }
};
 
int main () {
    DerviedClass derived;
    // => BaseClass Param Constructor
    //    DerivedClass Constructor
    return 0
}

Destructors work in the same way as constructors, except that the destructor of the derived class runs first.

class BaseClass {
    public:
        string name;
 
    BaseClass (string name) {
        cout << "BaseClass Constructor" << endl;
    }
    ~BaseClass () {
        cout << "BaseClass Destructor" << endl;
    }
};
 
class DerviedClass : public BaseClass {
    DerivedClass () : BaseClass("default") {
        cout << "DerivedClass Constructor" << endl;
    }
    ~DerivedClass () {
        cout << "DerivedClass Destructor" << endl;
    }
};
 
int main () {
    DerviedClass derived;
    // => BaseClass Constructor
    //    DerivedClass Constructor
    //    Derived Class Destructor
    //    BaseClass Destructor
    return 0
}

Multiple Inheritance

In C++, you are not limited to inheriting from only one class; you can inherit from multiple classes. However, if you have the same attribute or method names in multiple parent classes, ambiguity can arise.

class BaseClass1 {
    public:
        int value;
        void print() {
            cout << "BaseClass1" << endl;
        }
};
 
class BaseClass2 {
    public:
        int value;
        void print() {
            cout << "BaseClass2" << endl;
        }
};
 
class DerivedClass : public BaseClass1, public BaseClass2 {
    public:
};
 
int main () {
    DerivedClass derived;
    cout << derived.value << endl; // => err
    derived.print(); // => err
    return 0;
}

You can either overload the member variables and functions by specifying the implementations in the DerivedClass, or specify which parent class implementation to use by using ::.

int main () {
    DerivedClass derived;
    cout << derived.BaseClass1::value << endl; // => BaseClass 1 value
    derived.BaseClass2::print(); // => BaseClass2
    return 0;
}

You can also override the functions in the derived class using :: to resolve ambiguity. Ambiguity can also occur when parent classes share the same grandparent class.

class GrandParentClass {
    public:
        int common_value;
};
 
class BaseClass1 : public GrandParentClass {
};
 
class BaseClass2 : public GrandParentClass {
};
 
class DerivedClass : public BaseClass1, public BaseClass2 {
};

It is ambiguous whether the common_value in DerivedClass comes from BaseClass1 or BaseClass2. To solve this, we can use the virtual keyword when the parent classes inherit from the grandparent class.

class GrandParentClass {
    public:
        int common_value;
};
 
class BaseClass1 : virtual public GrandParentClass {
};
 
class BaseClass2 : virtual public GrandParentClass {
};
 
class DerivedClass : public BaseClass1, public BaseClass2 {
};

The virtual keyword allows DerivedClass to inherit members of the grandparent class without ambiguity. Using this approach, the DerivedClass constructor will run the GrandParentClass default constructor by default. Therefore, if you want to use parameterized or default constructors of the grandparent and parent classes, you will need to specify which one to use with ::. As you can see, multiple inheritance introduces complexity when resolving ambiguity, which is often criticized.

Dynamic Binding

In the following example, you might expect the overridden methods to be called for the derived class.

class BaseClass {
    public:
        int a;
        void print () {
            cout << a << endl;
        };
    BaseClass (int a) : a(a) {};
};
 
class DerivedClass : public BaseClass {
    public:
        int b;
        void print () {
            cout << a << ", " << b << endl;
        };
    DerivedClass (int a, int b) : BaseClass(a), b(b) {};
};
 
int main () {
    BaseClass* array[] = {
        new BaseClass(1),
        new BaseClass(2),
        new DerivedClass(3,4),
        new DerivedClass(5,6)
    };
 
    for (int i = 0; i < 4; i++) {
        array[i] -> print();
    } // => 1 2 3 5 not 1 2 3 4 5 6
 
    for (int i = 0; i < 4; i++) {
        delete array[i];
    }
 
    return 0;
}

However, all objects run BaseClass.print() because the array is a pointer to the BaseClass. By default, methods are statically bound, meaning that the method to use is decided at compile time based on the type of the declared pointer. To use the overridden method for DerivedClass or to implement dynamic binding, you can use the virtual keyword when defining the print function in the BaseClass.

class BaseClass {
    public:
        int a;
        virtual void print () {
            cout << a << endl;
        };
    BaseClass (int a) : a(a) {};
};
 
int main () {
    BaseClass* array[] = {
        new BaseClass(1),
        new BaseClass(2),
        new DerivedClass(3,4),
        new DerivedClass(5,6)
    };
 
    for (int i = 0; i < 4; i++) {
        array[i] -> print();
    } // => 1 2 3 4 5 6
 
    for (int i = 0; i < 4; i++) {
        delete array[i];
    }
 
    return 0;
}

Dynamic binding determines the type of the object at runtime, making the program more flexible, but also slower.

Virtual Destructor

When you run the code below, you'll notice that only the BaseClass destructors are executed because they are statically bound.

class BaseClass {
    public:
        int a;
        void print () {
            cout << a << endl;
        };
    BaseClass (int a) : a(a) {};
 
    ~BaseClass () {
        cout << "BaseClass Destructor" << endl;
    }
};
 
class DerivedClass : public BaseClass {
    public:
        int b;
        void print () {
            cout << a << ", " << b << endl;
        };
    DerivedClass (int a, int b) : BaseClass(a), b(b) {};
 
    ~DerivedClass () {
        cout << "DerivedClass Destructor" << endl;
    }
};
 
int main () {
    BaseClass* array[] = {
        new BaseClass(1),
        new BaseClass(2),
        new DerivedClass(3,4),
        new DerivedClass(5,6)
    };
 
    for (int i = 0; i < 4; i++) {
        array[i] -> print();
    }
 
    for (int i = 0; i < 4; i++) {
        delete array[i];
    } // => Only BaseClass Destructors are executed
 
    return 0;
}

When the DerivedClass has pointer attributes that need to be freed in the destructor, the static binding shown above can lead to a memory leak. Hence, it's important to use dynamic binding for the destructor in such cases.

class BaseClass {
    public:
        int a;
        void print () {
            cout << a << endl;
        };
    BaseClass (int a) : a(a) {};
 
    virtual ~BaseClass () {
        cout << "BaseClass Destructor" << endl;
    }
};
 
int main () {
    BaseClass* array[] = {
        new BaseClass(1),
        new BaseClass(2),
        new DerivedClass(3,4),
        new DerivedClass(5,6)
    };
 
    for (int i = 0; i < 4; i++) {
        array[i] -> print();
    }
 
    for (int i = 0; i < 4; i++) {
        delete array[i];
    } // => Only BaseClass Destructors & DerivedClass Destructors are executed
 
    return 0;
}

Exercises

From this article, there will be an exercise section where you can test your understanding of the material introduced in the article. I highly recommend solving these questions by yourself after reading the main part of the article. You can click on each question to see its answer.

Resources