- Основы ООП: абстракция
- Основы ООП: инкапусляция
- Основы ООП: наследование
- Основы ООП: полиморфизм
- Основы ООП: композиция (дизайн)
- Определение класса
- Права доступа
- Конструкторы: по умолчанию, параметрический, копирования
- Деструкторы
- Ссылка на себя: this
- Наследование
- Динамическое создание объектов: new и delete
- Указатели
- Виртуальные методы (virtual и override)
- Статические члены
- Вложенные типы, пространства имен и оператор контекста '::'
- Разнесение объявления и определения класса
- Абстрактные классы и интерфейсы
- Перегрузка операторов
- Приведение объектных типов: dynamic_cast и указатель void *
- Указатели, ссылки и const
- Управление наследованием: final и delete
Конструкторы: по умолчанию, параметрический, копирования
Мы уже сталкивались с конструкторами в главе, посвященной структурам (см. раздел Конструкторы и деструкторы). Для классов они работают, во многом, аналогичным образом. Напомним основные моменты и дополним их.
Конструктор — это метод с именем, совпадающим с именем класса, и имеющий тип void, то есть он не возвращает значения. Обычно ключевое слово void перед именем конструктора опускают. В классе может быть несколько конструкторов: они должны различаться количеством или типом параметров. В момент создания нового объекта, программа вызывает конструктор, чтобы в нем можно было установить начальные значения полям.
Один из способов создания объекта, который мы использовали — описание в коде переменной соответствующего класса. В этой строке и будет вызван конструктор. Это происходит автоматически.
В зависимости от наличия и типов параметров, конструкторы делятся на:
- конструктор по умолчанию: без параметров;
- конструктор копирования: с единственным параметром типа ссылки на объект того же класса;
- конструктор параметрический: с произвольным набором параметров, кроме одиночной ссылки для копирования из предыдущего пункта.
Конструктор по умолчанию
Самый простой конструктор — без параметров — называется конструктором по умолчанию. В отличие от C++, в MQL5 не считается конструктором по умолчанию такой конструктор, у которого есть параметры и все они имеют значения по умолчанию (то есть все параметры опциональные, см. раздел Необязательные параметры).
Определим конструктор по умолчанию для класса Shape.
class Shape
|
Разумеется, его следует делать в публичной секции класса.
Иногда конструкторы намеренно делают защищенными или закрытыми, чтобы держать под контролем процесс создания объектов, например, через так называемые фабричные методы. Но в данном случае, мы рассматриваем стандартный вариант композиции класса.
Для установки начальных значений переменным объекта мы могли бы воспользоваться привычными инструкциями присвоения:
public:
|
Однако в синтаксисе конструкторов предусмотрен другой вариант. Он называется списком инициализации и пишется после заголовка функции, через двоеточие. Сам список представляет собой последовательность имен полей, разделенных запятыми, причем справа от каждого имени в круглых скобках указывается нужное начальное значение.
Например, для конструктора Shape можно написать так:
public:
|
Данный синтаксис является более предпочтительным, чем присваивание переменных в теле конструктора по нескольким причинам.
Во-первых, присваивание в теле функции производится уже после того, как соответствующая переменная создана. В зависимости от типа переменной это может означать, что для неё был сначала вызван конструктор по умолчанию и потом перезаписано новое значение (а это — лишние накладные расходы). В случае списка инициализации переменная сразу создается с нужным значением. Вероятно, компилятор сможем оптимизировать присваивание и в отсутствии списка инициализации, но в общем случае это не гарантировано.
Во-вторых, некоторые поля класса могут быть объявлены с модификатором const. Тогда их можно установить только в списке инициализации.
В-третьих, переменные-поля пользовательских типов могут не иметь конструктора по умолчанию (то есть в их классе все доступные конструкторы имеют параметры). Это значит, что при создании переменной в неё нужно передать фактические параметры, и список инициализации позволяет это сделать: значения аргументов указываются внутри круглых скобок, как будто при явном вызове конструктора. Список инициализации можно использовать в определении конструкторов, но не других методов.
Параметрический конструктор
У параметрического конструктора, по определению, есть несколько параметров (один или больше).
Например, представим, что для координат x и y описана специальная структура с параметрическим конструктором:
struct Pair
|
Тогда мы можем использовать поле coordinates нового типа Pair вместо двух целочисленных полей x и y в классе Shape. Такая конструкция объектов называется включением или композиционным агрегированием. Объект Pair является неотъемлемой частью объекта Shape. Пара координат автоматически создается и уничтожается вместе с объектом "хозяином".
Поскольку у Pair нет конструктора без параметров, поле coordinates должно быть указано в списке инициализации конструктора Shape, с двумя параметрами (int, int):
class Shape
|
Без списка инициализации не удастся создать подобные автоматические объекты.
С учетом изменения способа хранения координат в объекте нам необходимо обновить метод toString:
string toString() const
|
Но это не окончательная его версия: вскоре мы внесем еще некоторые правки.
Напомним, что автоматические переменные описывались в разделе Инструкции объявления/определения. Они называются автоматическими, потому что компилятор создает их (выделяет память) автоматически, и также автоматически удаляет, когда выполнение программы покидает контекст (блок кода), в котором переменная была создана.
В случае объектных переменных автоматическое создание означает не только выделение памяти, но и вызов конструктора. А автоматическое удаление объекта сопровождается вызовом его деструктора (см. далее раздел Деструкторы). Причем, если объект входит в состав другого объекта, то его время жизни совпадает с временем жизни своего "хозяина", как в случае поля coordinates — экземпляра Pair в составе объекта Shape.
Статические (в том числе глобальные) объекты также управляются компилятором автоматически.
Альтернативой автоматическому распределению является динамическое создание объектов и работа с ними через указатели.
В разделе про наследование мы узнаем, как можно унаследовать один класс от другого. В этом случае список инициализации — единственный способ вызвать параметрический конструктор базового класса (компилятор не способен автоматически сформировать вызов конструктора с параметрами, как он делает неявно для конструктора по умолчанию).
Добавим в класс Shape еще один конструктор, позволяющий задать конкретные значения переменным. Он как раз будет является параметрическим конструктором (их можно создать сколько угодно: для разных целей и с разным набором параметров).
Shape(int px, int py, color back) :
|
Список инициализации гарантирует, что когда выполняется тело конструктора, все внутренние поля (включая вложенные объекты, если они есть) уже созданы и проинициализированы.
Порядок инициализации членов класса соответствует не списку инициализации, а последовательности их объявления в классе.
Если в классе описан конструктор с параметрами, и при этом требуется разрешить создание объектов без аргументов, программист должен реализовать конструктор по умолчанию явным образом.
В том случае, если в классе вообще нет конструкторов, компилятор неявным образом предоставляет конструктор по умолчанию в виде заглушки, которая ответственна за инициализацию полей следующих типов: строки, динамические массивы, автоматические объекты с конструктором по умолчанию. Если таких полей нет, неявный конструктор по умолчанию ничего не делает. Поля других типов неявный конструктор не "трогает", поэтому в них будет содержаться случайный "мусор". Чтобы этого избежать, программист должен описать конструктор в явном виде и установить начальные значения самостоятельно.
Конструктор копирования
Конструктор копирования позволяет создать объект на основе другого объекта, переданного по ссылке в качестве единственного параметра.
Например, для класса Shape конструктор копирования мог бы выглядеть так:
class Shape
|
Обратите внимание, что защищенные и закрытые члены другого объекта доступны в текущем объекте, поскольку права доступа работают на уровне класса. Иными словами, два объекта одного класса могут обращаться к данным друг друга при наличии ссылки (или указателя).
При наличии такого конструктора можно создавать объекты с помощью одного из двух синтаксисов:
void OnStart()
|
Следует различать инициализацию объекта при создании и присваивание.
Второй вариант (помечен комментарием "синтаксис 2") будет работать даже если конструктора копирования не существует, но есть конструктор по умолчанию. В этом случае компилятор сформирует менее эффективный код: сначала создаст с помощью конструктора по умолчанию пустой экземпляр приемной переменной (s3, в данном случае), а потом скопирует поля образца (s, в данном случае) поэлементно. Фактически, получится тот же случай, что с переменной s4, для которой определение и присваивание выполнены отдельными инструкциями.
Если конструктора копирования нет, то попытка использовать первый синтаксис приведет к ошибке "недопустимое преобразование параметра" ("parameter conversion not allowed"), так как компилятор попытается взять какой-либо другой конструктор из имеющихся, с отличным набором параметров.
Имейте в виду, что если в классе есть поля с модификатором const, присваивание таких объектов запрещено по понятной причине: константное поле нельзя менять, его можно только однократно установить при создании объекта. Поэтому конструктор копирования становится единственным способом продублировать объект.
В частности, в следующих разделах мы дополним наш пример Shape1.mq5, и в классе Shape появится такое поле (со строкой-описанием — type). Тогда оператор присваивания станет генерировать ошибки (в частности, для таких строк, как с переменной s4):
attempting to reference deleted function
|
Благодаря подробным формулировкам компилятора можно понять суть и причины происходящего: во-первых, упоминается оператор присваивания ('='), а не конструктор копирования; во-вторых, сообщается, что оператор присваивания был неявно удален из-за наличия модификатора const. Здесь нам встречаются пока неизвестные понятия, которые мы изучим позднее: перегрузка операторов в классах, приведение объектных типов и возможность помечать методы удаленными.
В разделе Наследование, после того как мы научимся описывать производные классы, потребуется сделать некоторые уточнения относительно конструкторов копирования в иерархиях классов.