Skip to content

Latest commit

 

History

History
102 lines (69 loc) · 9.1 KB

Lec10.md

File metadata and controls

102 lines (69 loc) · 9.1 KB

Лекция по ОиМП №10

Виртуальное наследование

Представим, что у нас есть два класса со схожим функционалом. Конечно, каждый из классов мы можем реализовать отдельно друг от друга, но это не оптимально, так как схожие куски кода будут повторяться.

Есть решение:

Общую часть схожих классов вынесем в отдельный "базовый" класс, а затем сделаем схожие классы наследниками базового.

Заметим, что приватные переменные базового класса мы не можем использовать в классах-наследниках, а поэтому для общих переменных нужно использовать модификатор доступа protected. Аналогично с классами - если мы объявим класс-наследник приватным, то снаружи мы не будем знать, что он наследник базового класса, и не сможем с ним работать.

class Parent {
public:
    size_t sz1 = 0;
private:
    size_t sz2 = 0;
protected:
    size_t sz3 = 0;
}

class Inheritanc : public Parent {
    size_t x;
    x = sz1;
    x = sz2; // ошибка
    x = sz3;
}

Как реализовать схожие методы для классов-наследников? Используем механизм, называемый виртуальный полиморфизм.

Полиморфизм - механизм языка, который одинаковым синтаксическим конструкциям придает разную семантику в зависимости от контекста.

Делается это с помощью ключевого слова virtual. Теперь методы классов-наследников будут перегружать методы базового класса, и, аналогично, они будут виртуальными. В зависимости от переданного объекта у нас будет вызван нужный метод. Даже если у базового класса метод будет приватным, мы сможем объявить этот метод у классов-наследников публичным. Константность у методов должна совпадать.

С помощью ключевого слова override можно сделать проверку на совпадение имени/константности у методов (желательно его писать).

С помощью ключевого слова final можно ограничить наследование данной функции/класса у данного в дальнейшем.

virtual method(...) final override {

}

Объект, переданный как базовый класс, теперь будет адекватно восприниматься как любой из переданных. классов-наследников. Передавать так мы сможем только по указателю или ссылке, по значению может лежать только объект соответствующего класса.

Базовый класс может хранить адрес объекта класса наследника (приведение выполняется неявно). Наоборот можно сделать с помощью специального кастования dynamic_cast.

dynamic_cast<Наследник*>(объект базового класса)

Если в объекте базового класса у вас будет храниться не тот класс-наследник, то выбросится исключение.

Также из класса наследника мы можем вызывать функции базового класса (виртуального вызова не произойдет). Виртуальный вызов происходит только у объекта, переданного по ссылке или указателю.

Виртуальные деструкторы

Если мы сделаем указатель на объект базового класса, в котором будет храниться класс-наследник, то, если деструктор не объявлен виртуальным, при удалении объекта вызовется деструктор базового класса, что будет являться неопределенным поведением (объекты класса-наследника не удалятся). Поэтому объявляем деструктор базового клсса виртуальным. Альтернатива - объявить деструктор базового класса protected.

Память

Как это реализуется в памяти:

Объект базового класса будет являться частью объектов классов-наследников (указатель на наследник будет указывать на область памяти, в которой сначала идет объект базового класса, а потом идут остальные объекты наследника).

Если мы объявляем какой-то виртуальный метод, то для класса реализуется таблица виртуальных функций (virtual function table), в которой лежат наши виртуальные методы и функции с адресами. В каждом виртуальном классе есть неявное поле, называемое, например, __vft (указатель на таблицу для базового класса), реализуемый в начале класса. С помощью переходов по адресам в RunTime реализуется вызов функции (что немного медленнее, чем вызов обычной функции). Виртуальные функции нельзя сделать inline (на этапе компиляции компилятор не знает, какой именно нужно вызвать метод).

Ковариантность

Если мы хотим перегрузить метод так, чтобы он возвращал не указатель на базовый класс, а указатель на класс-наследник, то мы можем так сделать, все будет работать. Такой механизм называется ковариантность

Абстрактность

Если мы захотим создать общий виртуальный метод в базовом классе без реализации, то мы можем объявить его так:

virtual method(...) = 0;

Мы не сможем создать объект базового класса, так как теперь базовый класс будет абстрактным. Метод базового класса теперь будет вызывать метод класса-наследника (но не обязательно объявлять в каждом классе-наследнике данный метод).

Множественное наследование

Для класса наследника может существовать не один базовый класс (базовые классы пишутся через запятую). Класс-наследник будет хранить методы всех базовых для него классов.

class A : public B, public C {
}

Ромбовидное наследование

class Unit {};

class AntiGround : public virtual Unit {};

class Flying : public virtual Unit {};

class Bomber : public AntiGround, public Flying {

}

Если мы не сделаем AntiGround и Flying виртуальными наследниками, то тогда в классе Bomber будет храниться две копии базового класса Unit. Тогда он не сможет вызвать метод из Unit, потому что не поймет, через какого класса-родителя нужно вызывать этот метод. Благодаря виртуальному наследованию у нас будет храниться лишь одна копия Unit'a.

Чем отличаются struct и class

В struct все поля по умолчанию public, а в class - private.