Inheritance is one of the fundamental principles of object-oriented programming (OOP). It allows the creation of new classes by inheriting functionality from existing classes. This can be useful for the following purposes:
- Reusing already developed code;
- Extending the functionality of existing classes;
- Creating a logical structure and hierarchy of classes;
Basic inheritance
Let's start by understanding some terminology:
- The class from which the inheritance occurs is called the parent, base, or superclass;
- The class that inherits is called the child, derived, or subclass;
Let's declare the Animal class:
class Animal {
public:
int m_age;
std::string m_name;
void print() {
std::cout << "I am an animal named " << m_name << std::endl;
}
};
You have two properties (age and name) and one method (print name). Suppose you want to create a couple more classes Dog and Cat. Most likely, both dogs and cats have ages and names. You can create each class and provide them with these fields, but this would be redundant code duplication. Instead, you can simply inherit these fields from the Animal class (since both cats and dogs are animals).
In C++, inheritance is done using the : operator. Here's how you can inherit the Animal class:
class Dog : public Animal {
public:
void bark() {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void meow() {
std::cout << "Meow!" << std::endl;
}
};
: operator, you use the keyword public. Inheritance can also be public, private, or protected. Essentially, this specifier changes the behavior of specifiers for fields and methods. This is more specialized information and goes beyond the scope of this course. Just remember that in most cases, using the public specifier for class inheritance is sufficient.In this case, the Dog and Cat classes will inherit all the members of the Animal class, including data and methods.
Let's write our main function and check this. Here is our complete code:
#include <iostream>
class Animal {
public:
int m_age;
std::string m_name;
void print() {
std::cout << "I am an animal named " << m_name << std::endl;
}
};
class Dog : public Animal {
public:
void bark() {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void meow() {
std::cout << "Meow!" << std::endl;
}
};
int main() {
Animal fish;
fish.m_name = "Nemo";
fish.print();
std::cout << std::endl;
Dog dog;
dog.m_name = "Barsik";
dog.print();
dog.bark();
std::cout << std::endl;
Cat cat;
cat.m_name = "Murzik";
cat.print();
cat.meow();
return 0;
}
The output of our program will be:
I am an animal named Nemo
I am an animal named Barsik
Woof!
I am an animal named Murzik
Meow!
The members of the base class Animal are accessible both on their own (when creating an object, for example fish) and to derived classes (Dog and Cat) by default. This means that after creating objects, you can change your attributes like this:
Animal fish;
fish.m_name = "Nemo";
Dog dog;
dog.m_name = "Barsik";
Cat cat;
cat.m_name = "Murzik";
A derived class can override the methods of the base class. For example, the following code declares the print() method in the Dog class, which overrides the print() method of the base class:
class Dog : public Animal {
public:
void bark() {
std::cout << "Woof!" << std::endl;
}
void print() {
std::cout << "I am a dog and my name is: " << m_name << std::endl;
}
};
In this case, if you create an object of the Dog class and call the print() method, the following text will be output: I am a dog and my name is: Barsik
Protected access specifier
Wait a minute. In our Animal class, all attributes and methods are public, but we discussed that it is better to hide attributes to enhance code security. Let's fix this misunderstanding and move the attributes to the private section (and, of course, create a setter - setName()):
class Animal {
private:
int m_age;
std::string m_name;
public:
void setName(std::string name) {
m_name = name;
}
void print() {
std::cout << "I am an animal named " << m_name << std::endl;
}
};
But this code will not compile and will give an error: error: "m_name" is a private member of "Animal".
This happens because the private specifier restricts access from everywhere except the class itself. Fortunately, you have another access specifier: protected. The protected access specifier allows access to the class members by derived classes. Access to a protected member outside the class body is closed. Let's rewrite our example using the new access specifier:
#include <iostream>
class Animal {
private:
int m_age;
protected:
std::string m_name;
public:
void setName(std::string name) {
m_name = name;
}
void print() {
std::cout << "I am an animal named " << m_name << std::endl;
}
};
class Dog : public Animal {
public:
void bark() {
std::cout << "Woof!" << std::endl;
}
void print() {
std::cout << "I am a dog and my name is: " << m_name << std::endl;
}
};
class Cat : public Animal {
public:
void meow() {
std::cout << "Meow!" << std::endl;
}
void print() {
std::cout << "I am a cat and my name is: " << m_name << std::endl;
}
};
int main() {
Animal fish;
fish.setName("Nemo");
fish.print();
std::cout << std::endl;
Dog dog;
dog.setName("Barsik");
dog.print();
dog.bark();
std::cout << std::endl;
Cat cat;
cat.setName("Murzik");
cat.print();
cat.meow();
return 0;
}
As a result, the following code will be output in the console:
I am an animal named Nemo
I am a dog and my name is: Barsik
Woof!
I am a cat and my name is: Murzik
Meow!
Note that m_name is declared as protected, so you can only access it from the methods of the base class Animal (from the setName() method) and from the methods (print()) of the derived classes (Cat and Dog). If you try to directly access it from objects (for example: dog.m_name = "bars";), you will get an error. This is necessary because you want any descendant of the Animal class to have the m_name field, but it is important that no one can change it directly.
Multiple inheritance
Multiple inheritance allows a derived class to inherit from multiple base classes. Let's create two additional classes (LandAnimal and SwimmingAnimal):
class LandAnimal {
public:
void move() {
std::cout << "I am moving on land." << std::endl;
}
};
class SwimmingAnimal {
public:
void move() {
std::cout << "I am swimming." << std::endl;
}
};
And now our creatures can inherit not only from the Animal class, but also from LandAnimal or SwimmingAnimal (depending on how they want to move):
class Dog : public Animal, public SwimmingAnimal {
public:
void bark() {
std::cout << "Woof!" << std::endl;
}
void print() {
std::cout << "I am a dog and my name: " << m_name << std::endl;
}
};
class Cat : public Animal, public LandAnimal {
public:
void meow() {
std::cout << "Meow!" << std::endl;
}
void print() {
std::cout << "I am a cat and my name: " << m_name << std::endl;
}
};
Now our dog and cat objects have access to the move() method. Of course, we sent the dog swimming just for the sake of example. We could have created another class with sharks or sea turtles, but that would have just complicated our example.
Here is the final output of our program:
I am an animal named Nemo
I am a dog and my name: Barsik
I am swimming.
Woof!
I am a cat and my name: Murzik
I am moving on land.
Meow!
Multiple inheritance is a rather complex tool in the object-oriented programming arsenal and can lead to unexpected results, so here are a few tips:
- Use multiple inheritance only when absolutely necessary.
- Avoid multiple inheritance of members with the same names.
- Use method overloading to resolve issues with multiple inheritance of methods.
Advantages/disadvantages of inheritance
Inheritance has the following advantages:
- Code reuse: Inheritance allows for reusing code from existing classes, which can save time and effort.
- Functionality extension: Inheritance allows for extending the functionality of existing classes.
- Creation of class hierarchies: Inheritance allows for the creation of class hierarchies that can model relationships between objects in the real world.
Inheritance has the following disadvantages:
- Complexity: Inheritance can make code more complex and harder to understand.
- Dependency: Inheritance can create a dependency between the derived and base classes.
Conclusion
Using inheritance means that you don't need to redefine functionality from parent classes in child classes. You automatically get the methods and member variables of the superclass through inheritance and then simply add specific methods or member variables that you want.
This saves time and effort and is also very efficient: if you ever update or modify the base class (for example, add new functions or fix a bug), all our derived classes will automatically inherit these changes.