- Основы ООП: абстракция
- Основы ООП: инкапусляция
- Основы ООП: наследование
- Основы ООП: полиморфизм
- Основы ООП: композиция (дизайн)
- Определение класса
- Права доступа
- Конструкторы: по умолчанию, параметрический, копирования
- Деструкторы
- Ссылка на себя: this
- Наследование
- Динамическое создание объектов: new и delete
- Указатели
- Виртуальные методы (virtual и override)
- Статические члены
- Вложенные типы, пространства имен и оператор контекста '::'
- Разнесение объявления и определения класса
- Абстрактные классы и интерфейсы
- Перегрузка операторов
- Приведение объектных типов: dynamic_cast и указатель void *
- Указатели, ссылки и const
- Управление наследованием: final и delete
Наследование
При определении класса разработчик может унаследовать его от другого класса, воплощая тем самым концепцию наследования. Для этого после имени класса ставится двоеточие, необязательный модификатор прав доступа (одно из ключевых слов public, protected, private), и имя родительского класса. Например, вот как мы можем описать класс Rectangle, производный от Shape:
class Rectangle : public Shape
|
Модификаторы доступа в заголовке класса управляют "видимостью" членов родительского класса, включенных в класс наследника:
- public — все унаследованные члены сохраняют свои права и ограничения;
- protected — меняет права унаследованных public-членов на protected;
- private — делает все унаследованные члены закрытыми (private).
Модификатор public используется в подавляющем большинстве определений. Два остальных варианта имеет смысл применять только в исключительных случаях, потому что они нарушают базовый принцип наследования: объекты производного класса должны обычно "являться" ("is a") полноценными представителями родительского семейства, а если мы "урезаем" их права, они утрачивают часть своих характеристик. Структуры также можно наследовать друг от друга по аналогичной схеме. Наследовать классы от структур или структуры от классов запрещено.
В отличие от C++, MQL5 не поддерживает множественное наследование. Родителей у класса может быть не больше одного.
Объект производного класса имеет встроенный в себя объект базового класса. Учитывая, что базовый класс может в свою очередь быть унаследованным от какого-то другого класса-прародителя, создаваемый объект можно сравнить с матрешками, вложенными одна в другую.
В новом классе нам потребуется конструктор, который заполняет поля объекта аналогично тому, как это было сделано в базовом классе.
class Rectangle : public Shape
|
В данном случае список инициализации превратился в одиночный вызов конструктора Shape. В списке инициализации нельзя напрямую устанавливать переменные базового класса, потому что за их инициализацию отвечает базовый конструктор. Однако, при необходимости, мы могли бы изменить protected-поля базового класса из тела конструктора Rectangle (инструкции в теле функции выполняются уже после того, как отработал базовый конструктор в списке инициализации).
У прямоугольника есть два размера, поэтому добавим их в качестве защищенных полей dx и dy. Для установки их значений требуется дополнить список параметров конструктора.
class Rectangle : public Shape
|
Важно отметить, что в объектах Rectangle в неявном виде присутствует унаследованная от Shape функция toString (впрочем, как и draw, но та пока пуста). Поэтому корректен следующий код:
void OnStart()
|
Здесь продемонстрирован не только вызов toString, но и создание объекта-прямоугольника с помощью нашего нового конструктора.
Конструктор по умолчанию (без параметров) отсутствует в классе Rectangle. Это означает, что пользователь класса не может создавать объекты-прямоугольники простым способом, без аргументов:
Rectangle r; // 'Rectangle' - wrong parameters count |
Компилятор выдаст ошибку "Неверное количество аргументов".
Создадим еще один дочерний класс — Ellipse. Пока он ничем не будет отличаться от Rectangle, кроме имени. Позднее мы внесем в них различия.
class Ellipse : public Shape
|
Поскольку количество классов увеличивается, было бы здорово выводить имя класса в методе toString. В разделе Специальные операторы sizeof и typename мы описывали оператор typename. Попробуем его использовать.
Напомним, что typename ожидает один параметр, для которого и возвращается название типа. Например, если мы создадим пару объектов s и r, соответственно классов Shape и Rectangle, то можем узнать их тип следующим образом:
void OnStart()
|
Но нам нужно каким-то образом получить это имя внутри класса. Для этой цели добавим в параметрический конструктор Shape строковый параметр и будем сохранять его в новом строковом поле type (обратите внимание на секцию protected и модификатор const: это поле скрыто от внешнего мира и не может редактироваться после создания объекта):
class Shape
|
В конструкторах производных классов станем заполнять этот параметр базового конструктора с помощью typename(this):
class Rectangle : public Shape
|
Теперь мы можем усовершенствовать метод toString с использованием поля type.
class Shape
|
Убедимся, что наша маленькая иерархия классов порождает объекты, как задумано, и выводит тестовые записи в журнал при вызове конструкторов и деструкторов.
void OnStart()
|
В результате получим примерно следующие записи в журнале (пустые строки добавлены намеренно, чтобы разделить вывод от разных объектов):
Pair::Pair(int,int) 0 0
|
По логу понятно, в какой последовательности вызываются конструкторы и деструкторы.
Для каждого объекта сначала создаются описанные в нем поля-объекты (если они есть), затем вызывается базовый конструктор и все конструкторы производных классов по цепочке наследования. Если в производном классе встречаются собственные (добавленные) поля неких объектных типов, конструкторы для них будут вызваны непосредственно перед конструктором этого производного класса. Когда объектных полей несколько, они создаются в той последовательности, в которой описаны в классе.
Деструкторы вызываются строго в обратном порядке.
В производных классах могут быть определены конструкторы копирования, которые мы изучили в разделе Конструкторы: по умолчанию, параметрический, копирования. Для конкретных типов фигур, например, прямоугольника, их синтаксис аналогичен:
class Rectangle : public Shape
|
А область применения слегка расширяется. Объект производного класса можно использовать для копирования в базовый класс (потому что производный содержит все данные для базового). Правда при этом, разумеется, игнорируются поля, добавленные в производном классе.
void OnStart()
|
Для копирования в обратном направлении нужно предоставить в базовом классе вариант конструктора со ссылкой на производный (что, в принципе, противоречит принципам ООП), иначе возникнет ошибка компиляции "нет подходящей перегрузки функции" ("no one of the overloads can be applied to the function call").
Сейчас мы можем описать в скрипте пару или большее количество переменных-фигур, чтобы затем "попросить" их нарисовать себя с помощью метода draw.
void OnStart()
|
Однако такая запись означает, что количество фигур, их типы и параметры жестко "зашиты" в программу, а в принципе пользователь должен выбирать, что и где рисовать. Отсюда следует необходимость создавать фигуры неким динамическим способом.