Skip to content

数组

数组是一种用于基于集群存储和处理任意类型数据的工具。实际上,几乎所有编程语言都支持数组。在 MQL5 中,数组尤为重要,因为它提供了一种方便的方式来组织与交易任务相关的串行数据。报价、指标读数、包含订单和交易的账户交易历史记录以及新闻,这些都是串行数据的例子,即随时间变化的值的序列。

数组可以被看作是一种容器变量:它可以包含预定义数量的相同类型的值,这些值通过它们的名称和索引(位置编号)来识别。

在本节中,我们将以内置数据类型为例,探讨描述数组和调用数组的通用语法。在本书接下来的部分中,随着了解如何通过面向对象技术扩展类型系统,我们将把数组与这些技术结合使用,以获得新的功能。

数组的特性

在阐述 MQL5 中声明数组的语法细节以及使用数组的实践之前,让我们先了解一些构建数组的基本概念。

数组的核心特性是维度数量。在一维数组中,其元素一个接一个地排列,就像一排士兵,只需一个数字(索引)就足以引用它们。给定历史深度下某一金融工具的逐根 K 线的开盘价就可以存储在这样的一维数组中。

在二维数组中,其元素在两个逻辑上相互垂直的方向上展开,形成一种类似正方形(一般情况下也可能是长方形)的结构,每个元素需要两个索引,即每个维度一个索引。这样的数组可用于存储每根历史 K 线的价格四要素(开盘价、最高价、最低价和收盘价)。K 线的编号对应第一维度,而第二维度使用 0 到 3 的数字,分别表示一种价格类型。

三维数组相当于一个具有三个轴的立方体(或者从更严格的几何角度来说,是直角平行六面体)。继续以逐根 K 线价格的数组为例,我们可以为其添加第三维度,用于遍历“市场报价”中的金融工具。

对于每个维度,数组都有一定的长度(大小),它设定了可能的索引范围。如果要加载 1000 根 K 线和 10 种金融工具的数据,那么我们得到的数组第一维度大小为 1000 个元素,第二维度为 4 个元素(OHLC,即开盘价、最高价、最低价和收盘价),第三维度为 10 个元素。

所有维度大小的乘积就是数组元素的总数;在我们的例子中,总数为 40000。在 MQL5 中,这个总数不能超过 2147483647(int 类型的最大值)。

对于四维数组,我们很难想象出一个具体的实体形状,因为我们生活在三维世界中。然而,MQL5 允许创建最多四维的数组。

需要注意的是,你总是可以使用一维数组来替代任意维度(包括超过 4 维)的多维数组。这只是将多个索引重新计算并排列成一个连续索引的问题。例如,如果一个二维数组有 10 列(第一维度,X 轴)和 5 行(第二维度,Y 轴),它可以转换为一个具有相同元素数量(即 50 个元素)的一维数组。在这种情况下,元素索引可以通过以下公式得到:

索引 = Y * N + X

这里,N 是第一维度的元素数量,在我们的例子中是 10,也就是每行的大小;Y 是行号(0 到 4);X 是该行中的列号(0 到 9)。

各维度的大小是数组区别于普通变量的另一个特性。因此,在描述数组时,除了数组名称和数据类型之外,还必须以某种方式指定维度数量和每个维度的大小(见下一节)。

你应该区分变量(数组元素)的字节大小和数组的元素数量大小。从理论上讲,数组在内存中占用的总大小应该是单个元素的大小(取决于数据类型)与元素数量的乘积。然而,这个公式在实践中并不总是适用。特别是,由于字符串的长度可能不同,很难评估字符串数组所占用的内存量。

根据内存分配方式,数组可以分为动态数组和固定大小数组。

固定大小数组在代码中描述时,所有维度的大小都是确定的。之后无法调整其大小。然而,在实际任务中,经常会遇到要处理的数据量是不确定的情况,因此希望在算法运行过程中调整数组的大小。动态数组就是为此目的而存在的。正如我们接下来会看到的,描述动态数组时不需要指定第一维度的大小,然后可以使用 MQL5 的特殊 API 函数对其进行“扩展”或“压缩”。

MQL5 文档中使用了含义模糊的术语,将固定大小数组称为“静态数组”。“静态”(static)修饰符也可以应用于数组,并且使用了相同的概念。如果声明一个动态数组带有 static 修饰符,那么从内存分配的角度来看它是非静态的(动态的),而从 static 修饰符的角度来看它又是静态的。为了避免歧义,本书中的“静态”特性仅指声明属性。

除了动态数组和固定大小数组之外,MQL5 中还有用于存储报价和技术指标缓冲区的特殊数组。这些数组被称为时间序列数组,因为它们的索引与时间相关。实际上,这些数组是一维且动态的。然而,与其他动态数组不同的是,终端会为它们自动分配内存。我们将在处理时间序列和指标的章节中讨论这些数组。

数组的描述

数组的描述继承了变量描述的一些特点。首先,我们需要注意的是,根据声明的位置,数组可以是全局的,也可以是局部的。与变量类似,在描述数组时也可以使用 conststatic 修饰符。对于一维固定大小数组,其声明语法如下:

c
类型 数组标识符[大小];

这里,“类型”和“数组标识符”分别表示元素的类型名称和数组的标识符,而方括号中的“大小”是一个定义大小的整型常量。

对于多维数组,必须根据维度数量指定多个大小:

c
类型 二维数组标识符[大小1][大小2];
类型 三维数组标识符[大小1][大小2][大小3];
类型 四维数组标识符[大小1][大小2][大小3][大小4];

动态数组的描述方式类似,只是在第一对方括号中留空(在使用这样的数组之前,必须使用 ArrayResize 函数为其分配所需的内存量,详见关于动态数组的部分):

c
类型 一维动态数组标识符[];
类型 二维动态数组标识符[][大小2];
类型 三维动态数组标识符[][大小2][大小3];
类型 四维动态数组标识符[][大小2][大小3][大小4];

对于固定大小数组,允许进行初始化:在等号后面为元素指定初始值,以逗号分隔的列表形式给出,整个列表用花括号括起来。例如:

c
int array1D[3] = {10, 20, 30};

这里,一个大小为 3 的整型数组取值为 10、20 和 30。

使用初始化列表时,无需在方括号中指定数组的大小(对于第一维度)。编译器会根据列表的长度自动评估大小。例如:

c
int array1D[] = {10, 20, 30};

初始值既可以是常量,也可以是常量表达式,即编译器在编译期间可以计算的公式。例如,下面的数组填充了一分钟、一小时、一天和一周的秒数(以公式形式表示比 86400 或 604800 更具说明性):

c
int seconds[] = {60, 60 * 60, 60 * 60 * 24, 60 * 60 * 24 * 7};

在代码开头,这样的值通常被定义为预处理器宏,然后在文本中需要的地方插入该宏的名称。这一选项在与预处理器相关的部分进行描述。

初始化元素的数量不能超过数组的大小。否则,编译器会给出错误信息“初始化项过多”。如果值的数量小于数组的大小,剩余的元素将被初始化为零。因此,有一种简洁的表示法可以将整个数组初始化为零:

c
int array2D[2][3] = {0};

或者只使用空花括号:

c
int array2D[2][3] = {};

这无论对于多少维度都适用。

要初始化多维数组,列表必须嵌套。例如:

c
int array2D[3][2] = {{1, 2}, {3, 4}, {5, 6}};

这里,数组的第一维度大小为 3;因此,在外部花括号内用两个逗号分隔出 3 个元素。然而,由于数组是二维的,它的每个元素又是一个数组,每个数组的大小为 2。这就是为什么每个元素都表示为花括号内的一个列表,每个列表包含 2 个值。

假设我们需要一个转置后的数组(第一维度大小为 2,第二维度大小为 3),那么它的初始化将有所不同:

c
int array2D[2][3] = {{1, 3, 5}, {2, 4, 6}};

如果需要,我们可以在初始化列表中跳过一个或多个值,用逗号标记它们的位置。所有跳过的元素也将被初始化为零。

c
int array1D[3] = {, , 30};

这里,前两个元素将等于 0。

语言语法允许在最后一个元素后面放置一个逗号:

c
string messages[] =
{
  "undefined",
  "success",
  "error",
};

这简化了添加新元素的操作,特别是对于多行输入。具体来说,如果我们在字符串数组中忘记在新添加的元素前输入逗号,那么旧字符串和新字符串将合并为一个元素(具有相同的索引),而不会出现新元素。此外,一些数组可能会自动生成(由另一个程序或宏生成)。因此,所有元素统一的格式是很自然的。

“堆”和“栈”

对于可能很大的数组,区分其在内存中的全局和局部位置很重要。

全局变量和数组的内存是在“堆”中分配的,即程序可用的空闲内存。实际上,除了计算机和操作系统的物理特性外,这种内存没有什么限制。“堆”这个名称的由来是因为程序总是会分配或释放不同大小的内存区域,这导致空闲区域随机地分散在整个内存块中。

局部变量和数组位于“栈”中,即预先为程序分配的有限内存区域,特别是用于局部元素。“栈”这个名称源于这样一个事实:在算法执行期间,会发生函数的嵌套调用,这些调用根据“堆积”原则积累它们的内部数据:例如,OnStart 由终端调用,你的应用代码中的一个函数从 OnStart 中被调用,然后你的另一个函数又从前一个函数中被调用,等等。同时,当进入每个函数时,会创建其局部变量,当调用嵌套函数时,这些局部变量会继续存在。嵌套函数也会创建局部变量,这些变量会在前面的变量之上“堆叠”在栈中。结果,栈通常包含了从启动到当前代码行路径上所有已激活函数的一些局部数据层。只有当栈顶的函数完成时,其局部数据才会从栈中移除。一般来说,栈是一种按照先进后出(FILO/LIFO)原则工作的存储结构。

由于栈的大小是有限的,建议只在栈中创建局部变量。然而,数组可能会非常大,很快就会耗尽整个栈空间。同时,程序执行会以错误结束。因此,我们应该将数组描述为全局的 static(静态)数组,或者为它们动态分配内存(这也是从堆中进行的)。

使用数组

通过类似的语法,在方括号中指定所需的索引,可向数组元素写入值或从数组元素读取值。要将值存入元素,需使用赋值运算符 =。例如,要替换一维数组中第 0 个元素的值:

cpp
array1D[0] = 11;

索引从 0 开始。最后一个元素的索引等于元素数量减 1。当然,既可以使用常量作为索引,也可以使用能转换为整数类型的其他表达式(有关表达式的更多细节,请参阅后续章节),例如整数变量、函数调用或包含整数的另一个数组的元素(间接寻址)。

cpp
int index;
// ... 
// index = ... // 以某种方式赋值索引
// ...
array1D[index] = 11;

对于多维数组,必须为所有维度指定索引。

cpp
array2D[index1][index2] = 12;

作为索引,允许的整数类型不包括 longulong。若尝试将“长整数”的值用作索引,它会被隐式转换为 int,因此编译器会给出“类型转换可能导致数据丢失”的警告。

对数组元素的读取访问遵循相同原则。例如,可按如下方式将数组元素打印到日志中:

cpp
Print(array2D[1][2]);

在脚本 GoodTimes 中,我们已看到在 Greeting 函数内对包含问候语字符串的局部静态数组 messages 的定义,以及在返回语句中对其元素的使用。

cpp
string Greeting() 
{
  static int counter = 0;
  static const string messages[3] = // 定义
  {
    "Good morning", "Good day", "Good evening" // 初始化
  };
  return messages[counter++ % 3];   // 使用
}

执行 return 语句时,我们读取由表达式 counter++ % 3 定义索引的元素。取模运算 % 3 确保每次增加 1 的 counter 被限制在正确的索引值范围内:0、1 或 2。若没有取模运算,从该函数第 4 次调用开始,请求元素的索引就会超出数组大小。在这种情况下,会出现程序运行时错误(“数组越界”),并且脚本会从图表中卸载。

MQL5 API 包含许多用于数组操作的通用函数:动态数组的内存分配、填充、复制、排序和搜索等操作,都在“数组操作”部分介绍。不过,现在我们先介绍其中一个函数:ArrayPrint 可将数组元素以方便的格式(考虑维度)打印到日志中。

脚本 Arrays.mq5 展示了一些数组定义的示例,结果会打印到日志中。在学习循环和表达式之后,我们再讨论对数组元素的操作。

cpp
void OnStart()
{
  char array[100];      // 未初始化
  int array2D[3][2] =
  {
    {1, 2},             // 示例格式
    {3, 4},
    {5, 6}
  };
  int array2Dt[2][3] =
  {
    {1, 3, 5},
    {2, 4, 6}
  };
  ENUM_APPLIED_PRICE prices[] =
  {
    PRICE_OPEN, PRICE_HIGH, PRICE_LOW, PRICE_CLOSE
  };
  // double d[5] = {1, 2, 3, 4, 5, 6}; // 错误:初始值数量超过数组大小
  ArrayPrint(array);    // 打印随机“垃圾”值
  ArrayPrint(array2D);  // 在日志中显示二维数组
  ArrayPrint(array2Dt); // 以“转置”形式显示相同的二维数据
  ArrayPrint(prices);   // 了解价格枚举元素的值
}

以下是日志记录的一种示例:

[ 0]   0   0   0   0   0   0   0   0   0   0   0   0 -87 105  82 119   0
       0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0
[34]   0   0   0 -32  -3  -1  -1   7   0   0   2   0   0   0   0   0   0
       0   2   0   0   0   0   0   0   0 -96 104  82 119   0   0   0   0
[68]   0   0   3   0   0   0   0   0  -1  -1  -1  -1   0   0   0   0 100
      48   0   0   0   0   0   0   0   0   0   0   0   0   0   0
    [,0][,1]
[0,]   1   2
[1,]   3   4
[2,]   5   6
    [,0][,1][,2]
[0,]   1   3   5
[1,]   2   4   6
2 3 4 1

名为 array 的数组未进行初始化,因此为其分配的内存可能包含随机值。每次运行脚本时,这些值都会改变。建议始终对局部数组进行初始化,以防万一。

数组 array2Darray2Dt 以矩阵形式清晰地打印到日志中。这与我们在源代码中对初始化列表的格式化方式并无关联。

prices 数组的类型是内置枚举 ENUM_APPLIED_PRICE。通常,数组可以是任何类型,包括结构体、函数指针等,这些我们后续会详细介绍。由于枚举基于 int 类型,所以值以数字形式显示,而非元素名称(要获取枚举特定元素的名称,可使用 EnumToString 函数,但 ArrayPrint 不支持这种模式)。

包含 d 数组定义的代码行存在错误:初始值的数量超过了数组的大小。