Конструкторы и деструкторы

В числе методов, которые можно определить для структуры, есть специальные: конструктор(ы) и декструктор.

Конструктор имеет имя, совпадающее с именем структуры, и не возвращает значение (тип void). Конструктор, если он определен, будет вызываться в момент инициализации для каждого нового экземпляра структуры. За счет этого в конструкторе можно особым образом рассчитать начальное состояние структуры.

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

Например, мы можем описать пару конструктор в структуре Result: один — без параметров, и второй с одним параметром типа строка, для задания статуса.

struct Result
{
   ...
   void Result()
   {
      status = "ok";
   }
   void Result(string s)
   {
      status = s;
   }
};

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

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

Наличие явных конструкторов делает невозможным использование синтаксиса агрегатной инициализации. В связи с этим строка Result r = {}; в методе calculate перестанет компилироваться. Теперь мы имеем право использовать только один из конструкторов, которые сами предоставили. Например, следующие инструкции вызывают конструктор без параметров:

   Result r1;
   Result r2();

А создание структуры с заполненным статусом можно сделать так:

   Result r3("success");

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

   Result array[10];

Деструктор — это функция, которая будет вызвана при уничтожении объекта структуры. Деструктор имеет имя, совпадающее с именем структуры, но с префиксом в виде символа тильды '~'. Деструктор также как и конструктор не возвращает значения, но он и не принимает параметров.

Деструктор может быть только один.

Явно вызвать деструктор нельзя. Программа сама делает это при выходе из блока кода, где была определена локальная переменная-структура, или при освобождении массива структур.

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

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

struct Result
{
   ...
   void Result()
   {
      static int count = 0;
      Print(__FUNCSIG__" ", ++count);
      status = "ok";
   }
 
   void Result(string s)
   {
      static int count = 0;
      Print(__FUNCSIG__" ", ++count);
      status = s;
   }
 
   void ~Result()
   {
      static int count = 0;
      Print(__FUNCSIG__" ", ++count);
   }
};

Три статических переменных с именем count существуют независимо друг от друга: каждая ведет подсчет в контексте своей функции.

В результате запуска скрипта мы получим следующий журнал:

Result::Result() 1
Result::Result() 2
Result::Result() 3
Result::~Result() 1
Result::~Result() 2
0.5 1 ok
1.00000 2.00000 3.00000
Result::Result(string) 1
0.5 1 ok
1.00000 2.00000 3.00000
Result::~Result() 3
Result::~Result() 4

Разберемся, что это значит.

Первый экземпляр структуры создается в функции OnStart, в строке с вызовом calculate. При входе в конструктор значение счетчика count однократно инициализируется нулем и далее инкрементируется при каждом исполнении конструктора, поэтому в первый раз выводится значение 1.

Внутри функции calculate определяется локальная переменная типа Result, она зарегистрирована под номером 2.

Третий экземпляр структуры не столь очевиден. Дело в том, что для передачи результата из функции компилятор создает в неявном виде временную переменную, куда копирует данные локальной переменной. Вероятно, это поведение изменится в дальнейшем, и тогда локальный экземпляр будет "перемещаться" из функции без дублирования.

Последний вызов конструктора происходит в методе со строковым параметром, поэтому в нем счетчик вызовов равен 1.

Важно, что общее количество вызовов обоих конструкторов совпадает с количеством вызовов деструктора: 4.

Более подробно про конструкторы и деструкторы мы поговорим в Главе про классы.