Динамические массивы

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

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

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

Например, в предыдущем разделе мы использовали массив array1D:

int array1D[] = {12345678910};

Из-за списка инициализации его размер известен компилятору, и потому массив является фиксированным.

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

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

Познакомимся с техническим описанием этой и других функций по работе с динамическими массивами, а затем проверим их с помощью тестового скрипта ArrayDynamic.mq5.

bool ArrayIsDynamic(const void &array[])

Функция проверяет, является ли переданный массив динамическим. Массив может быть любой разрешенной размерности от 1 до 4. Элементы массива могут быть любого типа.

Функция возвращает true для динамического массива или false в остальных случаях (фиксированный массив или массив с временным рядом, контролируемый самим терминалом или индикатором).

int ArrayResize(void &array[], int size, int reserve = 0)

Функция устанавливает новый размер size в первом измерении динамического массива array. Массив может быть любой разрешенной размерности от 1 до 4. Элементы массива могут быть любого типа.

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

Функция возвращает новый размер массива, если его изменение прошло успешно, или -1 в случае ошибки.

Если функция применяется к фиксированному массиву или временному ряду, их размер не изменяется. В этих случаях, если запрошенный размер меньше или равен текущему размеру массива, функция вернет значение параметра size, а иначе — -1.

При увеличении размера уже существующего массива все данные его элементов сохраняются. Добавленные элементы ничем не инициализируются и могут содержать произвольные некорректные данные ("мусор").

Установка размера массива в 0 — ArrayResize(array, 0) — не освобождает реально выделенную под него память, включая и возможный резерв. Такой вызов лишь обнулит метаданные по массиву. Это сделано из соображений оптимизации будущих операций с массивом, которые нельзя исключить. Для принудительной "чистки" памяти следует использовать ArrayFree (см. далее).

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

template<typename T>
struct DynArray
{
   int size;
   int capacity;
   T memory[];
};
 
template<typename T>
int DynArraySize(DynArray<T> &array)
{
   return array.size;
}
 
template<typename T>
void DynArrayFree(DynArray<T> &array)
{
   ArrayFree(array.memory);
   ZeroMemory(array);
}
 
template<typename T>
int DynArrayResize(DynArray<T> &arrayint sizeint reserve = 0)
{
   if(size > array.capacity)
   {
      static int temp;
      temp = array.capacity;
      long ul = (long)GetMicrosecondCount();
      array.capacity = ArrayResize(array.memorysize + reserve);
      array.size = MathMin(sizearray.capacity);
      ul -= (long)GetMicrosecondCount();
      PrintFormat("Reallocation: [%d] -> [%d], done in %d µs",
         temparray.capacity, -ul);
   }
   else
   {
      array.size = size;
   }
   return array.size;
}

Преимущество функции DynArrayResize по сравнению со встроенной ArrayResize в том, что здесь мы вставили отладочную печать для тех моментов, когда внутренняя емкость массива перераспределяется.

Теперь мы можем взять стандартный пример для функции ArrayResize из документации MQL5 и заменить в ней вызовы встроенных функций на "самодельные" аналоги с префиксом "Dyn". Модифицированный результат представлен в скрипте ArrayCapacity.mq5.

void OnStart()
{
   ulong start = GetTickCount();
   ulong now;
   int   count = 0;
   
   DynArray<doublea;
   
   // быстрый вариант с резервированием памяти
   Print("--- Test Fast: ArrayResize(arr,100000,100000)");
   
   DynArrayResize(a100000100000);
   
   for(int i = 1i <= 300000 && !IsStopped(); i++)
   {
      // установим новый размер и резерв на 100000 элементов
      DynArrayResize(ai100000);
      // на круглых итерациях покажем размер массива и затраченное время
      if(DynArraySize(a) % 100000 == 0)
      {
         now = GetTickCount();
         count++;
         PrintFormat("%d. ArraySize(arr)=%d Time=%d ms",
            countDynArraySize(a), (now - start));
         start = now;
      }
   }
   DynArrayFree(a);
   
   // теперь медленный вариант без резервирования (с меньшим резервом)
   count = 0;
   start = GetTickCount();
   Print("---- Test Slow: ArrayResize(slow,100000)");
   
   DynArrayResize(a100000100000);
   
   for(int i = 1i <= 300000 && !IsStopped(); i++)
   {
      // установим новый размер, но с резервом в 100 раз меньше: 1000
      DynArrayResize(ai1000);
      // на круглых итерациях покажем размер массива и затраченное время
      if(DynArraySize(a) % 100000 == 0)
      {
         now = GetTickCount();
         count++;
         PrintFormat("%d. ArraySize(arr)=%d Time=%d ms",
            countDynArraySize(a), (now - start));
         start = now;
      }
   }
}

Единственное существенное отличие: в медленном варианте вызов ArrayResize(a, i) заменен на более щадящий DynArrayResize(a, i, 1000), то есть перераспределение запрашивается все-таки не на каждой итерации, а на каждой 1000-ой (иначе журнал "захлебнется" от сообщений).

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

--- Test Fast: ArrayResize(arr,100000,100000)
Reallocation: [0] -> [200000], done in 17 µs
1. ArraySize(arr)=100000 Time=0 ms
2. ArraySize(arr)=200000 Time=0 ms
Reallocation: [200000] -> [300001], done in 2296 µs
3. ArraySize(arr)=300000 Time=0 ms
---- Test Slow: ArrayResize(slow,100000)
Reallocation: [0] -> [200000], done in 21 µs
1. ArraySize(arr)=100000 Time=0 ms
2. ArraySize(arr)=200000 Time=0 ms
Reallocation: [200000] -> [201001], done in 1838 µs
Reallocation: [201001] -> [202002], done in 1994 µs
Reallocation: [202002] -> [203003], done in 1677 µs
Reallocation: [203003] -> [204004], done in 1983 µs
Reallocation: [204004] -> [205005], done in 1637 µs
...
Reallocation: [295095] -> [296096], done in 2921 µs
Reallocation: [296096] -> [297097], done in 2189 µs
Reallocation: [297097] -> [298098], done in 2152 µs
Reallocation: [298098] -> [299099], done in 2767 µs
Reallocation: [299099] -> [300100], done in 2115 µs
3. ArraySize(arr)=300000 Time=219 ms

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

void ArrayFree(void &array[])

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

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

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

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

Давайте протестируем изученные выше функции: ArrayIsDynamic, ArrayResize, ArrayFree.

В скрипте ArrayDynamic.mq5 написана функция ArrayExtend, которая увеличивает размер динамического массива на 1 и записывает в новый элемент переданное значение.

template<typename T>
void ArrayExtend(T &array[], const T value)
{
   if(ArrayIsDynamic(array))
   {
      const int n = ArraySize(array);
      ArrayResize(arrayn + 1);
      array[n] = (T)value;
   }
}

Функция ArrayIsDynamic используется для проверки в условном операторе, чтобы массив модифицировался только в том случае, если он динамический. Функция ArrayResize позволяет изменить размер массива, а функция ArraySize — узнать текущий размер (она будет рассмотрена в следующем разделе).

В главной функции скрипта применим ArrayExtend для массивов разной категории: динамического и фиксированного.

void OnStart()
{
   int dynamic[];
   int fixed[10] = {}; // заполнение нулями
   
   PRT(ArrayResize(fixed0)); // предупреждение: неприменимо для фиксированного массива
   
   for(int i = 0i < 10; ++i)
   {
      ArrayExtend(dynamic, (i + 1) * (i + 1));
      ArrayExtend(fixed, (i + 1) * (i + 1));
   }
   
   Print("Filled");
   ArrayPrint(dynamic);
   ArrayPrint(fixed);
   
   ArrayFree(dynamic);
   ArrayFree(fixed); // предупреждение: неприменимо для фиксированного массива
   
   Print("Free Up");
   ArrayPrint(dynamic); // ничего не выводит
   ArrayPrint(fixed);
   ...
}

В строках кода, где вызываются функции, неприменимые для фиксированных массивов, компилятор выдает предупреждение "нельзя использовать для массива постоянного размера" ("cannot be used for static allocated array"). Важно отметить, что внутри функции ArrayExtend таких предупреждений нет, потому что в функцию может передаваться массив любой категории. Именно поэтому мы выполняем проверку с помощью ArrayIsDynamic.

После цикла в OnStart массив dynamic расширится до 10 и получит элементы, равные возведенным в квадрат индексам. Массив fixed останется заполнен нулями и не поменяет размер.

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

Посмотрим на результат выполнения скрипта.

   ArrayResize(fixed,0)=0
   Filled   
     1   4   9  16  25  36  49  64  81 100
   0 0 0 0 0 0 0 0 0 0
   Free Up
   0 0 0 0 0 0 0 0 0 0

Особый интерес вызывают динамические массивы с указателями на объекты. Определим простой класс-пустышку Dummy и создадим массив указателей на такие объекты.

class Dummy
{
};
 
void OnStart()
{
   ...
   Dummy *dummies[] = {};
   ArrayExtend(dummiesnew Dummy());
   ArrayFree(dummies);
}

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

1 undeleted objects left
1 object of type Dummy left
24 bytes of leaked memory

Дело в том, что функция управляет только той памятью, которая выделена под массив. В данном случае, эта память хранила один указатель, но то, на что он указывает, массиву не принадлежит. Иными словами, если массив содержит указатели на "внешние" объекты, то о них нужно позаботиться самостоятельно. Например, так:

for(int i = 0i < ArraySize(dummies); ++i)
{
   delete dummies[i];
}

Эту "чистку" нужно запускать до вызова ArrayFree.

Для сокращения записи можно использовать следующие макросы (цикл по элементам, вызов delete для каждого из них):

#define FORALL(Afor(int _iterator_ = 0_iterator_ < ArraySize(A); ++_iterator_)
#define FREE(P) { if(CheckPointer(P) == POINTER_DYNAMICdelete (P); }
#define CALLALL(ACALLFORALL(A) { CALL(A[_iterator_]) }

Тогда удаление указателей упрощается до такой записи:

   ...
   CALLALL(dummies, FREE);
   ArrayFree(dummies);

В качестве альтернативного решения вы можете использовать класс-обертку для указателей, вроде AutoPtr, который мы рассмотрели в разделе Шаблоны объектных типов. Тогда массив следует описать с типом AutoPtr. Поскольку в массиве будут храниться объекты-обертки, а не указатели, при очистке массива автоматически вызовутся деструкторы для каждой "обертки", а из них в свою очередь освободится память указателей.