MQL5 编程基础:数组

Dmitry Fedoseev | 21 三月, 2014

简介

数组连同变量及函数是几乎所有编程语言的组成部分。很多编程新手往往对数组“心存惧意”。听上去不可思议,但这是事实!我可以向你们保证,它们没有什么好可怕的。事实上,数组和普通的变量类似。不考虑符号特性的细节,语句的编写没有太大区别,无论是使用简单变量:

Variable0=1;
Variable1=2;

Variable2=Variable0+Variable1;

或使用数组:

double Variable[3];

Variable[0]=1;
Variable[1]=2;

Variable[2]=Variable[0]+Variable[1];

如您所见,区别并不是很大,除了我们在使用数组时变量的名称包含括号。还有一个更值得注意的区别 - 在声明变量时,您需要指定每个变量的名称,而在声明数组时,您只需要写下它的名称一次并在括号中指定变量的数量(数组元素数量)。在处理大量的实际编程任务挑战时,使用数组相比使用变量的优越性变得更加明显。

数组看上去有些复杂的原因是不是和使用 "[" 和 "]" 有一定的关系?这些符号很少在编程中的数组以外的任何场合使用,因此读者可能不记得它们在键盘上的位置并感到不适。而事实上,您可以轻松记住它们的位置 - 这两个键呈逻辑顺序位于 "Enter" 键的旁边:左括号后面是右括号。


数组的定义和基本属性

因此,数组是具有相同名称的编号的变量集。数组的基本属性包括数组的名称、变量类型(int、double 等)和数组大小。数组元素的索引从 0 开始。说到数组元素,使用“索引”总比“编号”好,以表明我们从 0 开始计数数组元素(编号通常从 1 开始)。以此方式将元素编入索引,最后一个元素的索引比数组元素的数量小 1。

如果数组声明如下:

double Variable[3];

它具有下列元素:Variable[0]、Variable[1] 和 Variable[2]。

从表面上看,这种元素数量和最后一个元素的索引的不一致可能看上去不太方便。事实上,相比数组元素的索引从 1 开始或数组大小通过其最后一个元素的索引而非数组中的实际元素数量决定的编程语言,这带来了显著的优势。

为了在 MQL5 中确定数组大小,我们使用 ArraySize() 函数:

double Variable[3];

int Size=ArraySize(Variable);

在执行代码后,Size 变量的值将等于 3。


静态数组和动态数组

数组可以是静态和动态的。如果数组的大小在其声明时指定,则数组是静态的:

double Variable[3];

静态数组的大小无法在程序中更改。在声明数组时,其大小可被直接指定为一个数字(如上例中所示)或使用预定义的常数

#define SIZE 3

double Variable[SIZE];

大小未在声明中指定的数组是动态数组:

double Variable[];

在您可以使用这种数组前,您需要设置其大小。大小通过 ArrayResize() 函数设置:

ArrayResize(Variable,3);

动态数组的大小可以在程序执行期间按需要多次更改,这是动态数组和静态数组之间的基本区别。

如果您需要完全释放数组,使用 ArrayFree() 函数:

ArrayFree(Variable);

在执行此函数时,数组大小设为 0。此函数起到的作用与下述操作相似:

ArrayResize(Variable,0);

释放数组在以下情形中有用:后续程序操作中不再需要数组时(这减少了程序使用的内存),或在函数执行开始时(如果数组用于数据收集)。

ArrayIsDynamic() 函数允许您确定任何指定数组是静态还是动态:

bool dynamicArray=ArrayIsDynamic(Variable);

如果数组是动态的,dynamicArray 变量将包含 true 值;静态则包含 false 值。


数组初始化

有时候需要在数组声明后立即填充值。假设您希望创建同一类型的多个按钮并将它们排成一行,其中每个按钮具有自己的文本。这正是数组的巨大优势。没必要为每个按钮复制代码(可能有好几十个),也无需重复调用同一函数。您可以通过在一个循环中迭代数组来创建必要数量的按钮,仅编写一次函数调用代码。

我们简单地声明一个数组并立即为其元素赋值:

string Variable[] = {"Button 1", "Button 2", "Button 3"};

通过这种方式声明,数组仍然是静态数组,尽管其大小并未指定。这是因为其元素的数量通过值列表(在花括号中)定义。

如果您指定数组元素的数量将不会有任何错误:

string Variable[3] = {"Button 1", "Button 2", "Button 3"};

但最好不要这样做 - 在对程序的进一步改进中,您可能需要更改数组值列表并使用更多或更少的元素数量。要在使用数组的代码部分确定数组的大小,建议使用 ArraySize() 函数而不是具体数值。此方法允许您只需修改值列表而不用触碰主代码。更为恰当的做法是为数组大小声明一个变量,并在初始化程序时将 ArraySize() 函数获得的值分配给该变量。

如果静态数组无法通过值列表初始化,最好使用一个常量指定数组大小。一般而言,我们遵循减少代码量的原则,这些代码在程序需要进一步改进时需要被修改。如果您需要使用相同的值填充所有数组元素,使用 ArrayInitialize() 函数:

ArrayInitialize(Variable,1);

在执行上述代码后,所有 Var 数组元素将具有值 1。如果只需要将相同的值分配给数组的部分元素,我们使用 ArrayFill() 函数:

double Variable[4];

ArrayFill(Variable,0,2,1);
ArrayFill(Variable,2,2,2);

在执行这些代码后,元素 0 和元素 1 将具有值 1,同时元素 2 和元素 3 将具有值 2。


数组迭代循环

通常,我们使用 for 循环来处理数组。如果使用其大小提前已知的静态数组,取决于具体的任务,我们正向或反向迭代数组:

//--- forwards
for(int i=0; i<SIZE; i++){ 
  // some manipulations on the Variable[i] element
}

//--- backwards
for(int i=SIZE-1; i>=0; i--){
  // some manipulations on the Variable[i] element
}

如果数组是动态的,您应该在循环前面为数组大小声明一个变量,获取数组大小并执行循环:

int Size=ArraySize(Var);

for(int i=0; i<Size; i++){
  // some manipulations on the Variable[i] element
}

如果在检查 for 循环的状况时调用 ArraySize() 函数而不是使用数组大小的变量,由于该函数在每次循环迭代时都会被调用,循环时间将显著延长。因此,调用函数比调用变量要花费更多的时间:

for(int i=0; i<ArraySize(Variable); i++){
   // some manipulations on the Variable[i] element
}
不建议使用上述代码。

如果程序算法允许反向循环迭代,可以不需要数组大小变量:

for(int i=ArraySize(Variable)-1; i>=0; i--){
  // some manipulations on the Variable[i] element
}

在这种情形下,ArraySize() 函数将只在循环开始时调用一次,循环运行将会加快。


多维数组

到目前为止,我们只讨论了一维数组。它们可以表现为下面的形式:

一维数组

数组可以是多维的。一维数组的每个索引只包含一个值,而多维数组的每个索引具有多个值。多维数组的声明如下:

double Variable[10][3];

这表示该数组的第一个维度有 10 个元素,第二个维度有 3 个元素。可以对其描绘如下:

多维数组

为使其更容易理解,可以将二维数组描绘为一个平面。第一个维度的大小决定长度,第二个维度的大小决定宽度,而元素的值定义平面上指定点的参数,例如海拔高度。

数组也可以是三维的:

double Variable[10][10][10];

此数组可以用一个立方体或平行四边形表示:第一个维度决定长度,第二个维度决定宽度,第三个维度决定高度,而元素的值定义空间中指定点的参数。

MQL5 中允许的数组维度最大为 4。

仅多维数组的第一个维度可以是静态或动态,其余所有维度均为静态。因此,ArrayResize() 函数只允许您更改第一个维度的大小。其他维度的大小必须在声明数组时指定:

double Variable[][3][3];

在使用 ArraySize() 函数决定多维数组的大小时,我们应牢记:在使用 ArrayResize() 函数更改数组大小时,函数的第二个参数是数组第一个维度的大小。但是,ArraySize() 函数返回元素总数量而不是第一个维度的大小:

double Variable[][3][3]; 

ArrayResize(Variable,3); 
int Size = ArraySize(Variable);

在上述代码执行后,Size 变量将等于 27。如果您需要获得第一个维度的大小,在循环中迭代多维数组时记住这个特性。

double Variable[][3][3];
 
ArrayResize(Variable,3); 

int Size=ArraySize(Variable)/9; // Determine the size of the first dimension

for(int i=0; i<Size; i++) {
   for(int j=0; j<3; j++) {
      for(int k=0; k<3; k++) {
            //  some manipulations on the Var[i][j][k] element;
      }   
   }   
}

如上文中提到的,最好遵循减少代码量的原则,这些代码在程序需要进一步改进时需要被修改。在上面的代码示例中,我们使用数字 9,但这同样是可以计算的。为此,我们可以使用 ArrayRange() 函数,以返回包含在指定数组维度中的元素数量。如果数组维度数已知,我们可以执行一个简单的计算:

int Elements=ArrayRange(Variable,1)*ArrayRange(Variable,2);
int Size=ArraySize(Variable)/Elements;

我们可以使其更加通用:

int Elements=1; // One element for a one-dimensional array
int n=1; // Start with the second dimension (dimensions are numbered from zero)

while(ArrayRange(Variable,n) > 0){ // Until there are elements in the dimension
   Elements*=ArrayRange(Variable,n); // Multiplication of the number of elements
   n++; // Increase in the dimension's number
}

从这一点上,您可能会觉得为这种计算创建一个函数会比较好。遗憾的是,这是不可能的,因为随机数组无法传递至函数。在声明函数的自变量时,除了第一个维度,您需要明确指定所有数组维度的元素数量,从而令这样一个函数变得毫无意义。那些计算在程序初始化过程中会更容易和更好地完成。当声明数组时,使用常量决定维度的大小是较为明智的:

#define SIZE1 3
#define SIZE2 3
#define TOTAL SIZE1*SIZE2 

使用值列表初始化多维数组与一维数组的初始化类似。但由于多维数组由几个数组组成,必须使用花括号将这些数组一一隔开。

假设我们有如下数组:

double Variable[3][3];

此数组由三个数组组成,每个数组具有三个元素:

double Variable[][3]={{1, 2, 3},{ 4, 5, 6},{7, 8, 9}};

三维数组以同样的方式进行处理。代码可以分解为多行,以帮助理解数组结构:

double Variable[][3][3]={
   {
      {1, 2, 3},
      {4, 5, 6},
      {7, 8, 9}
   },
   {
      {10, 20, 30},
      {40, 50, 60},
      {70, 80, 90}
   },
   {
      {100, 200, 300},
      {400, 500, 600},
      {700, 800, 900}
   }
};

使用 ArrayInitialize() 函数的多维数组的初始化与一维数组的初始化以同样的方式执行:

ArrayInitialize(Variable,1);

在执行代码后,所有数组元素将具有值 1。这对于 ArrayFill() 函数也是一样的:

double var[3][3][3];

ArrayFill(Variable,0,9,1);
ArrayFill(Variable,9,9,10);
ArrayFill(Variable,18,9,100);

在执行这些代码后,与第一个维度的第一个元素相关的所有元素将具有值 1,与第二个元素相关的元素将具有值 10,与第三个元素相关的元素将具有值 100。


将数组传递至函数

与变量不同,数组只能通过引用传递至函数。这意味着函数没有创建自己的数组实例,而是直接使用传递给它的数组。因此,函数对数组做出的所有更改会影响原始数组。

如果变量以通常的方式(通过值)传递至函数,无法通过函数更改传递的变量的值:

int x=1;
Func(x);

void  Func(int arg){
   arg=2;
}

在执行 Func() 函数后,x 值仍然等于 1。

如果变量通过引用传递(通过 & 表示),函数可以更改传递给它的这个变量的值:

int x=1;
Func(x);

void  Func(int &arg){
   arg=2;
}

在执行 Func() 函数后,x 值变为 2。

当传递数组至函数时,您需要指定自变量通过引用传递并代表一个数组(括号中):

void Func(double &arg[]){
   // ...
}

当传递多维数组至函数时,应指定维度大小(第一个维度除外):

double var[][3][3];

void Func(double &arg[][3][3]){
   // ...
}

在这种情形下,使用常量更为明智:

#define SIZE1 3
#define SIZE2 3

double Var[][SIZE1][SIZE2];

void Func(double &arg[][SIZE1][SIZE2]){
   // ...
}


从文件保存和载入数组

当从文件保存和载入数组时,您应始终考虑数组的第一个维度的大小和数组元素总数量的值之间的差异。要保存数组,我们首先将数组大小(元素的总数量由 ArraySize() 函数决定)然后是整个数组写入文件:

bool SaveArrayToFile(string FileName,string &Array[])
  {
//--- Open the file
   int h=FileOpen(FileName,FILE_TXT|FILE_WRITE);
   if(h==-1) return(false); // Error opening the file
//--- Write to the file
   FileWriteInteger(h,ArraySize(Array),INT_VALUE); // Write the array size
   FileWriteArray(h,Array); // Write the array
//--- Close the file
   FileClose(h);
   return(true); // Saving complete
  }

结果,我们得到一个相当通用的用于保存一维数组的函数。

要从文件载入数组,我们首先需要读取数组大小、调整大小并最后读取数组:

bool LoadArrayFromFile(string FileName,double &Array[])
  {
//--- Open the file
   int h=FileOpen(FileName,FILE_BIN|FILE_READ);
   if(h==-1) return(false); // Error opening the file
//--- Read the file
   int Size=FileReadInteger(h,INT_VALUE); // Read the number of array elements
   ArrayResize(Array,Size); // Resize the array. 
                            // In one-dimensional arrays the size of the first dimension is equal to the number of array elements.
   FileReadArray(h,Array); // Read the array from the file
//--- Close the file
   FileClose(h);
   return(true); // Reading complete
  }

当从文件载入多维数组时,您将需要计算第一个维度的大小。例如,假设我们正读取一个三维数组:

bool LoadArrayFromFile3(string FileName,double &Array[][SIZE1][SIZE2])
  {
//--- Open the file
   int h=FileOpen(FileName,FILE_BIN|FILE_READ);
   if(h==-1)return(false); // Error opening the file
//--- Read the file   
   int SizeTotal=FileReadInteger(h,INT_VALUE); // Read the number of array elements
   int Elements=SIZE1*SIZE2; // Calculate the number of elements 
   int Size=SizeTotal/Elements; // Calculate the size of the first dimension
   ArrayResize(Array,Size); // Resize the array
   FileReadArray(h,Array); // Read the array
//--- Close the file
   FileClose(h);
   return(true); // Reading complete
  }

极有可能,文件包含的是一个 2*3 数组,而我们尝试将其作为 3*3 数组读取。您可以通过将计算得出的第一个维度的大小乘以元素数量来检查大小间的一致性。如果结果值等于数组元素的总数量,我们可以说它们是一致的。

然而,Var[2][3] 数组将与 Var[3][2] 数组相符。如果您还需要适用这种情形,您应该保存多维数组的更多信息。例如,您可以首先保存数组元素的数量,然后保存数组的维度数,接下来保存每个维度的大小和数组本身。

上文提及的最后一个函数不是通用的,仅设计用于读取三维数组,其中第二个维度的大小等于 SIZE1,第三个维度的大小等于 SIZE2。由于没有动态更改除第一个维度以外的所有数组维度的方法,这不是问题 - 我们将为需要在程序中使用的数组创建函数。

通用性在这种情形下不是必要的:数组维度(第一个维度除外)的大小将不通过程序的外部参数控制。但是,如果您需要实现控制其他维度的大小的可能性,您可以使用已知大小更大的多维数组和额外的变量或通过应用面向对象编程 (OOP) 技巧解决此任务。下文中我们将更多地讨论第二种方法。


使用动态数组

动态数组在您不提前知道数组大小时使用。如果数组大小取决于程序属性窗口中设置的参数,使用动态数组将不会是一个问题:数组大小将仅在程序初始化期间更改一次。

数组可用于动态地收集特定信息,例如有关挂单的信息。它们的数量可能有所变化,即所需大小无法预知。在这种情形下,最简单的处理莫过于在传递订单前将数组大小改为 0,然后随着我们传递每个订单将数组大小增加一个元素。这很管用,但速度太慢。

在传递订单前,我们可以根据订单数量仅更改数组大小一次。这将需要另一个变量用于数组最后一个活动元素的索引(或实际活动的数组元素的数量而不是索引)。如果您已知数组的最大大小,此方法适用。如果数组的最大大小未知,我们可以通过使用区块调整数组大小来加快处理,如下面的类所示:

class CDynamicArray
  {
private:
   int               m_ChunkSize;    // Chunk size
   int               m_ReservedSize; // Actual size of the array
   int               m_Size;         // Number of active elements in the array
public:
   double            Element[];      // The array proper. It is located in the public section, 
                                     // so that we can use it directly, if necessary
   //+------------------------------------------------------------------+
   //|   Constructor                                                    |
   //+------------------------------------------------------------------+
   void CDynamicArray(int ChunkSize=1024)
     {
      m_Size=0;                            // Number of active elements
      m_ChunkSize=ChunkSize;               // Chunk size
      m_ReservedSize=ChunkSize;            // Actual size of the array
      ArrayResize(Element,m_ReservedSize); // Prepare the array
     }
   //+------------------------------------------------------------------+
   //|   Function for adding an element at the end of array             |
   //+------------------------------------------------------------------+
   void AddValue(double Value)
     {
      m_Size++; // Increase the number of active elements
      if(m_Size>m_ReservedSize)
        { // The required number is bigger than the actual array size
         m_ReservedSize+=m_ChunkSize; // Calculate the new array size
         ArrayResize(Element,m_ReservedSize); // Increase the actual array size
        }
      Element[m_Size-1]=Value; // Add the value
     }
   //+------------------------------------------------------------------+
   //|   Function for getting the number of active elements in the array|
   //+------------------------------------------------------------------+
   int Size()
     {
      return(m_Size);
     }
  };

您可以在随附的 CDynamicArray.mqh 文件中找到此类。该文件必须位于终端数据目录的 MQL5\Include 文件夹中。

现在,我们来评估和比较两种情形下的代码性能:其中数组大小依次加 1 或使用区块增加:

int n=50000;
   double ar[];
   CDynamicArray da;

//--- Option 1 (increasing the size by the 1st element)
   long st=GetTickCount(); // Store the start time 
   ArrayResize(ar,0); // Set the array size to zero 
   for(int i=0;i<n;i++)
     {
      ArrayResize(ar,i+1); // Resize the array sequentially
      ar[i]=i;
     }
   Alert("Option 1: "+IntegerToString(GetTickCount()-st)+" ms"); // Message regarding the amount of time required to perform the first option

//--- Option 2 (increasing the size using chunks)
   st=GetTickCount(); // Store the start time 
   for(int i=0;i<n;i++)
     {
      da.AddValue(i); // Add an element
     }
   Alert("Option 2: "+IntegerToString(GetTickCount()-st)+" ms"); // Message regarding the amount of time required to perform the second option

  }

此测试的脚本形式可在随附的 sTest_Speed.mq5 文件中找到。该文件必须位于终端数据目录的 MQL5\Scripts 文件夹中。

第一个选项的性能用时数秒,而第二个选项几乎是瞬时。


数组索引编排顺序

在调整数组大小时,新的元素通常被添加到数组的末尾:

double ar[]; // Array
ArrayResize(ar,2); // Prepare the array
ar[0]=1; // Set the values
ar[1]=2; 
ArrayResize(ar,3); // Increase the array size
ar[2]=3; // Set the value for the new array element
Alert(ar[0]," ",ar[1]," ",ar[2]); // Print array values

在执行这些代码后,数组中包含的值应为 1、2 和 3。

数组中的元素也可以逆序编排索引。索引编排顺序由 ArraySetAsSeries() 函数设置:

ArraySetAsSeries(ar,true); // set indexing in reverse order
ArraySetAsSeries(ar,false); // set normal indexing

在更改按逆序编排索引的数组的大小时,新的元素被添加至数组的开头:

double ar[]; // Array
ArrayResize(ar,2); // Prepare the array
ar[0]=1; // Set the values
ar[1]=2; 
ArraySetAsSeries(ar,true); // Change the indexing order
ArrayResize(ar,3); // Increase the array size
ar[0]=3; // Set the value for the new array element
Alert(ar[0]," ",ar[1]," ",ar[2]); // Print array values

在执行这些代码后,数组中包含的值应为 3、2 和 1。

结果在两种情形下新元素被添加至数组的同一侧,唯一不同的是索引编排顺序。此函数不能用于在数组的开头添加元素,如果该数组按正常顺序编排索引的话。要在正常编排索引的数组的末尾添加元素,您只需要增加数组大小并为最后一个元素分配值。

要添加元素至数组的开头,您应该增加数组大小,移动所有值并为 0 元素分配新值。如果数组按逆序编排索引,可以轻松地将新元素添加至数组开头。但如果您需要将新元素添加至数组的末尾,您首先应增大数组大小,然后将所有值移至开头并为最后一个元素分配新值。索引编排顺序操作不会解决此问题。

数组索引编排顺序可使用 ArrayIsSeries() 函数决定:

bool series=ArrayIsSeries(ar);

如果数组以逆序编排索引,函数将返回 true。

以逆序编排索引的数组主要用于“EA 交易”。在开发“EA 交易”时,从右向左对柱进行计数往往更为方便,因此在复制价格数据和指标缓冲区至数组时以逆序编排索引。


复制数组

最简单的复制方法是在循环中迭代数组,然后逐个将元素从一个数组复制到另一个数组。但是,在 MQL5 中有一个特殊函数可用于复制数组 - ArrayCopy()

double ar1[]={1,2,3};
double ar2[];

ArrayCopy(ar2,ar1);

在执行上述代码后,ar2 数组将由三个具有和 ar1 数组相同的值的元素组成: 1、2、3。

如果要复制的元素数量与要复制到的数组不符,数组大小将自动增加(数组必须是动态的)。如果数组大于要复制的元素数量,其大小保持不变。

ArrayCopy() 函数还允许您仅复制数组的一部分。使用该函数的可选参数,您可以指定要复制的第一个元素、第一个复制元素在新数组中的索引以及您打算复制的元素数量。

除了将元素从一个数组复制到另一个数组,ArrayCopy() 函数可用于在同一数组内复制元素:

double ar1[]={1,2,3,4,5};
ArrayCopy(ar1,ar1,1,2);

我们从索引为 2 的元素开始复制数据,并将它们从索引为 1 的元素开始粘贴。在执行代码后,数组将包含下述值: 1、3、4、5、5。

ArrayCopy() 函数还允许您将数据向右移位:

double ar1[]={1,2,3,4,5};
ArrayCopy(ar1,ar1,2,1);

我们从索引为 1 的元素开始获取数据,并将它们从索引为 2 的元素开始安排。在执行代码后,数组将包含下述值: 1、2、2、3、4。

ArrayCopy() 函数也可应用于多维数组,它将多维数组视作一维数组处理,将其所有元素视为一个序列:

double ar[3][2]={{1, 2},{3, 4},{5, 6}};
ArrayCopy(ar,ar,2,4);

在执行这些代码后,数组将如下所示: {1, 2}、{5, 6}、{5, 6}。


排序数组

数组可使用 ArraySort() 函数排序:

double ar[]={1,3,2,5,4};
ArraySort(ar);

在执行上述代码后,数组值将按照下面显示的顺序排列: 1、2、3、4、5。

ArraySort() 函数无法应用至多维数组。您可以在《Electronic Tables in MQL5》(MQL5 中的电子表格)一文中找到多维数组排序和数据结构的相关信息。


我们使用 ArrayBsearch() 函数进行二分搜索。此函数仅能在排序数组中正确工作。二分搜索的名称取自该算法不断将数组划分为两个部分的事实。该算法首先将目标值与数组的中间元素的值进行比较,以确定包含目标元素的那一半数组 - 左边的子数组或右边的子数组。然后算法将目标值与子数组的中间元素的值进行比较,依此类推。

ArrayBsearch() 函数返回具有目标值的元素的索引:

double ar[]={1,2,3,4,5};

int index=ArrayBsearch(ar,3);

在执行这些代码后,索引变量将具有值 2。

如果无法在数组中找到目标值,函数将返回最接近的稍小的值的元素的索引。由于这个属性,函数可用于按时间搜索柱。如果没有柱具有指定的时间,具有较少时间的柱将被用于计算。

如果目标值不在数组中并超出了数组值的范围,函数将返回 0(如果目标值小于最小值)或最后一个索引(如果目标值大于最大值)。

只有一种方法可用于搜索无序数组 - 迭代数组:

int FindInArray(int &Array[],int Value){
   int size=ArraySize(Array);
      for(int i=0; i<size; i++){
         if(Array[i]==Value){
            return(i);
         }
      }
   return(-1);
}

在上例中,函数返回具有目标值的元素的索引。如果目标值不在数组中,函数返回 -1。


查找最大值和最小值

数组中的最大值和最小值可使用 ArrayMaximum()ArrayMinimum() 函数查找,函数将分别返回具有最大值或最小值的元素的索引:

double ar[]={3,2,1,2,3,4,5,4,3};

int MaxIndex=ArrayMaximum(ar);
int MinIndex=ArrayMinimum(ar);
double MaxValue=ar[MaxIndex];
double MinValue=ar[MinIndex];

在执行上述代码后,MaxIndex 变量将等于 6,MinIndex 变量将等于 2,MaxValue 将具有值 5 而 MinValue 将具有值 1。

ArrayMaximum() 和 ArrayMinimum() 函数允许您通过在搜索范围中指定第一个元素的索引和元素数量来限制搜索范围:

int MaxIndex=ArrayMaximum(ar,5,3);
int MinIndex=ArrayMinimum(ar,5,3);

在本例中,MaxIndex 将具有值 6 而 MinIndex 将具有值 - 5。请注意,指定的范围中包含两个具有最小值 4 的位置 - 位置 5 和位置 7。在这种情况下,函数返回更接近数组开头的元素的索引。这些函数以同样的方式处理以逆序编排索引的数组 - 它们返回最小索引。

至此,我们已将 MQL5 中处理数组的所有可用标准函数讨论完毕。


使用 OOP 创建多维数组

一组用于创建多维数组的类包含三个类:一个基类和两个子类。取决于在创建对象阶段选择的子类,对象可表示双变量数组或对象数组。对象数组的每个元素可表示其他对象或变量数组。

基类和子类几乎不包含任何函数,除了基类中的析构函数和子类中的构造函数。基类中的析构函数用于在程序或函数完成后删除所有对象。子类中的构造函数仅用于根据在构造函数的参数中指定的大小缩放数组:在一个类中缩放对象数组,在其他类中缩放变量数组。下面是实现这些类的代码:

//+------------------------------------------------------------------+
//|   Base class                                                     |
//+------------------------------------------------------------------+
class CArrayBase
  {
public:
   CArrayBase       *D[];
   double            V[];

   void ~CArrayBase()
     {
      for(int i=ArraySize(D)-1; i>=0; i--)
        {
         if(CheckPointer(D[i])==POINTER_DYNAMIC)
           {
            delete D[i];
           }
        }
     }
  };
//+------------------------------------------------------------------+
//|   Child class 1                                                  |
//+------------------------------------------------------------------+
class CDim : public CArrayBase
  {
public:
   void CDim(int Size)
     {
      ArrayResize(D,Size);
     }
  };
//+------------------------------------------------------------------+
//|   Child class 1                                                  |
//+------------------------------------------------------------------+
class CArr : public CArrayBase
  {
public:
   void CArr(int Size)
     {
      ArrayResize(V,Size);
     }
  };

您可以在随附的 CMultiDimArray.mqh 文件中找到这些类。该文件必须位于终端数据目录的 MQL5\Include 文件夹中。

现在,我们使用此类构建一个类似一维数组:

CArrayBase * A; // Declare a pointer
   A=new CArr(10); // Load a child class instance that scales the array of variables. 
                   // The array will consist of 10 elements.

//--- Now the array can be used:
   for(int i=0; i<10; i++)
     {
      //--- Assign to each element of the array successive values from 1 to 10
      A.V[i]=i+1;
     }
   for(int i=0;i<10;i++)
     {
      //--- Check the values
      Alert(A.V[i]);
     }
   delete A; // Delete the object
  }

此示例的脚本形式可在随附的 sTest_1_Arr.mq5 文件中找到。该文件必须位于终端数据目录的 MQL5\Scripts 文件夹中。

现在,我们尝试创建二维数组。第一个维度的每个元素将包含第二个维度的不同元素数量 - 第一个包含一个,第二个包含两个,等等:

CArrayBase*A;  // Declare a pointer
   A=new CDim(3); // The first dimension represents an array of objects

//--- Each object of the first dimension represents an array of variables of different sizes 
   A.D[0]=new CArr(1);
   A.D[1]=new CArr(2);
   A.D[2]=new CArr(3);
//--- Assign values
   A.D[0].V[0]=1;

   A.D[1].V[0]=10;
   A.D[1].V[1]=20;

   A.D[2].V[0]=100;
   A.D[2].V[1]=200;
   A.D[2].V[2]=300;
//--- Check the values
   Alert(A.D[0].V[0]);

   Alert(A.D[1].V[0]);
   Alert(A.D[1].V[1]);

   Alert(A.D[2].V[0]);
   Alert(A.D[2].V[1]);
   Alert(A.D[2].V[2]);
//---
   delete A; // Delete the object

此示例的脚本形式可在随附的 sTest_2_Dim.mq5 文件中找到。该文件必须位于终端数据目录的 MQL5\Scripts 文件夹中。

结果数组为静态,因为类没有更改数组大小的方法。但由于 D[] 和 V[] 数组位于类的公共部分,它们对任何操作均可用。并且您可以轻松地缩放 V[] 数组。在缩放 D[] 数组并减小其大小时,您应该首先删除指向要删除对象的对象,或在增加数组大小时将对象载入它们。

如果需要,读者也可以考虑使用 OOP 或数据结构的其他方法实现多维数组。


总结

本文涵盖 MQL5 中处理数组的所有可用标准函数。我们已论述了处理数组的特性和一些最重要的技巧。MQL5 语言一共提供了 15 个函数,其中部分函数极其重要,而其他函数可能完全用不到,除非是在您需要解决一些非常规问题的情形中。这些函数可依据重要性和使用频率排列如下:

  1. ArraySize()ArrayResize() 是基本函数。

  2. ArrayMaximum()ArrayMinimum()ArrayCopy()ArrayInitialize()ArrayFill()ArrayFree() 是令数组处理变得非常简单的函数。

  3. ArraySort() 是一个重要和方便的函数,但由于其较低的功能性而很少用到。

  4. ArrayBsearch() 函数很少用到,但它可能在罕见的例外情形中非常重要。

  5. ArraySetAsSeries()ArrayRange()ArrayGetAsSeries()ArrayIsDynamic()ArrayIsSeries() 函数极少用到或几乎不会用到。

对于在本文中作为编程技巧之一进行论述的动态数组的使用,读者应予以特别关注,因为这对于程序性能可产生极大的影响,或者可以说能够决定程序性能。