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

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
- Portfolio Courses. 2022. Introduction To Inheritance | C++ Tutorial. YouTube.
- Portfolio Courses. 2022. Multilevel Inheritance | C++ Tutorial. YouTube.
- Portfolio Courses. 2022. How Constructors Work With Inheritance | C++ Tutorial. YouTube.
- Portfolio Courses. 2022. Introduction To Destructors In Inheritance | C++ Tutorial. YouTube.
- Portfolio Courses. 2022. Multiple Inheritance Deep Dive | C++ Tutorial. YouTube.
- Portfolio Courses. 2022. Dynamic Binding (Polymorphism) With The Virtual Keyword | C++ Tutorial. YouTube.
- Portfolio Courses. 2022. Virtual Destructors | C++ Tutorial. YouTube.