In this article, inheritance is described at three levels: beginner, intermediate and advanced. Expert no. And not a word about SOLID. Fair.
Inheritance is one of the fundamental principles of OOP. According to it, a class can use variables and methods of another class as its own.
A class that inherits data is called a subclass (subclass), derived class, or child class (child). A class from which data or methods are inherited is called a super class, a base class, or a parent. The terms “parent” and “child” are extremely useful for understanding inheritance. As the child receives the characteristics of its parents, the derived class receives the methods and variables of the base class.
Inheritance is useful because it allows you to structure and reuse code, which, in turn, can significantly speed up the development process. Despite this, inheritance should be used with caution, since most changes in the superclass will affect all subclasses, which can lead to unintended consequences.
In this example, the turn_on()
method and the serial_number
variable serial_number
not declared or defined in the Computer
subclass. However, they can be used because they are inherited from the base class.
Important note : private variables and methods cannot be inherited.
#include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } private: int pincode = 87654321; }; class Computer: public Device {}; int main() { Computer Computer_instance; Computer_instance.turn_on(); cout << "Serial number is: " << Computer_instance.serial_number << endl; // cout << "Pin code is: " << Computer_instance.pincode << endl; // will cause compile time error return 0; }
There are several types of inheritance in C ++:
public
) - public ( public
) and protected ( protected
) data is inherited without changing the level of access to them;protected
) - all inherited data becomes protected;private
) - all inherited data becomes private.For the base class Device
, the data access level does not change, but since the derived Computer
class inherits the data as private, the data becomes private for the Computer
class.
#include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } }; class Computer: private Device { public: void say_hello() { turn_on(); cout << "Welcome to Windows 95!" << endl; } }; int main() { Device Device_instance; Computer Computer_instance; cout << "\t Device" << endl; cout << "Serial number is: "<< Device_instance.serial_number << endl; Device_instance.turn_on(); // cout << "Serial number is: " << Computer_instance.serial_number << endl; // Computer_instance.turn_on(); // will cause compile time error cout << "\t Computer" << endl; Computer_instance.say_hello(); return 0; }
The Computer
class now uses the turn_on()
method like any private method: turn_on()
can be called from within the class, but an attempt to call it directly from main
will result in an error during compilation. For the base class Device
, the turn_on()
method turn_on()
remained public, and can be called from main
.
In C ++, constructors and destructors are not inherited. However, they are called when the child class initializes its object. Constructors are called one after the other hierarchically, starting with the base class and ending with the last derived class. Destructors are called in reverse order.
Important note: this article does not cover virtual desktops. Additional material on this topic can be found for example in this article on Habré .
#include <iostream> using namespace std; class Device { public: // constructor Device() { cout << "Device constructor called" << endl; } // destructor ~Device() { cout << "Device destructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } ~Computer() { cout << "Computer destructor called" << endl; } }; class Laptop: public Computer { public: Laptop() { cout << "Laptop constructor called" << endl; } ~Laptop() { cout << "Laptop destructor called" << endl; } }; int main() { cout << "\tConstructors" << endl; Laptop Laptop_instance; cout << "\tDestructors" << endl; return 0; }
Constructors: Device
-> Computer
-> Laptop
.
Destroyers: Laptop
-> Computer
-> Device
.
Multiple inheritance occurs when a subclass has two or more superclasses. In this example, the Laptop
class inherits both Monitor
and Computer
at the same time.
#include <iostream> using namespace std; class Computer { public: void turn_on() { cout << "Welcome to Windows 95" << endl; } }; class Monitor { public: void show_image() { cout << "Imagine image here" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.show_image(); return 0; }
Multiple inheritance requires careful design, as it can lead to unintended consequences. Most of these effects are caused by ambiguity in inheritance. In this example, the Laptop
inherits the turn_on()
method from both parents and it is unclear which method should be called.
#include <iostream> using namespace std; class Computer { private: void turn_on() { cout << "Computer is on." << endl; } }; class Monitor { public: void turn_on() { cout << "Monitor is on." << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will cause compile time error return 0; }
In spite of the fact that private data is not inherited, it is impossible to allow ambiguous inheritance by changing the level of access to data to private data. When compiling, first a search for a method or a variable occurs, and afterwards - checking the level of access to them.
Diamond problem is a classic problem in languages ​​that support the possibility of multiple inheritance. This problem occurs when classes B
and C
inherit A
, and class D
inherits B
and C
For example, classes A
, B
and C
define the print_letter()
method. If print_letter()
will be called by class D
, it is unclear which method should be called — a method of class A
, B
or C
Different languages ​​have different approaches to solving diamond-shaped problems. In C ++, the solution to the problem is left to the discretion of the programmer.
A diamond-shaped problem is primarily a design problem, and it should be provided at the design stage. At the design stage, it can be resolved as follows:
turn_on()
in the Laptop
subclass). #include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } }; class Computer: public Device {}; class Monitor: public Device {}; class Laptop: public Computer, public Monitor { /* public: void turn_on() { cout << "Laptop is on." << endl; } // uncommenting this function will resolve diamond problem */ }; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will produce compile time error // if Laptop.turn_on function is commented out // calling method of specific superclass Laptop_instance.Monitor::turn_on(); // treating Laptop instance as Monitor instance via static cast static_cast<Monitor&>( Laptop_instance ).turn_on(); return 0; }
If the turn_on()
method was not redefined in the Laptop, calling Laptop_instance.turn_on()
will result in a compile error. A Laptop
object can access two turn_on()
method turn_on()
at the same time: Device:Computer:Laptop.turn_on()
and Device:Monitor:Laptop.turn_on()
.
Since in C ++, when initializing an object of a child class, the constructors of all parent classes are called, another problem arises: the constructor of the base class Device
will be called twice.
#include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; return 0; }
Virtual inheritance prevents the appearance of multiple base class objects in the inheritance hierarchy. Thus, the constructor of the base class Device
will be called only once, and calling the turn_on()
method without redefining it in the child class will not cause a compilation error.
#include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } void turn_on() { cout << "Device is on." << endl; } }; class Computer: virtual public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: virtual public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); return 0; }
Note : Virtual inheritance in the Computer
and Monitor
classes will not allow diamond-shaped inheritance if the child class of the Laptop
class Device
from the Device
class is not virtual ( class Laptop: public Computer, public Monitor, public Device {};
).
In C ++, a class in which there exists at least one pure virtual method (pure virtual) is considered to be abstract. If the virtual method is not overridden in the child class, the code will not compile. Also, it is impossible to create an abstract class object in C ++ - an attempt will also cause an error during compilation.
#include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } virtual void say_hello() = 0; }; class Laptop: public Device { public: void say_hello() { cout << "Hello world!" << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.say_hello(); // Device Device_instance; // will cause compile time error return 0; }
C ++, unlike some OOP languages, does not provide a separate keyword for the interface. However, the implementation of the interface is possible by creating a pure abstract class (pure abstract class) - a class in which only method declarations are present. Such classes are also often called abstract base classes (Abstract Base Class - ABC).
#include <iostream> using namespace std; class Device { public: virtual void turn_on() = 0; }; class Laptop: public Device { public: void turn_on() { cout << "Device is on." << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); // Device Device_instance; // will cause compile time error return 0; }
Although inheritance is a fundamental principle of OOP, it should be used with caution. It is important to think that any code that will be used is likely to be changed and can be used in a way that is not obvious to the developer.
If inheritance does not occur from the interface (a pure abstract class in the context of C ++), but from a class in which there are any implementations, it is worth considering that the class of the heir is associated with the parent class of the closest possible connection. Most changes in the parent's class can affect the heir, which can lead to unexpected behavior. Such changes in the behavior of the heir are not always obvious - an error may occur in the already tested and working code. This situation is exacerbated by the presence of a complex class hierarchy. It is always worth remembering that the code can be changed not only by the person who wrote it, and the paths of inheritance that are obvious to the author may not be taken into account by his colleagues.
In contrast, it is worth noting that inheritance from partially implemented classes has an undeniable advantage. Libraries and frameworks often work like this: they provide the user with an abstract class with several virtual and many implemented methods. Thus, the greatest amount of work has already been done - complex logic has already been written, and the user needs only to customize the ready-made solution to fit his needs.
Inheritance from the interface (pure abstract class) presents inheritance as the possibility of structuring code and protecting the user. Since the interface describes what work will be done by the implementation class, but does not describe how, any user of the interface is protected from changes in the class that implements this interface.
First of all, it is worth noting that the example is closely related to the concept of polymorphism, but will be considered in the context of inheritance from a pure abstract class.
An application running abstract business logic must be configured from a separate configuration file. At an early stage of development, the formatting of this configuration file was not fully formed. Taking out file parsing for an interface provides several advantages.
The lack of unambiguity regarding the formatting of the configuration file does not slow down the process of developing the main program. Two developers can work in parallel - one on business logic, and the other on the parser. Since they interact through this interface, each of them can work independently. This approach makes it easier to cover the code with unit tests, since the necessary tests can be written using the mock for this interface.
Also, when changing the format of the configuration file, the business logic of the application is not affected. The only thing that requires a complete transition from one formatting to another is to write a new implementation of an already existing abstract class (parser class). In the future, returning to the original file format requires minimal work - replacing one already existing parser with another.
Inheritance offers many benefits, but must be carefully designed to avoid problems for which it opens up. In the context of inheritance, C ++ provides a wide range of tools that opens up a lot of opportunities for the programmer.
And SOLID is good.
Source: https://habr.com/ru/post/445948/
All Articles