Обнуление объектов и массивов

Обычно инициализация или заполнение переменных и массивов не вызывает проблем. Так, для простых переменных действительно довольно просто использовать оператор '=' в инструкции определения вместе с инициализацией или присвоить требуемое значение в любой более поздний момент.

Для структур доступна агрегатная инициализация вида (см. раздел Определение структур):

Struct struct = {value1, value2, ...};

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

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

Для массивов, как мы знаем, существуют функции ArrayInitialize и ArrayFill, но они поддерживают только числовые типы: массив строк или структур ими заполнить не получится.

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

void ZeroMemory(void &entity)

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

Переменные получают значение 0 (для чисел) или его эквивалент (NULL для строк и указателей).

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

Однако для допустимых объектов есть ограничения. Заполняться нулями могут лишь объекты структур и классов, которые:

  • содержат только открытые поля (то есть в них нет данных с типом доступа private или protected);
  • не содержат полей с модификатором const;
  • не содержат указателей.

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

Третье ограничение относится к разряду рекомендаций: внешнее обнуление указателя затруднит контроль за целостностью данных, что, вероятно, приведет к потере связанного объекта и утечке памяти.

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

Примеры работы с ZeroMemory приведены в скрипте ZeroMemory.mq5.

Проблемы со списком агрегатной инициализации демонстрируются с помощью структуры Simple:

#define LIMIT 5
   
struct Simple
{
   MqlDateTime data[]; // динамический массив запрещает список инициализации,
   // string s;        // и строковое поле тоже запретило бы,
   // ClassType *ptr;  // и указатель тоже
   Simple()
   {
      // выделяем память, она будет содержать произвольные данные
      ArrayResize(dataLIMIT);
   }
};

В функции OnStart или в глобальном контексте мы не можем определить и тут же обнулить объект такой структуры:

void OnStart()
{
   Simple simple = {}; // ошибка: cannot be initialized with initializer list
   ...

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

Поэтому вместо списка инициализации используем ZeroMemory:

void OnStart()
{
   Simple simple;
   ZeroMemory(simple);
   ...

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

Далее в коде определен класс Base.

class Base
{
public// public обязателен для ZeroMemory
   // const у любого поля вызовет ошибку компиляции при вызове ZeroMemory:
   // "not allowed for objects with protected members or inheritance"
   /* const */ int x;
   Simple t;   // используем вложенную структуру: она тоже будет обнулена
   Base()
   {
      x = rand();
   }
   virtual void print() const
   {
      PrintFormat("%d %d", &thisx);
      ArrayPrint(t.data);
   }
};

Поскольку класс далее используется в массивах объектов, обнуляемых с помощью ZeroMemory, мы вынуждены прописать для его полей секцию доступа public (что, в принципе, не типично для классов и сделано для иллюстрации навязываемых требований со стороны ZeroMemory). Также обратите внимание, что поля не могут иметь модификатор const. В противном случае мы получим ошибку компиляции с текстом, который, к сожалению, не особо соответствует проблеме: "запрещено для объектов с защищенными членами или наследованием".

Конструктор класса заполняет поле x случайным числом, чтобы потом можно было наглядно увидеть его очистку функцией ZeroMemory. Метод print выводит содержимое всех полей для анализа, включая и уникальный номер (дескриптор) объекта — &this.

MQL5 не запрещает "натравить" ZeroMemory на переменную указатель:

   Base *base = new Base();
   ZeroMemory(base); // установит указатель в NULL, но оставит объект

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

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

Функция позволяет работать с объектами иерархии классов. Например, мы можем описать производный от Base класс Dummy:

class Dummy : public Base
{
public:
   double data[]; // мог бы быть и многомерным: ZeroMemory сработает
   string s;
   Base *pointer// публичный указатель (опасно)
   
public:
   Dummy()
   {
      ArrayResize(dataLIMIT);
      
      // из-за последующего применения ZeroMemory к объекту
      // мы потеряем указатель 'pointer'
      // и получим предупреждения при завершении скрипта
      // о неудаленных объектах типа Base
      pointer = new Base();
   }
   
   ~Dummy()
   {
      // из-за применения ZeroMemory, этот указатель пропадет
      // и не будет освобожден
      if(CheckPointer(pointer) != POINTER_INVALIDdelete pointer;
   }
   
   virtual void print() const override
   {
      Base::print();
      ArrayPrint(data);
      Print(pointer);
      if(CheckPointer(pointer) != POINTER_INVALIDpointer.print();
   }
};

Он включает поля с динамическим массивом типа double, строку и указатель типа Base (это тот же тип, от которого класс наследован, но он здесь использован только для демонстрации проблем с указателем, чтобы не описывать еще один фиктивный класс). Когда функция ZeroMemory обнуляет объект Dummy, объект по указателю pointer теряется и не может быть освобожден в деструкторе. В результате, это приводит после завершения скрипта к появлению предупреждений об утечке памяти в оставшихся объектах.

ZeroMemory используется в OnStart для очистки массива объектов Dummy:

void OnStart()
{
   ...
   Print("Initial state");
   Dummy array[];
   ArrayResize(arrayLIMIT);
   for(int i = 0i < LIMIT; ++i)
   {
      array[i].print();
   }
   ZeroMemory(array);
   Print("ZeroMemory done");
   for(int i = 0i < LIMIT; ++i)
   {
      array[i].print();
   }

В журнал будет выведено примерно следующее (начальное состояние будет отличаться, т.к. оно выводит содержимое "грязной", только что выделенной памяти; здесь приведен небольшой фрагмент):

Initial state
1048576 31539
     [year]     [mon]    [day] [hour] [min] [sec] [day_of_week] [day_of_year]
[0]       0     65665       32      0     0     0             0             0
[1]       0         0        0      0     0     0         65624             8
[2]       0         0        0      0     0     0             0             0
[3]       0         0        0      0     0     0             0             0
[4] 5242880 531430129 51557552      0     0 65665            32             0
0.0 0.0 0.0 0.0 0.0
...
ZeroMemory done
1048576 0
    [year] [mon] [day] [hour] [min] [sec] [day_of_week] [day_of_year]
[0]      0     0     0      0     0     0             0             0
[1]      0     0     0      0     0     0             0             0
[2]      0     0     0      0     0     0             0             0
[3]      0     0     0      0     0     0             0             0
[4]      0     0     0      0     0     0             0             0
0.0 0.0 0.0 0.0 0.0
...
5 undeleted objects left
5 objects of type Base left
3200 bytes of leaked memory

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

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

Наконец, посмотрим, как ZeroMemory может решить проблему инициализации массива строк. Функции ArrayInitialize и ArrayFill не работают со строками.

   string text[LIMIT] = {};
   // некий алгоритм заполняет и использует 'text'
   // ...
   // затем нужно повторно использовать массив
   // вызовы функций дают ошибки:
   // ArrayInitialize(text, NULL);
   //      `-> no one of the overloads can be applied to the function call
   // ArrayFill(text, 0, 10, NULL);
   //      `->  'string' type cannot be used in ArrayFill function
   ZeroMemory(text);               // ok

В закомментированных инструкциях компилятор выдал бы ошибки, смысл которых в том, что тип string не поддерживается в данных функциях.

Выходом является функция ZeroMemory.