动态数组
动态数组可在程序执行期间根据编程人员的要求更改其大小。别忘了,要描述动态数组,应将位于数组标识符后面的第一对括号留空。MQL5 要求所有后续维度(如果多于一个维度)必须具有以常量指定的固定大小。
无法为“旧于”第一个维度的任何维度动态增加元素数量。此外,由于大小描述的严格性,数组具有“正方形”形状,例如,无法构造列或行长度不同的二维数组。如果任何这些限制对于算法实现至为关键,你不应使用标准 MQL5 数组,而应使用你自己的以 MQL5 编写的结构体或类。
请注意,如果一个数组在第一维度中没有大小,但有一个可让你确定该大小的初始化列表,则该数组是一个固定大小数组,而不是动态数组。
例如,在前一章节中,我们使用了 array1D 数组:
int array1D[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
由于有初始化列表,其大小对于编译器已知,因此数组大小固定。
与这个简单示例不同的是,判定真实程序中的一个特定数组是否是动态数组并不总是那么容易。尤其是,数组可作为参数传递到函数。然而,知道一个数组是否是动态数组可能很重要,因为仅可为此类数组通过调用 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 以及类似函数 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> &array, int size, int reserve = 0)
{
if(size > array.capacity)
{
static int temp;
temp = array.capacity;
long ul = (long)GetMicrosecondCount();
array.capacity = ArrayResize(array.memory, size + reserve);
array.size = MathMin(size, array.capacity);
ul -= (long)GetMicrosecondCount();
PrintFormat("Reallocation: [%d] -> [%d], done in %d µs",
temp, array.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<double> a;
// fast option with memory reservation
Print("--- Test Fast: ArrayResize(arr,100000,100000)");
DynArrayResize(a, 100000, 100000);
for(int i = 1; i <= 300000 && !IsStopped(); i++)
{
// set the new size and reserve to 100000 elements
DynArrayResize(a, i, 100000);
// 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",
count, DynArraySize(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(a, 100000, 100000);
for(int i = 1; i <= 300000 && !IsStopped(); i++)
{
// set new size but with 100 times smaller margin: 1000
DynArrayResize(a, i, 1000);
// 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",
count, DynArraySize(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(参见下文)。
我们测试上述函数: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(array, n + 1);
array[n] = (T)value;
}
}
|
ArrayIsDynamic 函数用于确保仅当数组是动态数组时才更新数组。这一判断是通过条件语句完成的。ArrayResize 函数可让你更改数组的大小,而 ArraySize 函数用于找出当前大小(将在下一节讨论该函数)。
在脚本的主函数中,我们将为不同类别的数组(fixed 数组和动态数组)应用 ArrayExtend。
void OnStart()
{
int dynamic[];
int fixed[10] = {}; // padding with zeros
PRT(ArrayResize(fixed, 0)); // warning: not applicable for fixed array
for(int i = 0; i < 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(dummies, new Dummy());
ArrayFree(dummies);
}
|
在使用一个新指针扩展了 dummy 数组之后,我们使用 ArrayFree 释放该数组,但在终端日志中有条目表明该对象还留在内存中。
1 undeleted objects left
1 object of type Dummy left
24 bytes of leaked memory
|
原因在于,该函数仅管理分配给该数组的内存。在此情况下,该内存保持一个指针,但该指针所指向的对象不属于该数组。换言之,如果该数组包含“外部”对象的指针,则你需要自己管理它们。例如:
for(int i = 0; i < ArraySize(dummies); ++i)
{
delete dummies[i];
}
|
这种删除操作必须在调用 ArrayFree 之前开始。
为缩短条目,可使用以下宏(对元素循环,为每个元素调用 delete):
#define FORALL(A) for(int _iterator_ = 0; _iterator_ < ArraySize(A); ++_iterator_)
#define FREE(P) { if(CheckPointer(P) == POINTER_DYNAMIC) delete (P); }
#define CALLALL(A, CALL) FORALL(A) { CALL(A[_iterator_]) }
|
此时指针删除操作可简化为以下表示:
...
CALLALL(dummies, FREE);
ArrayFree(dummies);
|
作为替代解决方案,可使用诸如 AutoPtr 的指针包装器类,我们已在 对象类型模板章节讨论过。然后该数组应使用 AutoPtr 类型声明。由于该数组存储的是包装器对象,而不是指针,因此当该数组被清除时,每个“包装器”的析构函数将被自动调用,并释放它们的指针内存。