Skip to content

数组操作

很难想象有哪个程序,尤其是与交易相关的程序,能离开数组。我们在“数组”章节中已经学习了描述和使用数组的一般原则。一系列用于处理数组的内置函数对这些原则进行了有机补充。

其中一些函数为最常用的数组操作提供了现成的实现,比如查找最大值和最小值、排序、插入以及删除元素。

然而,有一些函数对于使用特定类型的数组来说是必不可少的。特别是,动态数组在使用之前必须先分配内存,而用于指标缓冲区的数据数组(我们将在本书的第 5 部分学习这种 MQL 程序类型)使用由一个特殊函数设置的特殊元素索引顺序。

我们将从数组的日志输出操作开始学习处理数组的函数。我们在本书前面的章节中已经见过它,并且在后续的许多章节中它都会很有用。

由于 MQL5 数组可以是多维的(从 1 到 4 维),在接下来的文本中我们将需要提及维度编号。我们将从第一个维度开始称呼它们为编号,这在几何上更容易理解,并且强调了一个事实,即数组至少必须有一个维度(即使它是空的)。然而,正如 MQL5(以及许多其他编程语言)中的惯例一样,每个维度的数组元素都是从 0 开始编号的。因此,对于被描述为 array[5][10] 的数组,第一个维度的大小是 5,第二个维度的大小是 10。

数组日志记录

将变量、数组以及有关 MQL 程序状态的消息打印到日志中,是向用户告知信息、调试程序和诊断问题的最简单方法。对于数组,我们可以使用在示例脚本中已经了解的 Print 函数来逐个元素地打印。我们稍后会在与用户交互的部分正式介绍它。

不过,将遍历元素并对其进行精确格式化的整个常规操作交给 MQL5 环境来处理会更加方便。为此,API 提供了一个特殊的 ArrayPrint 函数。

我们在“使用数组”部分已经见过使用这个函数的示例。现在让我们更详细地讨论它的功能。

void ArrayPrint(const void &array[], uint digits = _Digits, const string separator = NULL,
  ulong start = 0, ulong count = WHOLE_ARRAY,
  ulong flags = ARRAYPRINT_HEADER | ARRAYPRINT_INDEX | ARRAYPRINT_LIMIT | ARRAYPRINT_DATE | ARRAYPRINT_SECONDS)

该函数使用指定的设置将数组记录到日志中。数组必须是内置类型之一或简单结构体类型。简单结构体是指其字段为内置类型的结构体,但不包括字符串和动态数组。如果结构体中包含类对象和指针,那么它就不属于简单结构体的范畴。

数组的维度必须是 1 维或 2 维。格式化会自动适应数组的结构,并在可能的情况下以可视化的形式显示数组(见下文)。尽管 MQL5 支持维度高达 4 的数组,但该函数不会显示 3 维或更多维的数组,因为很难将它们以“平面”形式表示出来。在程序编译或执行阶段,这不会产生错误。

除了第一个参数外,所有参数都可以省略,并且为它们定义了默认值。

  • digits 参数用于实数数组和结构体的数字字段。它设置数字小数部分显示的字符数。默认值是预定义的图表变量之一,即 _Digits,它表示当前图表中符号价格的小数位数。
  • 分隔字符 separator 用于在显示结构体数组中的字段时指定列分隔符。默认值为 NULL 时,函数使用空格作为分隔符。
  • startcount 参数分别设置起始元素的编号和要打印的元素数量。默认情况下,函数会打印整个数组,但结果可能会受到 ARRAYPRINT_LIMIT 标志的影响(见下文)。
  • flags 参数接受一组标志的组合,这些标志控制各种显示特性。以下是其中一些标志:
    • ARRAYPRINT_HEADER:在结构体数组之前输出包含结构体字段名称的标题;对非结构体数组无影响。
    • ARRAYPRINT_INDEX:按维度输出元素的索引(对于一维数组,索引显示在左侧;对于二维数组,索引显示在左侧和上方)。
    • ARRAYPRINT_LIMIT:用于大型数组,输出限制为前一百条和后一百条记录(默认启用此限制)。
    • ARRAYPRINT_DATE:用于 datetime 类型的值以显示日期。
    • ARRAYPRINT_MINUTES:用于 datetime 类型的值以显示精确到分钟的时间。
    • ARRAYPRINT_SECONDS:用于 datetime 类型的值以显示精确到秒的时间。

datetime 类型的值默认以 ARRAYPRINT_DATE | ARRAYPRINT_SECONDS 格式输出。

color 类型的值以十六进制格式输出。

枚举值以整数形式显示。

该函数不会输出嵌套数组、结构体和对象指针。这些部分会显示为三个点。

ArrayPrint.mq5 脚本演示了该函数的工作方式。

OnStart 函数定义了几个数组(一维、二维和三维),并使用 ArrayPrint(使用默认设置)将它们输出。

cpp
void OnStart()
{
   int array1D[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
   double array2D[][5] = {{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}};
   double array3D[][3][5] =
   {
      {{ 1,  2,  3,  4,  5}, { 6,  7,  8,  9, 10}, {11, 12, 13, 14, 15}},
      {{16, 17, 18, 19, 20}, {21, 22, 23, 24, 25}, {26, 27, 28, 29, 30}},
   };
   
   Print("array1D");
   ArrayPrint(array1D);
   Print("array2D");
   ArrayPrint(array2D);
   Print("array3D");
   ArrayPrint(array3D);
   ...
}

我们将在日志中得到以下内容:

plaintext
array1D
 1  2  3  4  5  6  7  8  9 10
array2D
         [,0]     [,1]     [,2]     [,3]     [,4]
[0,]  1.00000  2.00000  3.00000  4.00000  5.00000
[1,]  6.00000  7.00000  8.00000  9.00000 10.00000
array3D

array1D 数组不够大(可以在一行中显示),因此没有显示其索引。

array2D 数组有多行(有索引),因此显示了它们的索引(默认启用 ARRAYPRINT_INDEX)。

请注意,由于脚本是在 EURUSD 图表上运行的,该图表的价格是五位数,因此 _Digits = 5,这会影响 double 类型值的格式化。

array3D 数组被忽略了:没有为它输出任何行。

此外,脚本中还定义了 PairSimpleStruct 结构体:

cpp
struct Pair
{
   int x, y;
};
   
struct SimpleStruct
{
   double value;
   datetime time;
   int count;
   ENUM_APPLIED_PRICE price;
   color clr;
   string details;
   void *ptr;
   Pair pair;
};

SimpleStruct 包含内置类型的字段、一个指向 void 的指针,以及一个 Pair 类型的字段。

OnStart 函数中,创建了一个 SimpleStruct 类型的数组,并使用 ArrayPrint 以两种模式输出:默认设置和自定义设置(小数点后位数为 3,分隔符为 ;datetime 格式仅显示日期)。

cpp
void OnStart()
{
   ...
   SimpleStruct simple[] =
   {
      { 12.57839, D'2021.07.23 11:15', 22345, PRICE_MEDIAN, clrBlue, "text message"},
      {135.82949, D'2021.06.20 23:45', 8569, PRICE_TYPICAL, clrAzure},
      { 1087.576, D'2021.05.15 10:01:30', -3298, PRICE_WEIGHTED, clrYellow, "note"},
   };
   Print("SimpleStruct (default)");
   ArrayPrint(simple);
   
   Print("SimpleStruct (custom)");
   ArrayPrint(simple, 3, ";", 0, WHOLE_ARRAY, ARRAYPRINT_DATE);
}

这将产生以下结果:

plaintext
SimpleStruct (default)
       [value]              [time] [count] [type]    [clr]      [details] [ptr] [pair]
[0]   12.57839 2021.07.23 11:15:00   22345      5 00FF0000 "text message"   ...    ...
[1]  135.82949 2021.06.20 23:45:00    8569      6 00FFFFF0 null             ...    ...
[2] 1087.57600 2021.05.15 10:01:30   -3298      7 0000FFFF "note"           ...    ...
SimpleStruct (custom)
  12.578;2021.07.23;  22345;     5;00FF0000;"text message";  ...;   ...
 135.829;2021.06.20;   8569;     6;00FFFFF0;null          ;  ...;   ...
1087.576;2021.05.15;  -3298;     7;0000FFFF;"note"        ;  ...;   ...

请注意,我们在本例和前面部分使用的日志是在终端中生成的,用户可以在工具箱窗口的“专家”选项卡中查看。不过,接下来我们将了解测试器,它为某些类型的 MQL 程序(指标和专家顾问)提供了与终端本身相同的执行环境。如果这些程序在测试器中运行,ArrayPrint 函数和其他在“用户交互”部分描述的相关函数将把消息输出到测试代理的日志中。

到目前为止,我们一直并且在一段时间内仍将只使用脚本,而脚本只能在终端中执行。

动态数组

动态数组可以在程序执行过程中根据程序员的要求改变其大小。我们要记住,描述动态数组时,数组标识符后面的第一对方括号应该留空。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(固定数组,或者由终端本身或指标控制的时间序列数组)。

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

该函数设置动态数组第一维度的新大小。数组可以是 1 到 4 维之间的任何允许维度。数组元素可以是任何类型。

如果 reserve 参数大于 0,则为数组分配内存,并预留指定数量的元素空间。这可以提高有许多连续函数调用的程序的运行速度。在考虑预留空间后,数组的新请求大小超过当前大小时,才会进行实际的物理内存重新分配,并且新元素将从预留空间中获取。

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

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

当增加已存在数组的大小时,其所有元素的数据都会被保留。添加的元素不会进行任何初始化,可能包含任意不正确的数据(“垃圾数据”)。

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

重要的是要理解,reserve 参数并不是每次调用函数时都会使用,而只是在实际进行内存重新分配时才会使用,即当请求的大小超过数组当前包括预留空间在内的容量时。为了直观地展示这是如何工作的,我们将创建一个内部数组对象的不完整副本,并为其实现孪生函数 ArrayResize,以及类似的 ArrayFreeArraySize 函数,以拥有一个完整的工具集。

cpp
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 中呈现。

cpp
void OnStart()
{
   ulong start = GetTickCount();
   ulong now;
   int   count = 0;
   
   DynArray<double> a;
   
 // 带内存预留的快速选项
   Print("--- Test Fast: ArrayResize(arr,100000,100000)");
   
   DynArrayResize(a, 100000, 100000);
   
   for(int i = 1; i <= 300000 && !IsStopped(); i++)
   {
 // 将新大小和预留空间设置为 100000 个元素
      DynArrayResize(a, i, 100000);
 // 在“整”次迭代时,显示数组大小和经过的时间
      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);
   
 // 现在这是一个没有过多预留(预留较少)的慢速选项
   count = 0;
   start = GetTickCount();
   Print("---- Test Slow: ArrayResize(slow,100000)");
   
   DynArrayResize(a, 100000, 100000);
   
   for(int i = 1; i <= 300000 && !IsStopped(); i++)
   {
 // 设置新大小,但预留空间小 100 倍:1000
      DynArrayResize(a, i, 1000);
 // 在“整”次迭代时,显示数组大小和经过的时间
      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 函数的第三个参数设置的可能的预留内存),并将其第一维度的大小设置为 0。

从理论上讲,当当前代码块中的算法执行结束时,MQL5 中的数组会自动释放内存。数组是在本地(函数内部)还是全局定义的,以及它是固定大小还是动态大小都无关紧要,因为系统无论如何都会自动释放内存,不需要程序员进行显式操作。

因此,不一定需要调用这个函数。然而,有些情况下,数组在算法中用于从头重新填充内容,也就是说,在每次填充之前需要先释放它。这时这个功能就可能会派上用场。

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

让我们测试一下上面讨论的函数:ArrayIsDynamicArrayResizeArrayFree

ArrayDynamic.mq5 脚本中,编写了 ArrayExtend 函数,它将动态数组的大小增加 1,并将传递的值写入新元素。

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

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

cpp
void OnStart()
{
   int dynamic[];
   int fixed[10] = {}; // 用零填充
   
   PRT(ArrayResize(fixed, 0)); // 警告:不适用于固定数组
   
   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); // 警告:不适用于固定数组
   
   Print("Free Up");
   ArrayPrint(dynamic); // 不输出任何内容
   ArrayPrint(fixed);
   ...
}

在调用不适用于固定数组的函数的代码行中,编译器会生成“不能用于静态分配数组”的警告。重要的是要注意,在 ArrayExtend 函数内部没有这样的警告,因为可以将任何类别的数组传递给该函数。这就是为什么我们使用 ArrayIsDynamic 进行检查。

OnStart 中的循环之后,动态数组将扩展到 10 个元素,并获取等于索引平方的元素值。固定数组将仍然用零填充,并且大小不会改变。

使用 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,并创建一个指向此类对象的指针数组。

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

在用一个新指针扩展虚拟数组后,我们使用 ArrayFree 释放它,但终端日志中有记录表明对象仍留在内存中。

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

事实上,该函数仅管理为数组分配的内存。在这种情况下,该内存保存了一个指针,但它所指向的内容并不属于数组。换句话说,如果数组包含指向“外部”对象的指针,那么你需要自己处理这些对象。例如:

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

必须在调用 ArrayFree 之前开始这种删除操作。

为了缩短代码,可以使用以下宏(遍历元素,为每个元素调用 delete):

cpp
#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_]) }

然后,指针的删除操作简化为以下表示:

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

作为另一种解决方案,可以使用像 AutoPtr 这样的指针包装类,我们在“对象类型模板”部分讨论过它。然后数组应该用 AutoPtr 类型声明。由于数组将存储包装对象而不是指针,当数组被清除时,每个“包装器”的析构函数将自动被调用,并且指针内存将依次从它们中释放。

数组大小的测量

数组的主要特征之一是它的大小,也就是数组中元素的总数。需要注意的是,对于多维数组,其大小是所有维度长度的乘积。

对于固定数组,你可以在编译阶段使用基于 sizeof 运算符的语言结构来计算它们的大小:

sizeof(array) / sizeof(type)

其中 array 是数组标识符,type 是数组类型。

例如,如果在代码中定义了一个固定数组:

c
int fixed[][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};

那么它的大小为:

c
int n = sizeof(fixed) / sizeof(int); // 8

对于动态数组,这个规则不适用,因为 sizeof 运算符总是生成内部动态数组对象的相同大小:52 字节。

请注意,在函数内部,所有数组参数在内部都表示为动态数组包装器对象。这样做是为了可以将任何内存分配方式的数组(包括固定大小的数组)传递给函数。这就是为什么对于参数数组 sizeof(array) 将返回 52,即使通过它传递的是一个固定大小的数组。

“包装器” 的存在只影响 sizeof 运算符。ArrayIsDynamic 函数总是能够正确地确定通过参数数组传递的实际参数的类别。

为了在程序执行阶段获取任何数组的大小,请使用 ArraySize 函数。

c
int ArraySize(const void &array[])

该函数返回数组中元素的总数。数组的维度和类型可以是任意的。对于一维数组,该函数的调用类似于 ArrayRange(array, 0)(见下文)。

如果数组在分配内存时带有预留空间(ArrayResize 函数的第三个参数),则该预留空间的值不会被考虑在内。

在使用 ArrayResize 为动态数组分配内存之前,ArraySize 函数将返回 0。此外,在对数组调用 ArrayFree 之后,其大小也会变为 0。

c
int ArrayRange(const void &array[], int dimension)

ArrayRange 函数返回指定数组维度中的元素数量。数组的维度和类型可以是任意的。参数 dimension 必须在 0 到数组维度数减 1 的范围内。索引 0 对应第一维度,索引 1 对应第二维度,以此类推。

ArrayRange(array, i) 对于所有维度 i 的值的乘积等于 ArraySize(array)

让我们来看一下上述函数的示例(见文件 ArraySize.mq5)。

c
void OnStart()
{
   int dynamic[];
   int fixed[][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};
   
   PRT(sizeof(fixed) / sizeof(int));   // 8
   PRT(ArraySize(fixed));              // 8
   
   ArrayResize(dynamic, 10);
   
   PRT(sizeof(dynamic) / sizeof(int)); // 13 (不正确)
   PRT(ArraySize(dynamic));            // 10
   
   PRT(ArrayRange(fixed, 0));          // 2
   PRT(ArrayRange(fixed, 1));          // 4
   
   PRT(ArrayRange(dynamic, 0));        // 10
   PRT(ArrayRange(dynamic, 1));        // 0
   int size = 1;
   for(int i = 0; i < 2; ++i)
   {
      size *= ArrayRange(fixed, i);
   }
   PRT(size == ArraySize(fixed));      // true
}

数组的初始化和填充

只有固定大小的数组才能使用初始化列表来描述。动态数组只有在使用 ArrayResize 函数为其分配内存之后才能进行填充。可以使用 ArrayInitializeArrayFill 函数来填充它们。当你想批量替换固定数组或时间序列中的值时,这些函数在程序中也很有用。

下面在介绍这些函数之后会给出使用示例。

c
int ArrayInitialize(type &array[], type value)

该函数将数组的所有元素设置为指定的值。仅支持内置数值类型的数组(charucharshortushortintuintlongulongboolcolordatetimefloatdouble)。字符串、结构体和指针数组不能用这种方式填充,需要实现自己的初始化函数。数组可以是多维的。

该函数返回元素的数量。

如果动态数组在分配时带有预留空间(ArrayResize 函数的第三个参数),则预留空间不会被初始化。

如果在数组初始化后,使用 ArrayResize 增加其大小,新增的元素不会自动设置为指定的值。可以使用 ArrayFill 函数来填充这些元素。

c
void ArrayFill(type &array[], int start, int count, type value)

该函数用指定的值填充一个数值数组或数组的一部分。数组的部分由参数 startcount 指定,分别表示元素的起始编号和要填充的元素数量。

该函数不关心数组元素的编号顺序是否像时间序列那样设置,这个属性会被忽略。换句话说,数组元素总是从数组的开头到结尾进行计数。

对于多维数组,可以通过将所有维度的坐标转换为等效一维数组的连续索引来获得 start 参数。因此,对于二维数组,第一维索引为 0 的元素首先存储在内存中,然后是第一维索引为 1 的元素,依此类推。计算 start 的公式如下:

start = D1 * N2 + D2

其中 D1D2 分别是第一维和第二维的索引,N2 是第二维的元素数量。D2 的取值范围是从 0 到 (N2 - 1)D1 的取值范围是从 0 到 (N1 - 1)。例如,在数组 array[3][4] 中,索引为 [1][3] 的元素是连续的第 7 个元素,因此调用 ArrayFill(array, 7, 2, ...) 将填充两个元素:array[1][3] 和紧随其后的 array[2][0]。在图表中,可以这样表示(每个单元格包含元素的连续索引):

      [][0]  [][1]  [][2]  [][3]
[0][]    0      1      2      3
[1][]    4      5      6      7
[2][]    8      9     10     11

ArrayFill.mq5 脚本提供了使用上述函数的示例。

c
void OnStart()
{
   int dynamic[];
   int fixed[][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};
   
   PRT(ArrayInitialize(fixed, -1));
   ArrayPrint(fixed);
   ArrayFill(fixed, 3, 4, +1);
   ArrayPrint(fixed);
   
   PRT(ArrayResize(dynamic, 10, 50));
   PRT(ArrayInitialize(dynamic, 0));
   ArrayPrint(dynamic);
   PRT(ArrayResize(dynamic, 50));
   ArrayPrint(dynamic);
   ArrayFill(dynamic, 10, 40, 0);
   ArrayPrint(dynamic);
}

以下是可能的结果示例(动态数组未初始化元素中的随机数据会有所不同):

ArrayInitialize(fixed,-1)=8
    [,0][,1][,2][,3]
[0,]  -1  -1  -1  -1
[1,]  -1  -1  -1  -1
    [,0][,1][,2][,3]
[0,]  -1  -1  -1   1
[1,]   1   1   1  -1
ArrayResize(dynamic,10,50)=10
ArrayInitialize(dynamic,0)=10
0 0 0 0 0 0 0 0 0 0
ArrayResize(dynamic,50)=50
[ 0]           0           0           0           0           0
               0           0           0           0           0
[10] -1402885947  -727144693   699739629   172950740 -1326090126
           47384           0           0     4194184           0
[20]           2           0           2           0           0
               0           0  1765933056  2084602885 -1956758056
[30]    73910037 -1937061701          56           0          56
               0     1048601  1979187200       10851           0
[40]           0           0           0  -685178880 -1720475236
       782716519 -1462194191  1434596297   415166825 -1944066819
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

数组的复制与编辑

在本节中,我们将学习如何使用内置函数来插入和删除数组元素、改变元素顺序,以及复制整个数组。

bool ArrayInsert(void &target[], const void &source[], uint to, uint from = 0, uint count = WHOLE_ARRAY)

该函数将源数组 source 中指定数量的元素插入到目标数组 target 中。插入到目标数组的位置由参数 to 中的索引设置。从源数组开始复制元素的起始索引由参数 from 中的索引给出。参数 count 中的 WHOLE_ARRAY 常量((uint)-1)表示传输源数组的所有元素。

所有的索引和计数都相对于数组的第一维。换句话说,对于多维数组,插入操作不是针对单个元素进行的,而是针对由 “更高” 维度描述的整个结构进行的。例如,对于二维数组,参数 count 中的值 1 表示插入一个长度等于第二维的向量(见示例)。

因此,目标数组和源数组必须具有相同的结构。否则,将会发生错误,并且复制操作将失败。对于一维数组,这不是限制,但对于多维数组,必须确保除第一维之外的其他维度大小相等。特别地,不能将数组 [][4] 中的元素插入到数组 [][5] 中,反之亦然。

该函数仅适用于固定大小或动态大小的数组。不能使用此函数编辑时间序列数组(带有时间序列的数组)。禁止在参数 targetsource 中指定同一个数组。

当插入到固定数组中时,新元素会将现有元素向右移动,并将最右边的 count 个元素挤出数组。参数 to 的值必须在 0 到数组大小减 1 之间。

当插入到动态数组中时,旧元素也会向右移动,但它们不会消失,因为数组本身会扩展 count 个元素。参数 to 的值必须在 0 到数组大小之间。如果它等于数组的大小,新元素将被添加到数组的末尾。

指定的元素会从一个数组复制到另一个数组,即它们在原始数组中保持不变,并且在新数组中的 “副本” 成为独立的实例,与 “原始” 元素没有任何关系。

如果操作成功,该函数返回 true,否则返回 false

让我们看一些示例(ArrayInsert.mq5)。OnStart 函数提供了对几种不同结构的数组(包括固定数组和动态数组)的描述。

c
#define PRTS(A) Print(#A, "=", (string)(A) + " / status:" + (string)GetLastError())

void OnStart()
{
   int dynamic[];
   int dynamic2Dx5[][5];
   int dynamic2Dx4[][4];
   int fixed[][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};
   int insert[] = {10, 11, 12};
   int array[1] = {100};
   ...

首先,为了方便起见,引入了一个宏 PRTS,它在调用测试指令后立即显示错误代码(通过函数 GetLastError 获得)。这是我们熟悉的 PRT 宏的一个略微修改的版本。

在不同结构的数组之间复制元素的尝试将以错误代码 4006ERR_INVALID_ARRAY)结束。

c
// 不能混合一维和二维数组
PRTS(ArrayInsert(dynamic, fixed, 0)); // false:4006, ERR_INVALID_ARRAY
ArrayPrint(dynamic); // 空数组
// 不能混合第二维结构不同的二维数组
PRTS(ArrayInsert(dynamic2Dx5, fixed, 0)); // false:4006, ERR_INVALID_ARRAY
ArrayPrint(dynamic2Dx5); // 空数组
// 即使两个数组都是固定的(或都是动态的)
// “更高” 维度的大小也必须匹配
PRTS(ArrayInsert(fixed, insert, 0)); // false:4006, ERR_INVALID_ARRAY
ArrayPrint(fixed); // 未改变
   ...

目标索引必须在数组范围内。

c
// 目标索引 10 超出了数组 'insert' 的范围,
// 它的大小为 3,所以索引可以是 0、1、2
PRTS(ArrayInsert(insert, array, 10)); // false:5052, ERR_SMALL_ARRAY
ArrayPrint(insert); // 未改变
   ...

以下是成功的数组修改操作:

c
// 从 'fixed' 中复制第二行,为 'dynamic2Dx4' 分配内存
PRTS(ArrayInsert(dynamic2Dx4, fixed, 0, 1, 1)); // true
ArrayPrint(dynamic2Dx4);
// 将 'fixed' 中的两行都添加到 'dynamic2Dx4' 的末尾,它会扩展
PRTS(ArrayInsert(dynamic2Dx4, fixed, 1)); // true
ArrayPrint(dynamic2Dx4);
// 为 'dynamic' 分配内存,以容纳 'insert' 的所有元素
PRTS(ArrayInsert(dynamic, insert, 0)); // true
ArrayPrint(dynamic);
// 'dynamic' 扩展 1 个元素
PRTS(ArrayInsert(dynamic, array, 1)); // true
ArrayPrint(dynamic);
// 新元素会将 'insert' 中的最后一个元素挤出
PRTS(ArrayInsert(insert, array, 1)); // true
ArrayPrint(insert);

日志中会显示以下内容:

ArrayInsert(dynamic2Dx4,fixed,0,1,1)=true
    [,0][,1][,2][,3]
[0,]   5   6   7   8
ArrayInsert(dynamic2Dx4,fixed,1)=true
    [,0][,1][,2][,3]
[0,]   5   6   7   8
[1,]   1   2   3   4
[2,]   5   6   7   8
ArrayInsert(dynamic,insert,0)=true
10 11 12
ArrayInsert(dynamic,array,1)=true
 10 100  11  12
ArrayInsert(insert,array,1)=true
 10 100  11

bool ArrayCopy(void &target[], const void &source[], int to = 0, int from = 0, int count = WHOLE_ARRAY)

该函数将源数组的部分或全部内容复制到目标数组中。在目标数组中写入元素的位置由参数 to 中的索引指定。从源数组开始复制元素的起始索引由参数 from 中的索引给出。参数 count 中的 WHOLE_ARRAY 常量(-1)表示传输源数组的所有元素。如果 count 小于 0 或大于从 from 位置到源数组末尾的剩余元素数量,则会复制数组的整个剩余部分。

ArrayInsert 函数不同,ArrayCopy 函数不会移动接收数组中的现有元素,而是将新元素写入指定位置,覆盖旧元素。

所有的索引和元素数量的设置都考虑了元素的连续编号,而不管数组的维度数量及其结构如何。换句话说,元素可以从多维数组复制到一维数组,反之亦然,或者在多维数组之间(即使 “更高” 维度的大小不同)进行复制(见示例)。

该函数适用于固定数组、动态数组,以及作为指标缓冲区指定的时间序列数组。

允许将元素从一个数组复制到它自身。但是,如果目标区域和源区域重叠,需要记住迭代是从左到右进行的。

动态目标数组会根据需要自动扩展。固定数组保持其维度,并且复制的内容必须适合数组,否则将会发生错误。

支持内置类型的数组和具有简单类型字段的结构数组。对于数值类型,如果源类型和目标类型不同,函数将尝试转换数据。字符串数组只能复制到字符串数组中。不允许复制类对象,但可以复制对象的指针。

该函数返回复制的元素数量(如果发生错误则返回 0)。

在脚本 ArrayCopy.mq5 中有几个使用该函数的示例。

c
class Dummy
{
   int x;
};

void OnStart()
{
   Dummy objects1[5], objects2[5];
 // 错误:不允许包含对象的结构或类
   PRTS(ArrayCopy(objects1, objects2));
   ...

包含对象的数组会生成编译错误,提示 “不允许包含对象的结构或类”,但可以复制指针。

c
   Dummy *pointers1[5], *pointers2[5];
   for(int i = 0; i < 5; ++i)
   {
      pointers1[i] = &objects1[i];
   }
   PRTS(ArrayCopy(pointers2, pointers1)); // 5 / status:0
   for(int i = 0; i < 5; ++i)
   {
      Print(i, " ", pointers1[i], " ", pointers2[i]);
   }
 // 输出一些成对相同的对象描述符
   /*
   0 1048576 1048576
   1 2097152 2097152
   2 3145728 3145728
   3 4194304 4194304
   4 5242880 5242880
   */

具有简单类型字段的结构数组也可以毫无问题地进行复制。

c
struct Simple
{
   int x;
};

void OnStart()
{
   ...
   Simple s1[3] = {{123}, {456}, {789}}, s2[];
   PRTS(ArrayCopy(s2, s1)); // 3 / status:0
   ArrayPrint(s2);
   /*
       [x]
   [0] 123
   [1] 456
   [2] 789
   */
   ...

为了进一步演示如何处理不同类型和结构的数组,定义了以下数组(包括固定数组、动态数组以及不同维度的数组):

c
   int dynamic[];
   int dynamic2Dx5[][5];
   int dynamic2Dx4[][4];
   int fixed[][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};
   int insert[] = {10, 11, 12};
   double array[1] = {M_PI};
   string texts[];
   string message[1] = {"ok"};
   ...

当从固定数组的位置 1(数字 2)复制一个元素时,会在接收的动态数组 dynamic2Dx4 中分配一整行 4 个元素,并且由于只复制了 1 个元素,其余三个元素将包含随机的 “垃圾” 数据(以黄色突出显示)。

c
   PRTS(ArrayCopy(dynamic2Dx4, fixed, 0, 1, 1)); // 1 / status:0
   ArrayPrint(dynamic2Dx4);
   /*
       [,0][,1][,2][,3]
   [0,]   2   1   2   3
   */
   ...

接下来,将固定数组中从第三个元素开始的所有元素复制到同一个数组 dynamic2Dx4 中,但从位置 1 开始。由于复制了 5 个元素(固定数组中元素总数为 8,减去起始位置 3),并且它们被放置在索引 1 处,因此在接收数组中总共会占用 1 + 5 个位置,即 6 个元素。并且由于数组 dynamic2Dx4 的每一行(第二维)有 4 个元素,只能为其分配元素数量为 4 的倍数的内存,即还会分配 2 个元素,其中将保留随机数据。

c
   PRTS(ArrayCopy(dynamic2Dx4, fixed, 1, 3)); // 5 / status:0
   ArrayPrint(dynamic2Dx4);
   /*
       [,0][,1][,2][,3]
   [0,]   2   4   5   6
   [1,]   7   8   3   4
   */

当将多维数组复制到一维数组时,元素将以 “扁平” 形式呈现。

c
   PRTS(ArrayCopy(dynamic, fixed)); // 8 / status:0
   ArrayPrint(dynamic);
   /*
   1 2 3 4 5 6 7 8
   */

当将一维数组复制到多维数组时,元素将根据接收数组的维度进行 “展开”。

c
   PRTS(ArrayCopy(dynamic2Dx5, insert)); // 3 / status:0
   ArrayPrint(dynamic2Dx5);
   /*
       [,0][,1][,2][,3][,4]
   [0,]  10  11  12   4   5
   */

在这种情况下,复制了 3 个元素,它们适合放入长度为 5 个元素的一行中(根据接收数组的结构)。为该行剩余的两个元素分配了内存,但未填充(包含 “垃圾” 数据)。

我们可以从另一个源覆盖数组 dynamic2Dx5,包括从结构不同的多维数组进行覆盖。由于在接收数组中分配了两行,每行 5 个元素,而在源数组中分配了两行,每行 4 个元素,因此留下了 2 个未填充的元素。

c
   PRTS(ArrayCopy(dynamic2Dx5, fixed)); // 8 / status:0
   ArrayPrint(dynamic2Dx5);
   /*
       [,0][,1][,2][,3][,4]
   [0,]   1   2   3   4   5
   [1,]   6   7   8   0   0
   */

通过使用 ArrayCopy,可以更改固定接收数组中的元素。

c
   PRTS(ArrayCopy(fixed, insert)); // 3 / status:0
   ArrayPrint(fixed);
   /*
       [,0][,1][,2][,3]
   [0,]  10  11  12   4
   [1,]   5   6   7   8
   */

在这里,我们覆盖了数组 fixed 的前三个元素。然后让我们覆盖最后三个元素。

c
   PRTS(ArrayCopy(fixed, insert, 5)); // 3 / status:0
   ArrayPrint(fixed);
   /*
       [,0][,1][,2][,3]
   [0,]  10  11  12   4
   [1,]   5  10  11  12
   */

复制到等于固定数组长度的位置将不起作用(在这种情况下,动态目标数组会扩展)。

c
   PRTS(ArrayCopy(fixed, insert, 8)); // 4006, ERR_INVALID_ARRAY
   ArrayPrint(fixed); // 无变化

字符串数组与其他类型的数组组合会抛出错误:

c
   PRTS(ArrayCopy(texts, insert)); // 5050, ERR_INCOMPATIBLE_ARRAYS
   ArrayPrint(texts); // 空数组

但在字符串数组之间可以进行复制:

c
   PRTS(ArrayCopy(texts, message));
   ArrayPrint(texts); // "ok"

不同数值类型的数组在复制时会进行必要的转换。

c
   PRTS(ArrayCopy(insert, array, 1)); // 1 / status:0
   ArrayPrint(insert); // 10  3 12

在这里,我们将数字 Pi 写入整数数组中,因此得到了值 3(它替换了 11)。

bool ArrayRemove(void &array[], uint start, uint count = WHOLE_ARRAY)

该函数从数组中删除从索引 start 开始的指定数量的元素。数组可以是多维的,并且可以具有任何内置类型或具有内置类型字段的结构类型,但不包括字符串类型。

索引 start 和数量 count 是相对于数组的第一维而言的。换句话说,对于多维数组,删除操作不是针对单个元素进行的,而是针对由 “更高” 维度描述的整个结构进行的。例如,对于二维数组,参数 count 中的值 1 表示删除一整行,其长度等于第二维(见示例)。

start 必须在 0 到第一维大小减 1 之间。

该函数不能应用于带有时间序列的数组(内置的 timeseries 或指标缓冲区)。

为了测试该函数,我们准备了脚本 ArrayRemove.mq5。特别地,它定义了两个结构:

c
struct Simple
{
   int x;
   ```c
};

struct NotSoSimple
{
   int x;
   string s; // 一个字符串类型的字段会导致编译器生成一个隐式析构函数
};

Arrays with a simple structure can be processed by the function ArrayRemove successfully, while arrays of objects with destructors (even with implicit ones, as in NotSoSimple) cause an error:

void OnStart()
{
   Simple structs1[10];
   PRTS(ArrayRemove(structs1, 0, 5)); // true / status:0

   NotSoSimple structs2[10];
   PRTS(ArrayRemove(structs2, 0, 5)); // false / status:4005,
                                      // ERR_STRUCT_WITHOBJECTS_ORCLASS
   ...

   Next, arrays of various configurations are defined and initialized.

   int dynamic[];
   int dynamic2Dx4[][4];
   int fixed[][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};

   // 进行两次复制
   ArrayCopy(dynamic, fixed);
   ArrayCopy(dynamic2Dx4, fixed);

   // 显示初始数据
   ArrayPrint(dynamic);
   /*
   1 2 3 4 5 6 7 8
   */
   ArrayPrint(dynamic2Dx4);
   /*
       [,0][,1][,2][,3]
   [0,]   1   2   3   4
   [1,]   5   6   7   8
   */

   // 当从固定数组中删除元素时,被删除片段之后的所有元素都会向左移动。
   // 重要的是数组的大小不会改变,因此会出现被移动元素的重复副本。
   PRTS(ArrayRemove(fixed, 0, 1));
   ArrayPrint(fixed);
   /*
   ArrayRemove(fixed,0,1)=true / status:0
       [,0][,1][,2][,3]
   [0,]   5   6   7   8
   [1,]   5   6   7   8
   */

   // 在这里,我们通过偏移量 0 删除了二维数组 fixed 第一维的一个元素,
   // 即初始行。下一行的元素向上移动并保留在同一行中。

   // 如果对(内容与数组 fixed 相同的)动态数组执行相同的操作,
   // 它的大小会自动减少被删除元素的数量。
   PRTS(ArrayRemove(dynamic2Dx4, 0, 1));
   ArrayPrint(dynamic2Dx4);
   /*
   ArrayRemove(dynamic2Dx4,0,1)=true / status:0
       [,0][,1][,2][,3]
   [0,]   5   6   7   8
   */

   // 在一维数组中,每个被删除的元素对应一个单一的值。
   // 例如,在数组 dynamic 中,当从索引 2 开始删除三个元素时,我们得到以下结果:
   PRTS(ArrayRemove(dynamic, 2, 3));
   ArrayPrint(dynamic);
   /*
   ArrayRemove(dynamic,2,3)=true / status:0
   1 2 6 7 8
   */

   // 值 3、4、5 已被删除,数组大小减少了 3。
}

## bool ArrayReverse(void &array[], uint start = 0, uint count = WHOLE_ARRAY)
该函数反转数组中指定元素的顺序。要反转的元素由起始位置 `start` 和数量 `count` 确定。如果 `start = 0` 且 `count = WHOLE_ARRAY`,则会处理整个数组。

支持任意维度和类型的数组,包括固定数组和动态数组(包括指标缓冲区中的时间序列)。数组可以包含对象、指针或结构。对于多维数组,仅反转第一维。

`count` 的值必须在 `0` 到第一维的元素数量之间。请注意,`count` 小于 `2` 不会产生明显的效果,但可用于统一算法中的循环。

如果操作成功,该函数返回 `true`,否则返回 `false`。

可以使用 `ArrayReverse.mq5` 脚本来测试该函数。在其开头,定义了一个类,用于生成存储在数组中的对象。存在字符串和其他 “复杂” 字段不是问题。

```c
class Dummy
{
   static int counter;
   int x;
   string s; // 一个字符串类型的字段会导致编译器创建一个隐式析构函数
public:
   Dummy() { x = counter++; }
};

static int Dummy::counter;

对象通过一个序列号进行标识(在创建时分配)。

c
void OnStart()
{
   Dummy objects[5];
   Print("Objects before reverse");
   ArrayPrint(objects);
   /*
       [x]  [s]
   [0]   0 null
   [1]   1 null
   [2]   2 null
   [3]   3 null
   [4]   4 null
   */

   // 应用 ArrayReverse 之后,我们得到了预期的对象反转顺序。
   PRTS(ArrayReverse(objects)); // true / status:0
   Print("Objects after reverse");
   ArrayPrint(objects);
   /*
       [x]  [s]
   [0]   4 null
   [1]   3 null
   [2]   2 null
   [3]   1 null
   [4]   0 null
   */

   // 接下来,准备不同结构的数值数组,并使用不同的参数进行反转。
   int dynamic[];
   int dynamic2Dx4[][4];
   int fixed[][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};

   ArrayCopy(dynamic, fixed);
   ArrayCopy(dynamic2Dx4, fixed);

   PRTS(ArrayReverse(fixed)); // true / status:0
   ArrayPrint(fixed);
   /*
       [,0][,1][,2][,3]
   [0,]   5   6   7   8
   [1,]   1   2   3   4
   */

   PRTS(ArrayReverse(dynamic, 4, 3)); // true / status:0
   ArrayPrint(dynamic);
   /*
   1 2 3 4 7 6 5 8
   */

   PRTS(ArrayReverse(dynamic, 0, 1)); // 无操作(count = 1)
   PRTS(ArrayReverse(dynamic2Dx4, 2, 1)); // false / status:5052, ERR_SMALL_ARRAY

   // 在最后一种情况下,值 start(2)超过了第一维的大小,所以会发生错误。
}

通过对这些函数(ArrayInsertArrayCopyArrayRemoveArrayReverse)的学习和实践,我们能够更加灵活地操作数组,满足在编程中对数组进行各种处理的需求,无论是插入、复制、删除元素还是反转元素顺序等操作,都有了清晰的认识和实际的操作方法。

希望以上对这些函数的详细介绍和示例能够帮助你更好地理解和运用数组相关的操作,在实际编程中发挥更大的作用。如果在使用过程中遇到任何问题,建议参考相关的文档和进一步的示例来解决问题。

数组的移动(交换)

MQL5 具备以高效利用资源的方式(无需进行实际的内存分配和数据复制)交换两个数组内容的能力。在其他一些编程语言里,类似的操作不仅对数组适用,对其他变量也支持,这种操作被称作移动。

bool ArraySwap(void &array1[], void &array2[])

此函数用于交换两个相同类型的动态数组的内容。它支持任意类型的数组。不过,该函数不适用于时间序列数组、指标缓冲区以及任何带有 const 修饰符的数组。

对于多维数组,除第一维外的所有维度的元素数量必须一致。

若操作成功,函数返回 true;若出现错误,则返回 false

该函数的主要用途是,当将数组传递给函数或从函数返回数组,且已知源数组不再需要时,通过避免数组的实际复制来提升程序的运行速度。实际上,交换操作几乎能瞬间完成,因为应用程序数据不会发生任何移动。相反,它交换的是存储在描述动态数组的服务结构中的数组元数据(这仅需 52 字节)。

假设存在一个用于通过特定算法处理数组的类。同一个数组可能会经历不同的操作,所以将其作为类的成员保存是合理的。但随之而来的问题是,如何将数组传递给对象呢?在 MQL5 中,方法(以及一般的函数)只允许通过引用传递数组。先不考虑那些包含数组并通过指针传递的各种包装类,唯一简单的解决方案似乎如下:例如,在类的构造函数中定义一个数组参数,然后使用 ArrayCopy 将其复制到内部数组。不过,使用 ArraySwap 会更高效。

c
template<typename T>
class Worker
{
   T array[];
   
public:
   Worker(T &source[])
   {
      // ArrayCopy(array, source); // 耗费内存和时间 
      ArraySwap(source, array);
   }
   ...
};

由于在交换之前 array 数组为空,交换操作完成后,作为源参数的数组将变为空数组,而 array 数组将填充输入数据,且几乎不会产生额外开销。

在类的对象成为数组的 “所有者” 之后,我们可以使用所需的算法对其进行修改。例如,通过一个特殊的 process 方法,该方法将所请求算法的代码作为参数。这些算法可以是排序、平滑处理、混合、添加噪声等等。不过,首先让我们尝试通过 ArrayReverse 函数进行简单的数组反转操作来验证这个想法(见文件 ArraySwapSimple.mq5)。

c
bool process(const int mode)
{
   if(ArraySize(array) == 0) return false;
   switch(mode)
   {
   case -4:
      // 示例:洗牌操作
      break;
   case -3:
      // 示例:取对数操作
      break;
   case -2:
      // 示例:添加噪声操作
      break;
   case -1:
      ArrayReverse(array);
      break;
   ...
   }
   return true;
}

可以通过两种方法访问处理结果:逐个元素访问(通过重载 [] 运算符),或者一次性获取整个数组(在相应的 get 方法中再次使用 ArraySwap,当然也可以提供一个通过 ArrayCopy 进行复制的方法)。

c
T operator[](int i)
{
   return array[i];
}

void get(T &result[])
{
   ArraySwap(array, result);
}

为了实现通用性,该类被设计为模板类。这将使它在未来能够适配任意结构的数组。目前,你可以验证一个简单的 double 类型数组的反转操作:

c
void OnStart()
{
   double data[];
   ArrayResize(data, 3);
   data[0] = 1;
   data[1] = 2;
   data[2] = 3;
   
   PRT(ArraySize(data));        // 3
   Worker<double> simple(data);
   PRT(ArraySize(data));        // 0
   simple.process(-1);  // 反转数组
   
   double res[];
   simple.get(res);
   ArrayPrint(res); // 3.00000 2.00000 1.00000
}

排序任务更为实际,对于结构数组,可能需要按任意字段进行排序。在下一节中,我们将详细研究 ArraySort 函数,它可以对任何内置类型的数组按升序进行排序,但不能对结构数组排序。在那里,我们将尝试弥补这一 “不足”,同时继续使用 ArraySwap

数组中的比较、排序和搜索

MQL5 应用程序编程接口(API)包含多个函数,这些函数可用于比较和排序数组,以及在数组中搜索最大值、最小值或任何特定值。

int ArrayCompare(const void &array1[], const void &array2[], int start1 = 0, int start2 = 0, int count = WHOLE_ARRAY)

该函数返回对两个内置类型数组或具有内置类型字段(不包括字符串)的结构数组进行比较的结果。不支持类对象数组。此外,不能比较包含动态数组、类对象或指针的结构数组。

默认情况下,会对整个数组进行比较,但如有需要,您可以指定数组的部分内容进行比较,这通过参数 start1(第一个数组中的起始位置)、start2(第二个数组中的起始位置)和 count 来实现。

数组可以是固定大小或动态大小的,也可以是多维数组。在比较过程中,多维数组被视为等效的一维数组(例如,对于二维数组,第二行的元素紧跟在第一行元素之后,第三行元素紧跟在第二行元素之后,依此类推)。因此,对于多维数组,参数 start1start2count 是通过元素编号指定的,而不是沿着第一维的索引。

通过使用不同的 start1start2 偏移量,您可以比较同一个数组的不同部分。

数组会逐个元素进行比较,直到发现第一个不匹配的元素,或者到达其中一个数组的末尾。两个元素(在两个数组中处于相同位置)之间的关系取决于类型:对于数字,使用 ><== 运算符;对于字符串,使用 StringCompare 函数。结构是按字节进行比较的,这相当于对每对元素执行以下代码:

c
uchar bytes1[], bytes2[];
StructToCharArray(array1[i], bytes1);
StructToCharArray(array2[i], bytes2);
int cmp = ArrayCompare(bytes1, bytes2);

根据第一个不同元素的比较结果,得出数组 array1array2 的整体比较结果。如果没有发现差异,并且数组长度相等,则认为这两个数组相同。如果长度不同,则较长的数组被视为更大。

如果 array1 “小于” array2,函数返回 -1;如果 array1 “大于” array2,函数返回 +1;如果它们 “相等”,函数返回 0

如果发生错误,结果为 -2

让我们看一下脚本 ArrayCompare.mq5 中的一些示例。

首先创建一个简单的结构,用于填充要比较的数组。

c
struct Dummy
{
   int x;
   int y;
   
   Dummy()
   {
      x = rand() / 10000;
      y = rand() / 5000;
   }
};

类字段用随机数填充(每次运行脚本时,都会得到新的值)。

OnStart 函数中,我们定义一个小型的结构数组,并将连续的元素相互比较(就像移动长度为 1 个元素的数组相邻片段一样)。

c
#define LIMIT 10
 
void OnStart()
{
   Dummy a1[LIMIT];
   ArrayPrint(a1);
   
   // 相邻元素的两两比较
   // -1: [i] < [i + 1]
   // +1: [i] > [i + 1]
   for(int i = 0; i < LIMIT - 1; ++i)
   {
      PRT(ArrayCompare(a1, a1, i, i + 1, 1));
   }
   ...

以下是其中一种数组情况的结果(为了便于分析,在数组内容的右侧直接添加了 “大于”(+1)/“小于”(-1)标志列):

       [x] [y]   // 结果
   [0]   0   3   // -1
   [1]   2   4   // +1
   [2]   2   3   // +1
   [3]   1   6   // +1
   [4]   0   6   // -1
   [5]   2   0   // +1
   [6]   0   4   // -1
   [7]   2   5   // +1
   [8]   0   5   // -1
   [9]   3   6

将数组的前半部分和后半部分相互比较,结果为 -1

c
 // 比较前半部分和后半部分
   PRT(ArrayCompare(a1, a1, 0, LIMIT / 2, LIMIT / 2)); // -1

接下来,我们将比较包含预定义数据的字符串数组。

c
   string s[] = {"abc", "456", "$"};
   string s0[][3] = {{"abc", "456", "$"}};
   string s1[][3] = {{"abc", "456", ""}};
   string s2[][3] = {{"abc", "456"}}; // 最后一个元素省略:它为空
   string s3[][2] = {{"abc", "456"}};
   string s4[][2] = {{"aBc", "456"}};
   
   PRT(ArrayCompare(s0, s));  // s0 == s,一维和二维数组包含相同的数据
   PRT(ArrayCompare(s0, s1)); // s0 > s1,因为 "$" > ""
   PRT(ArrayCompare(s1, s2)); // s1 > s2,因为 "" > 空
   PRT(ArrayCompare(s2, s3)); // s2 > s3,由于长度不同:[3] > [2]
   PRT(ArrayCompare(s3, s4)); // s3 < s4,因为 "abc" < "aBc"

最后,让我们检查数组片段之间的关系:

c
   PRT(ArrayCompare(s0, s1, 1, 1, 1)); // 第二个元素(索引为 1)相等 
   PRT(ArrayCompare(s1, s2, 0, 0, 2)); // 前两个元素相等

bool ArraySort(void &array[])

该函数对数值数组(可能包括多维数组)按第一维进行排序。排序顺序为升序。要按降序对数组进行排序,可以对结果数组应用 ArrayReverse 函数,或者以相反的顺序处理它。

该函数不支持字符串、结构或类的数组。

如果操作成功,函数返回 true;如果出现错误,则返回 false

如果为数组设置了 “时间序列(timeseries)” 属性,则其中的元素以相反的顺序进行索引(请参阅 “Array indexing direction as in timeseries” 部分的详细信息),这对排序顺序会产生 “外部” 的反转效果:当您直接处理这样的数组时,会得到降序的值。在物理层面上,数组始终按升序进行排序,并且也是按此方式存储的。

在脚本 ArraySort.mq5 中,生成一个 10×3 的二维数组,并使用 ArraySort 进行排序:

c
#define LIMIT 10
#define SUBLIMIT 3
   
void OnStart()
{
   // 生成随机数据
   int array[][SUBLIMIT];
   ArrayResize(array, LIMIT);
   for(int i = 0; i < LIMIT; ++i)
   {
      for(int j = 0; j < SUBLIMIT; ++j)
      {
         array[i][j] = rand();
      }
   }
   
   Print("Before sort");
   ArrayPrint(array);    // 源数组
   
   PRTS(ArraySort(array));
   
   Print("After sort");
   ArrayPrint(array);    // 有序数组
   ...
}

根据日志,第一列按升序排序(由于是随机生成,具体数字会有所不同):

Before sort
      [,0]  [,1]  [,2]
[0,]  8955  2836 20011
[1,]  2860  6153 25032
[2,] 16314  4036 20406
[3,] 30366 10462 19364
[4,] 27506  5527 21671
[5,]  4207  7649 28701
[6,]  4838   638 32392
[7,] 29158 18824 13536
[8,] 17869 23835 12323
[9,] 18079  1310 29114
ArraySort(array)=true / status:0
After sort
      [,0]  [,1]  [,2]
[0,]  2860  6153 25032
[1,]  4207  7649 28701
[2,]  4838   638 32392
[3,]  8955  2836 20011
[4,] 16314  4036 20406
[5,] 17869 23835 12323
[6,] 18079  1310 29114
[7,] 27506  5527 21671
[8,] 29158 18824 13536
[9,] 30366 10462 19364

后续列中的值与第一列中的 “主导” 值同步移动。换句话说,尽管只有第一列是排序标准,但整个行都进行了置换。

但是,如果您想按除第一列之外的其他列对二维数组进行排序呢?您可以为此编写一个特殊的算法。其中一种选择作为模板函数包含在文件 ArraySort.mq5 中:

c
template<typename T>
bool ArraySort(T &array[][], const int column)
{
   if(!ArrayIsDynamic(array)) return false;
   
   if(column == 0)
   {
      return ArraySort(array); // 标准函数 
   }
   
   const int n = ArrayRange(array, 0);
   const int m = ArrayRange(array, 1);
   
   T temp[][2];
   
   ArrayResize(temp, n);
   for(int i = 0; i < n; ++i)
   {
      temp[i][0] = array[i][column];
      temp[i][1] = i;
   }
   
   if(!ArraySort(temp)) return false;
   
   ArrayResize(array, n * 2);
   for(int i = n; i < n * 2; ++i)
   {
      ArrayCopy(array, array, i * m, (int)(temp[i - n][1] + 0.1) * m, m);
      /* 等效代码
      for(int j = 0; j < m; ++j)
      {
         array[i][j] = array[(int)(temp[i - n][1] + 0.1)][j];
      }
      */
   }
   
   return ArrayRemove(array, 0, n);
}

给定的函数仅适用于动态数组,因为数组的大小会加倍,以便在数组的后半部分组装中间结果,最后使用 ArrayRemove 删除前半部分(原始部分)。这就是为什么在 OnStart 函数中,原始测试数组是通过 ArrayResize 进行分配的。

我们鼓励您自行研究排序原理(或者再翻阅几页内容)。

对于具有大量维度的数组(例如 array[][][]),也应该实现类似的操作。

现在回想一下,在上一节中,我们提出了按任意字段对结构数组进行排序的问题。如我们所知,标准的 ArraySort 函数无法做到这一点。让我们尝试想出一个 “变通方法”。我们以前一节 ArraySwapSimple.mq5 文件中的类为基础。将其复制到 ArrayWorker.mq5 并添加所需的代码。

Worker::process 方法中,我们将提供对辅助排序方法 arrayStructSort 的调用,要排序的字段将通过编号指定(我们将在下面描述如何实现):

c
   ...
   bool process(const int mode)
   {
      ...
      switch(mode)
      {
      ...
      case -1:
         ArrayReverse(array);
         break;
      default: // 按字段编号 'mode' 进行排序
         arrayStructSort(mode);
         break;
      }
      return true;
   }
   
private:
   bool arrayStructSort(const int field)
   {
      ...
   }

现在很清楚为什么 process 方法中之前的所有模式(mode 参数的值)都是负数了:零和正数保留用于排序,并对应于 “列” 编号。

对结构数组进行排序的思路取自对二维数组的排序。我们只需要以某种方式将单个结构映射到一维数组(表示二维数组的一行)。为此,首先需要确定数组应该是什么类型。

由于 worker 类已经是一个模板类,我们将向模板添加另一个参数,以便可以灵活设置数组类型。

c
template<typename T, typename R>
class Worker
{
   T array[];
   ...

现在,让我们回到类型关联,它允许将不同类型的变量相互覆盖。因此,我们得到了以下巧妙的构造:

c
   union Overlay
   {
      T r;
      R d[sizeof(T) / sizeof(R)];
   };

在这个联合中,结构的类型与 R 类型的数组相结合,并且其大小由编译器根据两种类型 TR 的大小比例自动计算。

现在,在 arrayStructSort 方法内部,我们可以部分复制二维数组排序的代码。

c
   bool arrayStructSort(const int field)
   {
      const int n = ArraySize(array);
      
      R temp[][2];
      Overlay overlay;
      
      ArrayResize(temp, n);
      for(int i = 0; i < n; ++i)
      {
         overlay.r = array[i];
         temp[i][0] = overlay.d[field];
         temp[i][1] = i;
      }
      ...

我们准备了 temp[][2] 类型为 R 的数组,而不是带有原始结构的数组,将其扩展到 array 中的记录数量,并在循环中写入以下内容:在每行的第 0 个索引处写入结构中所需字段 field 的 “显示值”,在第 1 个索引处写入该元素的原始索引。

这种 “显示” 基于这样一个事实,即结构中的字段通常以某种方式对齐,因为它们使用标准类型。因此,通过正确选择 R 类型,有可能在 “覆盖” 中使数组元素中的字段完全或部分匹配。

例如,在标准结构 MqlRates 中,前 6 个字段的大小为 8 字节,因此可以正确映射到 doublelong 类型的数组(这些是 R 模板类型的候选者)。

c
struct MqlRates 
{ 
   datetime time; 
   double   open; 
   double   high; 
   double   low; 
   double   close; 
   long     tick_volume; 
   int      spread; 
   long     real_volume; 
};

对于最后两个字段,情况更为复杂。如果仍然可以使用 int 类型作为 R 来访问 spread 字段,那么 real_volume 字段的偏移量不是其自身大小的倍数(因为在它之前的 int 类型字段,即 4 字节)。这些是特定方法存在的问题。可以对其进行改进,或者发明另一种方法。

但让我们回到排序算法。在填充 temp 数组之后,可以使用常规的 ArraySort 函数对其进行排序,然后可以使用原始索引来形成具有正确结构顺序的新数组。

c
      ...
      if(!ArraySort(temp)) return false;
      T result[];
      
      ArrayResize(result, n);
      for(int i = 0; i < n; ++i)
      {
         result[i] = array[(int)(temp[i][1] + 0.1)];
      }
      
      return ArraySwap(result, array);
   }

在函数退出之前,我们再次使用 ArraySwap,以便以高效利用资源的方式,用在局部数组 result 中得到的新的有序内容替换对象内部数组 array 的内容。

让我们检查 worker 类的实际运行情况:在 OnStart 函数中,定义一个 MqlRates 结构数组,并向终端请求几千条记录。

c
#define LIMIT 5000
 
void OnStart()
{
  ```c
   MqlRates rates[];
   int n = CopyRates(_Symbol, _Period, 0, LIMIT, rates);
   ...

   // 使用 CopyRates 函数获取数据后,使用 Worker 类对数据进行处理
   Worker<MqlRates, double> worker(rates);

   // 按开盘价(open)字段对数据进行排序
   worker.process(offsetof(MqlRates, open) / sizeof(double));

   // 逐个元素打印排序后的开盘价
   for (int i = 0; i < n; ++i)
   {
      Print(worker[i].open);
   }

   // 或者获取整个排序后的数组
   MqlRates sortedRates[];
   worker.get(sortedRates);
   ArrayPrint(sortedRates);

   // 为了简化按不同字段进行排序的操作,使用辅助函数 sort
   sort(worker, offsetof(MqlRates, tick_volume) / sizeof(double), "Sorting by tick volume...");
   sort(worker, offsetof(MqlRates, time) / sizeof(double), "Sorting by time...");
}

// 以下是辅助函数 sort 的实现
void sort(Worker<MqlRates, double> &worker, const int offset, const string title)
{
   Print(title);
   worker.process(offset);
   Print("First struct");
   StructPrint(worker[0]);
   Print("Last struct");
   StructPrint(worker[worker.size() - 1]);
}

// StructPrint 函数用于打印单个结构,通过将结构放入大小为 1 的数组中实现
template<typename S>
void StructPrint(const S &s)
{
   S temp[1];
   temp[0] = s;
   ArrayPrint(temp);
}

// 执行脚本后,我们可以得到类似以下的结果(取决于终端设置,即执行的是哪个货币对/时间周期)
// 例如,对于 EURUSD,M15 时间周期的数据:
// Sorting by open price...
// First struct
//                  [time]  [open]  [high]   [low] [close] [tick_volume] [spread] [real_volume]
// [0] 2021.07.21 10:30:00 1.17557 1.17584 1.17519 1.17561          1073        0             0
// Last struct
//                  [time]  [open]  [high]   [low] [close] [tick_volume] [spread] [real_volume]
// [0] 2021.05.25 15:15:00 1.22641 1.22664 1.22592 1.22618           852        0             0
// Sorting by tick volume...
// First struct
//                  [time]  [open]  [high]   [low] [close] [tick_volume] [spread] [real_volume]
// [0] 2021.05.24 00:00:00 1.21776 1.21811 1.21764 1.21794            52       20             0
// Last struct
//                  [time]  [open]  [high]   [low] [close] [tick_volume] [spread] [real_volume]
// [0] 2021.06.16 21:30:00 1.20436 1.20489 1.20149 1.20154          4817        0             0
// Sorting by time...
// First struct
//                  [time]  [open]  [high]   [low] [close] [tick_volume] [spread] [real_volume]
// [0] 2021.05.14 16:15:00 1.21305 1.21411 1.21289 1.21333           888        0             0
// Last struct
//                  [time]  [open]  [high]   [low] [close] [tick_volume] [spread] [real_volume]
// [0] 2021.07.27 22:45:00 1.18197 1.18227 1.18191 1.18225           382        0             0

// 以上实现的排序方法可能是最快的方法之一,因为它使用了内置的 ArraySort 函数。
// 然而,如果结构字段对齐存在困难,或者对将结构“映射”到数组的方法存在疑虑,
// 迫使我们放弃这种方法(从而放弃 ArraySort 函数),那么还有经过验证的“自行实现”的方法可供选择。

// 有许多排序算法很容易适配到 MQL5 中。本书附带的文件 QuickSortStructT.mqh 中提供了一种快速排序的实现。
// 这是 QuickSortT.mqh 的改进版本,我们在“字符串比较”部分使用过它。
// 模板类 QuickSortStructT 中的 Compare 方法是纯虚函数,必须在派生类中重新定义,
// 以返回所需类型及其字段的比较运算符 '>' 的类似功能。
// 为了方便用户,在头文件中创建了一个宏:

#define SORT_STRUCT(T, A, F)                                           \
{                                                                    \
   class InternalSort : public QuickSortStructT<T>                   \
   {                                                                 \
      virtual bool Compare(const T &a, const T &b) override          \
      {                                                              \
         return a.##F > b.##F;                                       \
      }                                                              \
   } sort;                                                           \
   sort.QuickSort(A);                                                \
}

// 使用这个宏,要按给定字段对结构数组进行排序,只需编写一条指令。例如:

   MqlRates rates[];
   CopyRates(_Symbol, _Period, 0, 10000, rates);
   SORT_STRUCT(MqlRates, rates, high);

// 这里,MqlRates 类型的 rates 数组按最高价(high)字段进行排序。

// 以下是在数组中搜索特定值的函数
int ArrayBsearch(const type &array[], type value)

// 该函数在数值数组中搜索给定的值。支持所有内置数值类型的数组。
// 数组必须按第一维升序排序,否则结果将不正确。
// 函数返回匹配元素的索引(如果有多个匹配元素,则返回第一个匹配元素的索引),
// 或者返回最接近该值的元素的索引(如果没有精确匹配),
// 也就是说,返回的索引对应的元素值可能大于或小于要搜索的值。
// 如果要搜索的值小于第一个(最小值)元素,则返回 0。
// 如果要搜索的值大于最后一个(最大值)元素,则返回其索引。
// 索引取决于数组中元素的编号方向:正向(从开始到结束)或反向(从结束到开始)。
// 可以使用“Array indexing direction as in timeseries”部分中描述的函数来识别和更改编号方向。
// 如果发生错误,返回 -1。
// 对于多维数组,搜索仅限于第一维。

// 在脚本 ArraySearch.mq5 中可以找到使用 ArrayBsearch 函数的示例。

void OnStart()
{
   int array[] = {1, 5, 11, 17, 23, 23, 37};
     // 索引  0  1   2   3   4   5   6
   int data[][2] = {{1, 3}, {3, 2}, {5, 10}, {14, 10}, {21, 8}};
     // 索引     0       1       2         3         4
   int empty[];
   ...

   // 对三个预定义数组(其中一个为空)执行以下语句:
   PRTS(ArrayBsearch(array, -1)); // 0
   PRTS(ArrayBsearch(array, 11)); // 2
   PRTS(ArrayBsearch(array, 12)); // 2
   PRTS(ArrayBsearch(array, 15)); // 3
   PRTS(ArrayBsearch(array, 23)); // 4
   PRTS(ArrayBsearch(array, 50)); // 6
   
   PRTS(ArrayBsearch(data, 7));   // 2
   PRTS(ArrayBsearch(data, 9));   // 2
   PRTS(ArrayBsearch(data, 10));  // 3
   PRTS(ArrayBsearch(data, 11));  // 3
   PRTS(ArrayBsearch(data, 14));  // 3
   
   PRTS(ArrayBsearch(empty, 0));  // -1, 5053, ERR_ZEROSIZE_ARRAY
   ...

   // 进一步,在辅助函数 populateSortedArray 中,用随机值填充 numbers 数组,
   // 并使用 ArrayBsearch 不断维护数组的排序状态。

void populateSortedArray(const int limit)
{
   double numbers[];  // 要填充的数组
   double element[1]; // 要插入的新值
   
   ArrayResize(numbers, 0, limit); // 预先分配内存
   
   for(int i = 0; i < limit; ++i)
   {
      // 生成一个随机数
      element[0] = NormalizeDouble(rand() * 1.0 / 32767, 3);
      // 找到它在数组中的位置
      int cursor = ArrayBsearch(numbers, element[0]);
      if(cursor == -1)
      {
         if(_LastError == 5053) // 空数组
         {
            ArrayInsert(numbers, element, 0);
         }
         else break; // 错误
      }
      else
      if(numbers[cursor] > element[0]) // 在 'cursor' 位置插入
      {
         ArrayInsert(numbers, element, cursor);
      }
      else // (numbers[cursor] <= value) // 在 'cursor' 之后插入
      {
         ArrayInsert(numbers, element, cursor + 1);
      }
   }
   ArrayPrint(numbers, 3);
}

// 每个新值首先进入一个单元素数组 element,因为这样更容易使用 ArrayInsert 函数
// 将其插入到结果数组 numbers 中。
// ArrayBsearch 函数允许确定新值应该插入的位置。
// 函数的结果显示在日志中:

void OnStart()
{
   ...
   populateSortedArray(80);
   /*
    示例(由于随机化,每次运行结果会不同)
   [ 0] 0.050 0.065 0.071 0.106 0.119 0.131 0.145 0.148 0.154 0.159
        0.184 0.185 0.200 0.204 0.213 0.216 0.220 0.224 0.236 0.238
   [20] 0.244 0.259 0.267 0.274 0.282 0.293 0.313 0.334 0.346 0.366
        0.386 0.431 0.449 0.461 0.465 0.468 0.520 0.533 0.536 0.541
   [40] 0.597 0.600 0.607 0.612 0.613 0.617 0.621 0.623 0.631 0.634
        0.646 0.658 0.662 0.664 0.670 0.670 0.675 0.686 0.693 0.694
   [60] 0.725 0.739 0.759 0.762 0.768 0.783 0.791 0.791 0.791 0.799
        0.838 0.850 0.854 0.874 0.897 0.912 0.920 0.934 0.944 0.992
   */
}

// 以下两个函数用于在数组中搜索最大值和最小值
int ArrayMaximum(const type &array[], int start = 0, int count = WHOLE_ARRAY)
int ArrayMinimum(const type &array[], int start = 0, int count = WHOLE_ARRAY)

// ArrayMaximum 和 ArrayMinimum 函数分别在数值数组中搜索具有最大值和最小值的元素。
// 搜索的索引范围由 start 和 count 参数设置:使用默认值时,将搜索整个数组。
// 函数返回找到的元素的位置。
// 如果为数组设置了“序列(serial)”属性(“时间序列(timeseries)”),
// 则其中的元素按相反顺序进行索引,这会影响此函数的结果(请参阅示例)。
// 下一节将讨论用于处理“序列”属性的内置函数。
// 关于“序列”数组的更多详细信息将在关于时间序列和指标的章节中讨论。
// 对于多维数组,搜索在第一维上进行。
// 如果数组中有多个具有最大值或最小值的相同元素,函数将返回第一个这样的元素的索引。

// 在文件 ArrayMaxMin.mq5 中给出了使用这些函数的示例。

#define LIMIT 10
   
void OnStart()
{
   // 生成随机数据
   int array[];
   ArrayResize(array, LIMIT);
   for(int i = 0; i < LIMIT; ++i)
   {
      array[i] = rand();
   }
   
   ArrayPrint(array);
   // 默认情况下,新数组不是时间序列
   PRTS(ArrayMaximum(array));
   PRTS(ArrayMinimum(array));
   // 启用“序列”属性
   PRTS(ArraySetAsSeries(array, true));
   PRTS(ArrayMaximum(array));
   PRTS(ArrayMinimum(array));
}

// 脚本将记录类似以下的一组字符串(由于随机数据生成,每次运行都会不同):
// 例如:
// 22242 5909 21570 5850 18026 24740 10852 2631 24549 14635
// ArrayMaximum(array)=5 / status:0
// ArrayMinimum(array)=7 / status:0
// ArraySetAsSeries(array,true)=true / status:0
// ArrayMaximum(array)=4 / status:0
// ArrayMinimum(array)=2 / status:0

数组中的时间序列索引方向

由于交易应用的特殊性,MQL5为数组操作带来了额外的特性。其中之一是数组元素可以包含与时间点对应的数据。例如,包含金融工具报价、价格跳动点以及技术指标读数的数组。数据的时间顺序意味着新元素会不断添加到数组末尾,其索引也会随之增加。

不过,从交易的角度来看,从当前时刻向过去进行计数更为方便。这样一来,索引为 0 的元素始终包含最新、最实时的值,索引为 1 的元素则包含前一个值,依此类推。

MQL5 允许你随时选择和切换数组的索引方向。一个从当前时刻向过去编号的数组被称为时间序列数组。如果索引是从过去向当前递增,那它就是普通数组。在时间序列数组中,随着索引的增加,时间是递减的。而在普通数组中,时间则像现实生活中一样是递增的。

需要着重注意的是,一个数组不一定非要包含与时间相关的值才能切换其寻址顺序。只是这个特性在处理历史数据时最为常用,实际上它也正是为处理历史数据而出现的。

这个数组属性不会影响数据在内存中的布局,仅仅改变编号顺序。实际上,我们可以在 MQL5 中通过一个“从后往前”的循环来遍历数组,从而实现类似的功能。但 MQL5 提供了现成的函数,将这些繁琐的操作对应用程序员隐藏起来。

时间序列数组可以是 MQL 程序中定义的任意一维动态数组,也可以是从 MetaTrader 5 核心传递给 MQL 程序的外部数组,例如实用函数的参数。例如,MQL 程序中的一种特殊类型——指标,会在 OnCalculate 事件处理函数中接收包含当前图表价格数据的数组。我们将在本书的第五部分深入研究时间序列数组的应用特性。

在 MQL 程序中定义的数组默认不是时间序列数组。

下面我们来看看一组用于确定和更改数组“序列”属性以及其“归属”终端情况的函数。在介绍完这些函数后,会给出包含示例的通用 ArrayAsSeries.mq5 脚本。

bool ArrayIsSeries(const void &array[])

该函数用于返回指定数组是否为“真正的”时间序列数组的标志,也就是说,该数组是否由终端自身控制和提供。你无法更改数组的这一特性。此类数组在 MQL 程序中以“只读”模式可用。

在 MQL5 文档中,“时间序列(timeseries)”和“序列(series)”这两个术语既用于描述数组的反向索引,也用于表示数组“归属”终端(即终端为其分配内存并填充数据)这一事实。在本书中,我们会尽量避免这种歧义,将具有反向索引的数组称为“时间序列数组”,而将终端数组称为终端自身的数组。

你可以根据自己的需求更改终端的任何自定义数组的索引方式,将其切换为时间序列模式或者恢复为标准模式。这可以通过 ArraySetAsSeries 函数实现,该函数不仅适用于终端自身的数组,也适用于自定义动态数组(见下文)。

bool ArrayGetAsSeries(const void &array[])

此函数返回指定数组是否启用了时间序列索引模式的标志,即索引是否是从当前时刻向过去递增的。你可以使用 ArraySetAsSeries 函数来更改索引方向。

索引方向会影响 ArrayBsearchArrayMaximumArrayMinimum 函数返回的值(见“数组中的比较、排序和搜索”部分)。

bool ArraySetAsSeries(const void &array[], bool as_series)

该函数根据 as_series 参数设置数组的索引方向:true 表示反向索引顺序,false 表示元素的正常顺序。

如果成功设置属性,函数返回 true;如果出现错误,则返回 false

该函数支持任何类型的数组,但禁止对多维数组和固定大小的数组更改索引方向。

ArrayAsSeries.mq5 脚本定义了几个小数组,用于对上述函数进行实验。

c
#define LIMIT 10
 
template<typename T>
void indexArray(T &array[])
{
   for(int i = 0; i < ArraySize(array); ++i)
   {
      array[i] = (T)(i + 1);
   }
}
 
class Dummy
{
   int data[];
};
 
void OnStart()
{
   double array2D[][2];
   double fixed[LIMIT];
   double dynamic[];
   MqlRates rates[];
   Dummy dummies[];
   
   ArrayResize(dynamic, LIMIT); // 分配内存
   // 用数字 1, 2, 3, ... 填充几个数组
   indexArray(fixed);
   indexArray(dynamic);
   ...

这里有一个二维数组 array2D、一个固定大小数组和一个动态数组,它们都是 double 类型,此外还有结构数组和类对象数组。为了便于演示,固定大小数组和动态数组使用辅助函数 indexArray 填充了连续的整数。对于其他类型的数组,我们仅检查“序列”模式的适用性,因为通过已填充数组的示例,反向索引效果的原理就会清晰明了。

首先,要确保这些数组都不是终端自身的数组:

c
   PRTS(ArrayIsSeries(array2D)); // false
   PRTS(ArrayIsSeries(fixed));   // false
   PRTS(ArrayIsSeries(dynamic)); // false
   PRTS(ArrayIsSeries(rates));   // false

所有 ArrayIsSeries 调用都返回 false,因为这些数组都是在 MQL 程序中定义的。在指标的 OnCalculate 函数的参数数组中,我们会看到返回 true 的情况(在第五部分)。

接下来,检查数组索引的初始方向:

c
   PRTS(ArrayGetAsSeries(array2D)); // false,不可能为 true
   PRTS(ArrayGetAsSeries(fixed));   // false
   PRTS(ArrayGetAsSeries(dynamic)); // false
   PRTS(ArrayGetAsSeries(rates));   // false
   PRTS(ArrayGetAsSeries(dummies)); // false

同样,所有结果都是 false

将固定大小数组和动态数组输出到日志中,以查看元素的原始顺序:

c
   ArrayPrint(fixed, 1);
   ArrayPrint(dynamic, 1);
   /*
       1.0  2.0  3.0  4.0  5.0  6.0  7.0  8.0  9.0 10.0
       1.0  2.0  3.0  4.0  5.0  6.0  7.0  8.0  9.0 10.0
   */

现在尝试更改索引顺序:

c
   // 错误:不允许进行参数转换
   // PRTS(ArraySetAsSeries(array2D, true));
 
   // 警告:不能用于静态分配的数组
   PRTS(ArraySetAsSeries(fixed, true));   // false
 
   // 此后一切正常
   PRTS(ArraySetAsSeries(dynamic, true)); // true
   PRTS(ArraySetAsSeries(rates, true));   // true
   PRTS(ArraySetAsSeries(dummies, true)); // true

针对 array2D 数组的语句会导致编译错误,因此被注释掉了。

针对固定大小数组的语句会触发编译器警告,提示不能将其应用于常量大小的数组。在运行时,最后 3 条语句都返回成功(true)。接下来看看数组的属性发生了怎样的变化:

c
   // 属性检查:
   // 首先,检查它们是否为终端自身的数组
   PRTS(ArrayIsSeries(fixed));            // false
   PRTS(ArrayIsSeries(dynamic));          // false
   PRTS(ArrayIsSeries(rates));            // false
   PRTS(ArrayIsSeries(dummies));          // false
   
   // 其次,检查索引方向
   PRTS(ArrayGetAsSeries(fixed));         // false
   PRTS(ArrayGetAsSeries(dynamic));       // true
   PRTS(ArrayGetAsSeries(rates));         // true
   PRTS(ArrayGetAsSeries(dummies));       // true

正如预期的那样,这些数组并没有变成终端自身的数组。不过,四个数组中有三个将其索引更改为了时间序列模式,其中包括一个结构数组和一个对象数组。为了展示结果,再次将固定大小数组和动态数组输出到日志中:

c
   ArrayPrint(fixed, 1);    // 无变化 
   ArrayPrint(dynamic, 1);  // 反向顺序
   /*
       1.0  2.0  3.0  4.0  5.0  6.0  7.0  8.0  9.0 10.0
      10.0  9.0  8.0  7.0  6.0  5.0  4.0  3.0  2.0  1.0
   */

由于没有对常量大小的数组应用该模式,所以它保持不变。而动态数组现在以反向顺序显示。

如果将数组设置为反向索引模式,调整其大小,然后恢复之前的索引,那么添加的元素将插入到数组的开头。

综上所述,MQL5 提供的这些函数使得在处理数组索引方向时更加灵活方便,尤其是在处理与时间序列相关的数据时,能够更好地满足交易应用的需求。但在使用时需要注意数组类型的限制,避免对多维数组和固定大小数组进行不允许的操作。

对象和数组的清零

通常,变量和数组的初始化或填充不会带来问题。对于简单变量,我们可以在定义语句中使用 = 运算符进行初始化,或者在后续的任何时刻赋予其所需的值。

对于结构体,可以使用聚合初始化(请参考“定义结构体”部分):

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

但这只有在结构体中不存在动态数组和字符串时才可行。此外,聚合初始化语法不能用于再次清理结构体。相反,你必须逐个为每个字段赋值,或者在程序中预留一个空结构体实例,并将其复制到需要清理的实例中。

如果此时讨论的是结构体数组,由于辅助但必要的指令,源代码会迅速变得冗长。

对于数组,有 ArrayInitializeArrayFill 函数,但它们仅支持数值类型:无法使用它们来填充字符串数组或结构体数组。

在这种情况下,ZeroMemory 函数可能会派上用场。它并非万能的,因为其适用范围存在显著限制,但了解它还是很有好处的。

void ZeroMemory(void &entity)

该函数可以应用于各种不同的实体:简单类型或对象类型的变量,以及它们的数组(固定大小、动态或多维)。

变量会被赋予 0 值(对于数字)或其等效值(对于字符串和指针为 NULL)。

对于数组,其所有元素都会被设置为 0。不要忘记,元素可以是对象,并且这些对象本身又可能包含其他对象。换句话说,ZeroMemory 函数可以在一次调用中执行深度内存清理。

然而,对于有效的对象存在一些限制。你只能用 0 填充满足以下条件的结构体和类的对象:

  1. 仅包含公共字段(即不包含访问类型为 privateprotected 的数据)。
  2. 不包含带有 const 修饰符的字段。
  3. 不包含指针。

前两个限制是由编译器内置的:尝试将包含不满足上述要求字段的对象清零会导致错误(见下文)。

第三个限制是一个建议:对指针进行外部清零会使检查数据完整性变得困难,这很可能导致关联对象的丢失和内存泄漏。

严格来说,可清零对象中字段需为公共的这一要求违反了类对象固有的封装原则,因此 ZeroMemory 主要用于简单结构体的对象及其数组。

在脚本 ZeroMemory.mq5 中给出了使用 ZeroMemory 的示例。

使用结构体 Simple 展示聚合初始化列表存在的问题:

c
#define LIMIT 5
   
struct Simple
{
   MqlDateTime data[]; // 动态数组会禁用初始化列表,
   // string s; // 字符串字段也会禁止,
   // ClassType *ptr; // 指针同样如此
   Simple()
   {
      // 分配内存,其中将包含任意数据
      ArrayResize(data, LIMIT);
   }
};

OnStart 函数或全局上下文中,我们无法定义并立即将这样一个结构体的对象清零:

c
void OnStart()
{
   Simple simple = {}; // 错误:不能使用初始化列表进行初始化
   ...

编译器会抛出“不能使用初始化列表”的错误。这是针对像动态数组、字符串变量和指针这样的字段的特定错误。特别是,如果 data 数组是固定大小的,则不会出现错误。

因此,我们使用 ZeroMemory 来替代初始化列表:

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

初始的清零操作也可以在结构体的构造函数中完成,但在外部进行后续的清理操作会更方便(或者为此提供一个使用相同 ZeroMemory 函数的方法)。

Base 中定义了以下类:

c
class Base
{
public: // 对于 ZeroMemory 来说,public 是必需的
   // 任何字段带有 const 修饰符都会在调用 ZeroMemory 时导致编译错误:
   // “对于带有保护成员或继承的对象不允许”
   /* const */ int x;
   Simple t;   // 使用嵌套结构体:它也会被清零
   Base()
   {
      x = rand();
   }
   virtual void print() const
   {
      PrintFormat("%d %d", &this, x);
      ArrayPrint(t.data);
   }
};

由于该类会在可使用 ZeroMemory 清零的对象数组中使用,我们不得不为其字段编写 public 访问部分(原则上,这对于类来说并不常见,这样做是为了说明 ZeroMemory 所施加的要求)。另外,请注意字段不能带有 const 修饰符。否则,我们会得到一个编译错误,错误信息很遗憾地与实际问题不太相符:“对于带有保护成员或继承的对象禁止”。

类的构造函数用一个随机数填充字段 x,以便之后能够清楚地看到 ZeroMemory 函数对其进行的清理操作。print 方法会显示所有字段的内容以供分析,包括唯一的对象编号(描述符)&this

MQL5 并不阻止将 ZeroMemory 应用于指针变量:

c
   Base *base = new Base();
   ZeroMemory(base); // 会将指针设置为 NULL,但对象会保留

然而,不应该这样做,因为该函数只会重置 base 变量本身,如果它引用了一个对象,这个对象将仍然“挂”在内存中,由于指针的丢失,程序无法访问该对象。

只有在使用 delete 运算符释放指针实例之后,才可以将指针清零。此外,像重置其他任何简单变量(非复合变量)一样,使用赋值运算符来重置上述示例中的单独指针会更容易。对于复合对象和数组,使用 ZeroMemory 才有意义。

该函数允许处理类层次结构的对象。例如,我们可以描述从 Base 派生的 Dummy 类的派生类:

c
class Dummy : public Base
{
public:
   double data[]; // 也可以是多维的:ZeroMemory 会起作用
   string s;
   Base *pointer; // 公共指针(有风险)
   
public:
   Dummy()
   {
      ArrayResize(data, LIMIT);
      
      // 由于后续会对对象应用 ZeroMemory
      // 我们将丢失 'pointer'
      // 并且在脚本结束时会收到关于未删除的 Base 类型对象的警告
      pointer = new Base();
   }
   
   ~Dummy()
   {
      // 由于使用了 ZeroMemory,这个指针将丢失
      // 并且不会被释放
      if(CheckPointer(pointer) != POINTER_INVALID) delete pointer;
   }
   
   virtual void print() const override
   {
      Base::print();
      ArrayPrint(data);
      Print(pointer);
      if(CheckPointer(pointer) != POINTER_INVALID) pointer.print();
   }
};

它包含 double 类型的动态数组、字符串和 Base 类型的指针字段(这与该类派生自的类型相同,但在这里仅用于演示指针问题,以免描述另一个虚拟类)。当 ZeroMemory 函数将 Dummy 对象清零时,pointer 指向的对象会丢失,并且无法在析构函数中释放。结果,这会导致在脚本终止后,剩余对象出现内存泄漏警告。

OnStart 中使用 ZeroMemory 来清理 Dummy 对象数组:

c
void OnStart()
{
   ...
   Print("Initial state");
   Dummy array[];
   ArrayResize(array, LIMIT);
   for(int i = 0; i < LIMIT; ++i)
   {
      array[i].print();
   }
   ZeroMemory(array);
   Print("ZeroMemory done");
   for(int i = 0; i < 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 如何解决字符串数组的初始化问题。ArrayInitializeArrayFill 函数不适用于字符串。

c
   string text[LIMIT] = {};
   // 一个算法填充并使用 'text'
   // ...
   // 然后需要重新使用该数组
   // 调用函数会产生错误:
   // ArrayInitialize(text, NULL);
   //      `-> 没有一个重载可以应用于该函数调用
   // ArrayFill(text, 0, 10, NULL);
   //      `->  'string' 类型不能用于 ArrayFill 函数
   ZeroMemory(text);               // 可行

在注释掉的指令中,编译器会生成错误,指出这些函数不支持 string 类型。

解决这个问题的方法就是使用 ZeroMemory 函数。