Lecture
Inheritance and polymorphism are two key concepts of object-oriented programming (OOP).
Inheritance allows you to create new classes that inherit properties and methods from existing classes (parent classes), thereby extending their functionality. A child class can use the properties and methods of the parent class, as well as add its own properties and methods.
Polymorphism allows the same method to be used with different classes derived from the same base class, without worrying about the type of the object. This means that different classes can have methods with the same name but different implementations.
An example of inheritance and polymorphism in OOP:
We have a base class "Shape", which contains methods for calculating area and perimeter. The classes "Circle", "Square" and "Rectangle" inherit from it. Each of these classes implements its own method for calculating area and perimeter. For example, the area-calculation method for a circle takes a radius, for a square - the side length, and for a rectangle - the length and width.
When we call the area-calculation method for each of these shapes, we can use the same method without worrying about what type of shape we are passing as the parameter. This is called polymorphism.

Inheritance - is one of the four main mechanisms of object-oriented programming (alongside encapsulation, polymorphism and abstraction) that makes it possible to describe a new class based on an already existing (parent) class, whose properties and functionality are then inherited by, and can be overridden in, the new class.
A descendant class implements the specification of an already existing class (the base class). This makes it possible to work with objects of the descendant class in exactly the same way as with objects of the base class.
Simple inheritance:
A class from which inheritance is performed is called the base or parent class (in English: base class). Classes that are derived from the base class are called descendants, heirs, or derived classes (in English: derived class).
Some languages use abstract classes. An abstract class is a class that contains at least one abstract method; it is described in the program, has fields and methods, and cannot be used to directly create an object. From an abstract class one can only inherit. Objects are created only on the basis of derived classes that inherit from the abstract one. For example, an abstract class could be the base class «university employee», from which the classes «graduate student», «professor», etc. descend. Since the derived classes have common fields and functions (for example, the field «year of birth»), these class members can be described in the base class. In the program, objects are created based on the classes «graduate student», «professor», but no objects can be created based on the class «university employee».
Multiple Inheritance
With multiple inheritance, a class can have more than one ancestor. In this case, the class inherits the methods of all its ancestors. The advantage of this approach is greater flexibility. Multiple inheritance is implemented in C++. Some other languages also support it, such as Python and Eiffel . Multiple inheritance is supported in UML.
Multiple inheritance is a potential source of errors that can arise from having identically named methods among the ancestors. In languages that position themselves as successors to C++ (Java, C#, etc.), it was decided to abandon multiple inheritance in favor of using interfaces. In practice, it is almost always possible to do without this mechanism. However, if it is needed, conflicts can still be resolved by, for example, using the scope-resolution operator «::» to call a specific method of a specific parent.
An attempt to solve the problem of identically named methods existing among predecessors was made in the Eiffel language, in which the description of a new class must explicitly specify the imported members of each of the inherited classes and their names in the child class.
Most modern object-oriented programming languages (C#, Java, Delphi, etc.) support the ability to simultaneously inherit from an ancestor class and implement the methods of multiple interfaces from one or more classes. In many respects, this mechanism replaces multiple inheritance - interface methods must be explicitly overridden, which rules out errors when inheriting functionally identical methods from different ancestor classes.
Private inheritance - is when the access specifier is set to private, so that all public and protected members of the base class become private members of the derived class. Private inheritance means implementation by means of - that is, some functionality needs to be taken from the base class, and the base class and its descendant have no conceptual relationship. Private inheritance does not have the character of a subtype relationship. Private (as well as protected) inheritance does not create a type hierarchy.

There are several scenarios in which it is worth using private inheritance instead of aggregation:
If all of this is needed, then private inheritance will be more preferable than aggregation.
Aggregation or composition – is when an object of another class becomes a member of one class:
Example of using composition:
Let class D have a member of class B.
In turn, class B has a member of class C.

Thus, by using composition we build a hierarchy of objects. From a design standpoint, private inheritance is equivalent to composition, except for the issue of overriding functions
In general, there is a fundamental difference between composition and (non-private) inheritance:
Composition — is a has-a relationship (has). Using composition, we build a hierarchy of objects.
Inheritance — is an is-a relationship (is). Using inheritance (non-private), we build a hierarchy of classes.
Example: a car has a steering wheel, wheels, etc.
And a car is a vehicle.
There are several scenarios in which it is worth using private inheritance instead of composition:
It is necessary to override abstract and/or virtual methods.
It is necessary to use an instance of the descendant polymorphically within a limited scope (for example, in a friend function). But in the general sense, there is no IS-A relationship between the descendant and the base class.
With private inheritance, the descendant can access the protected members of the base class.
Private inheritance makes it possible to implement "narrowing" inheritance, where the descendant exposes only a few methods of the base class (by raising their visibility with a using-declaration: using Base::ProtectedMethod).
If all of this is needed, then private inheritance will be more preferable than composition.

Polymorphism - the ability of objects with the same specification to have different implementations.
A programming language supports polymorphism if classes with the same specification can have different implementations - for example, a class's implementation can be changed at run time.
The essence of polymorphism can be expressed in the phrase: «One interface, many implementations».
Polymorphism is one of the four important mechanisms of object-oriented software (along with abstraction, encapsulation and inheritance).
Polymorphism makes it possible to write more abstract programs and increase the code reuse ratio. The common properties of objects are combined into a system that can be called by different names - interface, class.
The definition has both an external and an internal expression:
external commonality manifests as an identical set of methods with the same names and signatures (i.e., the names and the number and types of the arguments);
internal commonality - identical functionality of the methods. This can be described intuitively, or expressed as strict laws, rules that the methods must necessarily satisfy. When one method (function, operation) is defined multiple times with different behavior, this is called method overloading (function overloading, operator overloading).
Using parametric polymorphism, universal base types can be created. In the case of parametric polymorphism, a function is implemented once and behaves the same for all types, just as it would for a single specific type. Parametric polymorphism deals with parametric methods and types.
Parametric types.
Instead of writing a class for each specific type, you should create types that will be resolved at program run time, that is, we create a parametric type.
Abstract methods often belong to the category of deferred methods. The class in which this method is defined can call the method, and polymorphism ensures that the appropriate version of the deferred method is called in the child classes. Ad hoc polymorphism allows a special implementation for the data of each type.
Overload polymorphism is a special case of polymorphism.
class Shape:
def area(self):
pass
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
def perimeter(self):
return 2 * 3.14 * self.radius
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
def perimeter(self):
return 4 * self.side
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
def perimeter(self):
return 2 * (self.length + self.width)
shapes = [Circle(5), Square(10), Rectangle(3, 4)]
for shape in shapes:
print("Area: ", shape.area())
print("Perimeter: ", shape.perimeter())
Thus
Both of these principles are important OOP concepts and make it possible to create more flexible and scalable software systems, making code reuse easier and reducing the amount of code written.
An inner, or nested class (English: inner class) — in object-oriented programming, a class that is defined entirely within another class.
Nested classes are supported in the Java programming language starting from version 1.1, in C# and other languages on the .NET platform, as well as in the D programming language and in C++.
An instance of an ordinary (outer) class can exist as an independent object. Its existence does not require the mandatory presence of definitions of other classes or their instances. In the case of an inner class, its instance cannot exist without being bound to the top-level class that encloses it, or to an instance of that class.
In Java there are 4 types of inner classes:
An instance of an inner class can exist only when a specific instance of the outer class exists. This logical relationship determines the syntax for creating objects: first an object of the outer class is created, and later, based on it, an object of the inner class is created.
Inner non-static classes are described inside the main outer class. Instances of such classes have access to the public, protected, default and private fields of the outer class, as well as to the static and non-static methods of the outer instance with any access modifiers. Because instances of an inner class are always logically bound to instances of the enclosing class, they cannot contain (although they can inherit from an ancestor) definitions of static fields, methods and classes (except constants).
Example of declaring an inner class:

Creating the described class can be expressed with the following block of code: OuterClass.InnerClass inner = new OuterClass().new InnerClass();
They are declared inside the main class and are marked with the static keyword. Objects of such classes do not have access to the members of the outer class, except for the static ones. This is because a specific object of the outer class is not used to create such a class, and at the moment the inner class's code is executed, an object of the outer class may not exist at all. Instances of static nested classes can contain static fields, methods and classes, unlike other types of inner classes.
Example of declaring a nested static class:

Creating the described static nested class can be expressed with the following block of code: OuterClass.StaticInnerClass staticInner = new OuterClass.StaticInnerClass();
They are declared inside the methods of the main class. They can be used only within those methods. They have access to the members of the outer class. They have access to both local variables and method parameters under one condition - the variables and parameters used by the local class must be declared final. They cannot contain the definition of (but can inherit) static fields, methods and classes (except constants).
Example:
The described local class can only be created inside the method itself, strictly below the code that declares the class. Example creation code: InnerLocalClass innerLocal = new InnerLocalClass();
They are declared inside the methods of the main class. They can be used only within those methods. Unlike local classes, anonymous classes do not have a name. The main requirement for an anonymous class - it must inherit an existing class or implement an existing interface. They cannot contain the definition of (but can inherit) static fields, methods and classes (except constants). Example:
PHP 7 has a mechanism for describing anonymous classes; however, unlike Java, anonymous classes are not required to inherit an existing class or implement an existing interface, which is made possible by the dynamic nature of the language. Example:
Thus we have examined the merits of keeping data hidden, but sometimes you may encounter situations where you find that you have classes and functions outside those classes that need to work very closely with each other. For example, you might have a class that stores data, and a function (or another class) that displays this data on the screen. Although the storage class and the display code were separated to simplify maintenance, the display code is actually closely tied to the details of the storage class. Consequently, hiding information about the storage classes from the display code provides little real benefit.
In such cases there are two options:
A friend function – is a function that can access the private members of a class as if it were a member of that class. In all other respects, a friend function is like an ordinary function. A friend function can be either an ordinary function or a member function of another class. To declare a friend function, simply use the keyword friend before the prototype of the function you want to make a friend of the class. It does not matter whether you declare the friend function in the private or the public section of the class.
Here is an example of using a friend function:

In this example, we made a function called reset(), which takes an object of class Accumulator and sets the value of m_value to 0. Since reset() is not a member of class Accumulator, normally reset() would not have access to the private members of Accumulator. However, since Accumulator specifically declared this function reset() as a friend of the class, it is granted access to the private members of Accumulator.
Note that we must pass into reset() an Accumulator object. This is because reset() is not a member function. It has no *this pointer and no Accumulator object to work with unless one is specified.
Here is another example:

In this example, we declare the function isEqual() a friend of class Value. isEqual() takes two Value objects as parameters. Since isEqual() is a friend of class Value, it can access the private members of any Value object. In this case, it uses this access to compare the two objects and returns true if they are equal.
Although both of the examples above are rather contrived, the latter example is very similar to cases we will encounter later when we discuss operator overloading!
A function can be a friend of more than one class at the same time. For example, consider the following example:

Two things are worth noting in this example. First, since printWeather is a friend of both classes, it can access the private data of objects of both classes. Second, note the following line at the top of the example:
class Humidity;
This is a class prototype (forward declaration), which tells the compiler that we are going to define a class named Humidity later. Without this line, the compiler, when parsing the prototype for printWeather() inside class Temperature, would tell us that it does not know what Humidity is. Class prototypes serve the same role as function prototypes – they tell the compiler what something looks like so that it can be used now and defined later. However, unlike functions, classes have no return types or parameters, so class prototypes are always simply class ClassName, where ClassName – is the name of the class.
An entire class can also be made a friend of another class. This gives all the members of the friend class access to the private members of the other class. Here is an example:

Since class Display is a friend of Storage, any of the members of Display that use a Storage class object can directly access the private members of Storage. This program produces the following result:
6.7 5
A few additional notes about friend classes.
First, even though Display is a friend of Storage, Display does not have direct access to the *this pointer of Storage objects.
Second, the fact that Display is a friend of Storage does not mean that Storage is also a friend of Display. If you want two classes to be friends with each other, they must both declare each other as friends. Finally, if class A is a friend of B, and B – is a friend of C, this does not mean that A is a friend of C.
Be careful when using friend functions and classes, because this allows the friend function or class to break encapsulation. If the implementation details of a class change, the implementation details of the friend will also need to be changed. Therefore, limit the use of friend functions and classes to a minimum.
Instead of making the entire class a friend, you can make just a single member function a friend. This is done the same way as making an ordinary function a friend, except that you use the member function's name with the ClassName:: prefix included (for example, Display::displayItem).
However, this can actually be a bit trickier than expected. Let's convert the previous example to make Display::displayItem a friend member function. You might try something like this:
However, it turns out that this will not work. To make a member function a friend, the compiler must see the full definition of the class the member function belongs to (not just a forward declaration). Since class Storage has not yet seen the full definition of class Display, the compiler will produce an error at the point where we try to make the member function a friend.
Fortunately, this is easy to fix, simply by putting the definition of class Display before the definition of class Storage.
However, now we have a different problem. Since the member function Display::displayItem() uses Storage as a reference parameter, and we just moved the definition of Storage below the definition of Display, the compiler will complain that it does not know what Storage is. We cannot fix this by changing the order of the definitions, because then we would undo the previous fix.
Fortunately, this too can be fixed by performing a couple of simple steps.
First, we can add class Storage as a forward declaration.
Second, we can move the definition of Display::displayItem() out of the class to a place after the full definition of class Storage.
Here is what this looks like:



Now everything will compile correctly: the forward declaration of class Storage is enough to satisfy the declaration of Display::displayItem(), the full definition of Display satisfies the declaration of Display::displayItem() as a friend of Storage, and the full definition of class Storage is enough to satisfy the definition of the member function Display::displayItem(). If this is a bit confusing, see the comments in the program.
If this seems complicated – it is. Fortunately, this trick is necessary only because we are trying to do everything in one file. A better solution – is to put the definition of each class in a separate header file, and the definitions of the member functions in the corresponding .cpp files. That way, all the class definitions would be immediately visible in the .cpp files, and there would be no need to reorder classes and functions!
A friend function or class – is a function or class that can access the private members of another class as if it were a member of that class. This allows the friend function or class to work closely with the other class without forcing the other class to expose its private members (for example, through access functions).
Declaring friends is usually used when defining overloaded operators (which we will talk about in the next chapter), or, less commonly, when two or more classes need to interact closely with each other.
Note that in order to make a specific member function a friend, you must first see the full definition of the class that the member function belongs to.
чтобы еще лучше понять смысл классов интерфейсов и вообще ООП, рекомендуем прочитать UML диаграммы классов
https://intellect.icu/diagramma-klassov-class-diagram-4825
Отношения классов в UML
https://intellect.icu/otnosheniya-klassov-v-uml-4301
Comments