Конструкторы: по умолчанию, параметрический, копирования

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

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

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

В зависимости от наличия и типов параметров, конструкторы делятся на:

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

 

Конструктор по умолчанию

Самый простой конструктор — без параметров — называется конструктором по умолчанию. В отличие от C++, в MQL5 не считается конструктором по умолчанию такой конструктор, у которого есть параметры и все они имеют значения по умолчанию (то есть все параметры опциональные, см. раздел Необязательные параметры).

Определим конструктор по умолчанию для класса Shape.

class Shape
{
   ...
public:
   Shape()
   {
      ...
   }
};

Разумеется, его следует делать в публичной секции класса.

Иногда конструкторы намеренно делают защищенными или закрытыми, чтобы держать под контролем процесс создания объектов, например, через так называемые фабричные методы. Но в данном случае, мы рассматриваем стандартный вариант композиции класса.

Для установки начальных значений переменным объекта мы могли бы воспользоваться привычными инструкциями присвоения:

public:
   Shape()
   {
      x = 0;
      y = 0;
      ...
   }

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

Например, для конструктора Shape можно написать так:

public:
   Shape() :
      x(0), y(0),
      backgroundColor(clrNONE)
   {
   }

Данный синтаксис является более предпочтительным, чем присваивание переменных в теле конструктора по нескольким причинам.

Во-первых, присваивание в теле функции производится уже после того, как соответствующая переменная создана. В зависимости от типа переменной это может означать, что для неё был сначала вызван конструктор по умолчанию и потом перезаписано новое значение (а это — лишние накладные расходы). В случае списка инициализации переменная сразу создается с нужным значением. Вероятно, компилятор сможем оптимизировать присваивание и в отсутствии списка инициализации, но в общем случае это не гарантировано.

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

В-третьих, переменные-поля пользовательских типов могут не иметь конструктора по умолчанию (то есть в их классе все доступные конструкторы имеют параметры). Это значит, что при создании переменной в неё нужно передать фактические параметры, и список инициализации позволяет это сделать: значения аргументов указываются внутри круглых скобок, как будто при явном вызове конструктора. Список инициализации можно использовать в определении конструкторов, но не других методов.

 

Параметрический конструктор

У параметрического конструктора, по определению, есть несколько параметров (один или больше).

Например, представим, что для координат x и y описана специальная структура с параметрическим конструктором:

struct Pair
{
   int xy;
   Pair(int aint b): x(a), y(b) { }
};

Тогда мы можем использовать поле coordinates нового типа Pair вместо двух целочисленных полей x и y в классе Shape. Такая конструкция объектов называется включением или композиционным агрегированием. Объект Pair является неотъемлемой частью объекта Shape. Пара координат автоматически создается и уничтожается вместе с объектом "хозяином".

Поскольку у Pair нет конструктора без параметров, поле coordinates должно быть указано в списке инициализации конструктора Shape, с двумя параметрами (int, int):

class Shape
{
protected:
   // int x, y;
   Pair coordinates;  // координаты центра (включение объекта)
   ...
public:
   Shape() :
      // x(0), y(0),
      coordinates(00), // инициализация объекта
      backgroundColor(clrNONE
   {
   }
};

Без списка инициализации не удастся создать подобные автоматические объекты.

С учетом изменения способа хранения координат в объекте нам необходимо обновить метод toString:

   string toString() const
   {
      return (string)coordinates.x + " " + (string)coordinates.y;
   }

Но это не окончательная его версия: вскоре мы внесем еще некоторые правки.

Напомним, что автоматические переменные описывались в разделе Инструкции объявления/определения. Они называются автоматическими, потому что компилятор создает их (выделяет память) автоматически, и также автоматически удаляет, когда выполнение программы покидает контекст (блок кода), в котором переменная была создана.
 
В случае объектных переменных автоматическое создание означает не только выделение памяти, но и вызов конструктора. А автоматическое удаление объекта сопровождается вызовом его деструктора (см. далее раздел Деструкторы). Причем, если объект входит в состав другого объекта, то его время жизни совпадает с временем жизни своего "хозяина", как в случае поля coordinates — экземпляра Pair в составе объекта Shape.
 
Статические (в том числе глобальные) объекты также управляются компилятором автоматически.
 
Альтернативой автоматическому распределению является динамическое создание объектов и работа с ними через указатели.

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

Добавим в класс Shape еще один конструктор, позволяющий задать конкретные значения переменным. Он как раз будет является параметрическим конструктором (их можно создать сколько угодно: для разных целей и с разным набором параметров).

   Shape(int pxint pycolor back) :
      coordinates(pxpy),
      backgroundColor(back)
   {
   }

Список инициализации гарантирует, что когда выполняется тело конструктора, все внутренние поля (включая вложенные объекты, если они есть) уже созданы и проинициализированы.

Порядок инициализации членов класса соответствует не списку инициализации, а последовательности их объявления в классе.

Если в классе описан конструктор с параметрами, и при этом требуется разрешить создание объектов без аргументов, программист должен реализовать конструктор по умолчанию явным образом.

В том случае, если в классе вообще нет конструкторов, компилятор неявным образом предоставляет конструктор по умолчанию в виде заглушки, которая ответственна за инициализацию полей следующих типов: строки, динамические массивы, автоматические объекты с конструктором по умолчанию. Если таких полей нет, неявный конструктор по умолчанию ничего не делает. Поля других типов неявный конструктор не "трогает", поэтому в них будет содержаться случайный "мусор". Чтобы этого избежать, программист должен описать конструктор в явном виде и установить начальные значения самостоятельно.

 

Конструктор копирования

Конструктор копирования позволяет создать объект на основе другого объекта, переданного по ссылке в качестве единственного параметра.

Например, для класса Shape конструктор копирования мог бы выглядеть так:

class Shape
{
   ...
   Shape(const Shape &source) :
      coordinates(source.coordinates.xsource.coordinates.y),
      backgroundColor(source.backgroundColor)
   {
   }
   ...
};

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

При наличии такого конструктора можно создавать объекты с помощью одного из двух синтаксисов:

void OnStart()
{
   Shape s;
   ...
   Shape s2(s);   // ok: синтаксис 1 - копирование
   Shape s3 = s;  // ok: синтаксис 2 - копирование через инициализацию
                  //                   (если есть конструктор копирования)
                  //                 - или присваивание
                  //                   (если нет конструктора копирования,
                  //                    но есть конструктор по умолчанию)
   
   Shape s4;      // определение
   s4 = s;        // присваивание, а не конструктор копирования!
}

Следует различать инициализацию объекта при создании и присваивание.

Второй вариант (помечен комментарием "синтаксис 2") будет работать даже если конструктора копирования не существует, но есть конструктор по умолчанию. В этом случае компилятор сформирует менее эффективный код: сначала создаст с помощью конструктора по умолчанию пустой экземпляр приемной переменной (s3, в данном случае), а потом скопирует поля образца (s, в данном случае) поэлементно. Фактически, получится тот же случай, что с переменной s4, для которой определение и присваивание выполнены отдельными инструкциями.

Если конструктора копирования нет, то попытка использовать первый синтаксис приведет к ошибке "недопустимое преобразование параметра" ("parameter conversion not allowed"), так как компилятор попытается взять какой-либо другой конструктор из имеющихся, с отличным набором параметров.

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

В частности, в следующих разделах мы дополним наш пример Shape1.mq5, и в классе Shape появится такое поле (со строкой-описанием — type). Тогда оператор присваивания станет генерировать ошибки (в частности, для таких строк, как с переменной s4):

attempting to reference deleted function
   'void Shape::operator=(const Shape&)'
function 'void Shape::operator=(const Shape&)' was implicitly deleted
   because member 'type' has 'const' modifier

Благодаря подробным формулировкам компилятора можно понять суть и причины происходящего: во-первых, упоминается оператор присваивания ('='), а не конструктор копирования; во-вторых, сообщается, что оператор присваивания был неявно удален из-за наличия модификатора const. Здесь нам встречаются пока неизвестные понятия, которые мы изучим позднее: перегрузка операторов в классах, приведение объектных типов и возможность помечать методы удаленными.

В разделе Наследование, после того как мы научимся описывать производные классы, потребуется сделать некоторые уточнения относительно конструкторов копирования в иерархиях классов.