动态数组

动态数组可在程序执行期间根据编程人员的要求更改其大小。别忘了,要描述动态数组,应将位于数组标识符后面的第一对括号留空。MQL5 要求所有后续维度(如果多于一个维度)必须具有以常量指定的固定大小。

无法为“旧于”第一个维度的任何维度动态增加元素数量。此外,由于大小描述的严格性,数组具有“正方形”形状,例如,无法构造列或行长度不同的二维数组。如果任何这些限制对于算法实现至为关键,你不应使用标准 MQL5 数组,而应使用你自己的以 MQL5 编写的结构体或类。

请注意,如果一个数组在第一维度中没有大小,但有一个可让你确定该大小的初始化列表,则该数组是一个固定大小数组,而不是动态数组。

例如,在前一章节中,我们使用了 array1D 数组:

int array1D[] = {12345678910};

由于有初始化列表,其大小对于编译器已知,因此数组大小固定。

与这个简单示例不同的是,判定真实程序中的一个特定数组是否是动态数组并不总是那么容易。尤其是,数组可作为参数传递到函数。然而,知道一个数组是否是动态数组可能很重要,因为仅可为此类数组通过调用 ArrayResize 来手动分配内存。

在此等情况下,ArrayIsDynamic 函数可让你确定数组类型。

我们来了解一些处理动态数组的函数的一些技术内容,然后使用 ArrayDynamic.mq5 脚本测试动态数组。

bool ArrayIsDynamic(const void &array[])

该函数检查传递的函数是否为动态。一个数组可以是任何允许的 1 维到 4 维的数组。数组元素可以是任何类型。

对于动态数组,该函数返回 true,对于其它数组,返回 false(fixed 数组或具有 时间序列的数组由终端本身或由指标控制的数组)。

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

该函数设置动态 array 的第一维度中的新 size。一个数组可以是任何允许的 1 维到 4 维的数组。数组元素可以是任何类型。

如果 reserve 参数大于零,则为数组分配的内存会预留出指定数量元素的存储空间。这可以提高具有多个连续函数调用的程序的运行速度。在数组的新请求大小超过当前大小(含保留空间)之前,将不会进行物理内存重新分配,新元素的内存将从预留部分中获取。

如果修改成功,则函数返回数组的新大小,如果出错,则返回 -1。

如果函数被运用于 fixed 数组或时间序列,其大小不会改变。在这些情况下,如果请求的大小小于或等于数组的当前大小,则函数将返回 size 参数的值,如果小于,则返回 -1。

增加现有数组的大小时,其元素的所有数据将保留。添加的元素不会以任何值初始化,并且可能包含任意不正确数据(“垃圾”)。

将数组大小设置为 0 (ArrayResize(array, 0),) 并不会释放实际分配给该数组的内存,包括可能的预留空间。这种调用只会重置数组的元数据。这样做是为了优化数组的将来操作。要强制释放内存,请使用 ArrayFree(见下文)。

请务必理解,并非每次调用该函数时都会使用 reserve 参数,仅在实际执行了内存重新分配时才会使用,即当请求的大小超过数组的当前容量(包括预留空间)时才会使用。为直观说明这一工作原理,我们创建内部数组对象的不完整副本,并为其实线孪生函数 ArrayResize 以及类似函数 ArrayFreeArraySize,以获得完整工具包。

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 函数的一个优势在于,我们针对数组的内部容量被重新分配的情况插入调试打印。

现在我们看看来自 MQL5 文档的 ArrayResize 函数的标准示例,并以带有 "Dyn" 前缀的自制类似函数替换内置函数调用。修改后的结果在 ArrayCapacity.mq5 脚本中提供。

void OnStart()
{
   ulong start = GetTickCount();
   ulong now;
   int   count = 0;
   
   DynArray<doublea;
   
 // fast option with memory reservation
   Print("--- Test Fast: ArrayResize(arr,100000,100000)");
   
   DynArrayResize(a100000100000);
   
   for(int i = 1i <= 300000 && !IsStopped(); i++)
   {
 // set the new size and reserve to 100000 elements
      DynArrayResize(ai100000);
 // on "round" iterations, show the size of the array and the elapsed time
      if(DynArraySize(a) % 100000 == 0)
      {
         now = GetTickCount();
         count++;
         PrintFormat("%d. ArraySize(arr)=%d Time=%d ms"
            countDynArraySize(a), (now - start));
         start = now;
      }
   }
   DynArrayFree(a);
   
 // now this is a slow option without redundancy (with less redundancy)
   count = 0;
   start = GetTickCount();
   Print("---- Test Slow: ArrayResize(slow,100000)");
   
   DynArrayResize(a100000100000);
   
   for(int i = 1i <= 300000 && !IsStopped(); i++)
   {
 // set new size but with 100 times smaller margin: 1000
      DynArrayResize(ai1000);
 // on "round" iterations, show the size of the array and the elapsed time
      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 中的数组会自动释放内存。无论数组是局部定义(在函数内)还是全局定义,无论其是 fixed 数组或动态数组,因为系统在任何情况下均将释放内存,无需编程人员的明确操作。

因此没有必要调用该函数。但有些情况下,算法中使用数组以某些内容从头重新填充,即需要在每次填充之前释放。在此情况下,此功能可能就比较方便。

请记住,如果数组元素包含动态分配对象的指针,则该函数不会删除这些对象:编程人员必须为这些对调用 delete(参见下文)。

我们测试上述函数:ArrayIsDynamicArrayResizeArrayFree

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 函数用于找出当前大小(将在下一节讨论该函数)。

在脚本的主函数中,我们将为不同类别的数组(fixed 数组和动态数组)应用 ArrayExtend

void OnStart()
{
   int dynamic[];
   int fixed[10] = {}; // padding with zeros
   
   PRT(ArrayResize(fixed0)); // warning: not applicable for fixed array
   
   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); // warning: not applicable for fixed array
   
   Print("Free Up");
   ArrayPrint(dynamic); // outputs nothing
   ArrayPrint(fixed);
   ...
}

若代码行中调用了无法用于 fixed 数组的函数,计算机生成警告“无法用于静态分配数组”。务必要注意的是,在 ArrayExtend 函数中没有此类警告,因为任何类别的数组均可被传递到该函数。这就是我们使用 ArrayIsDynamic 来检查的原因。

OnStart, 中的一个循环后,dynamic 数组将扩展到 10,并且元素等于索引的平方。fixed 数组将保持填充零,大小不变。

使用 ArrayFree 释放 fixed 数组没有任何效果,动态数组实际上将被删除。在此情况下,最后一次打印尝试将不会在日志中生成任何行。

我们看看脚本执行结果。

   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 类型声明。由于该数组存储的是包装器对象,而不是指针,因此当该数组被清除时,每个“包装器”的析构函数将被自动调用,并释放它们的指针内存。