将对象和数组清零

通常,初始化或填充变量和数组并不会导致问题。因此,对于简单变量,我们可以配合 初始化在定义语句中直接使用运算符 '=',或者在之后赋予所需的值。

聚合视图初始化适用于结构体(参见章节 定义结构体):

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

但是,仅当结构体中没有动态数组和字符串时才适用。此外,聚合初始化句法不能再用于清除结构体。相反,要么必须单独为每个字段赋值,要么在程序中保留空结构体的实例,并将其复制到可清除的实例。

如果我们所说的数组同时还是结构体数组,则源代码会因其辅助但却必要的指令而快速增加。

对于数组,有 ArrayInitializeArrayFill 函数,但它们只支持数值类型:字符串或结构体数组不能使用这两个函数填充。

在此类情况下,便可以使用 ZeroMemory 函数。虽然这个函数不是万能的(在范围上有很大限制),但了解它也有好处。

void ZeroMemory(void &entity)

该函数可应用于广泛的不同实体:简单类型或对象类型变量及其数组(固定、动态或多维数组)。

变量获取 0 值(对于数字)或其等效值(对于字符串和指针,为 NULL)。

对于数组,所有其元素均设置为零。别忘了,元素可以是对象,而对象又能包含对象。换言之,ZeroMemory 函数在单次调用中执行深度内存清理。

然而,对有效对象存在限制。只有满足以下条件的结构和类对象才能用零填充:

  • 仅包含公共字段(即不包含访问类型为 privateprotected 的数据)
  • 不包含具有 const 修饰符的字段
  • 不包含指针

前两条限制由编译器强制检查:若对象的字段不满足指定要求,则尝试对这些对象执行归零将会导致错误(见下文)。

第三个限制是一项建议:对指针进行外部清零将导致难以检查数据完整性,从而可能导致关联对象丢失和内存泄漏。

严格来说,对可空化对象中字段的公开性要求违反了 封装 原理,该原理为类对象中所固有,因此,ZeroMemory 主要用于简单结构体对象及其数组。

ZeroMemory 使用示例在 ZeroMemory.mq5 脚本中提供。

通过 Simple 结构体演示了聚合初始化列表的问题:

#define LIMIT 5
   
struct Simple
{
   MqlDateTime data[]; // dynamic array disables initialization list,
   // string s; // and a string field would also forbid,
   // ClassType *ptr; // and a pointer too
   Simple()
   {
      // allocating memory, it will contain arbitrary data
      ArrayResize(dataLIMIT);
   }
};

OnStart 函数中或在全局上下文中,我们不能定义并立即空化一个该结构体的对象:

void OnStart()
{
   Simple simple = {}; // error: cannot be initialized with initializer list
   ...

编译器会抛出错误“无法使用初始化列表”。它特定于诸如动态数组、字符串变量和指针等字段。特别是,如果 data 数组是固定大小,则不会发生错误。

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

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

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

以下类在 Base 中定义。

class Base
{
public// public is required for ZeroMemory
   // const for any field will cause a compilation error when calling ZeroMemory:
   // "not allowed for objects with protected members or inheritance"
   /* const */ int x;
   Simple t;   // using a nested structure: it will also be nulled
   Base()
   {
      x = rand();
   }
   virtual void print() const
   {
      PrintFormat("%d %d", &thisx);
      ArrayPrint(t.data);
   }
};

由于该类后续会用于可通过 ZeroMemory 空化的对象数组中,因此我们不得不为其字段编写一个访问段 public(原则上对于类来说不并不常见,这样做是为了说明 ZeroMemory 施加的要求)。此外须注意,这些字段不能有 const 修饰符。否则将会出现编译错误,以及与问题不太相称的文本提示:“对于具有受保护成员或继承的对象是禁止的”。

类构造函数以随机数字填充 x 字段,所以稍后就能明确看到由函数 ZeroMemory 执行的清理。print 方法显示要分析的所有字段的内容,包含唯一对象编号(描述符)&this

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

   Base *base = new Base();
   ZeroMemory(base); // will set the pointer to NULL but leave the object

然而不应该这样做,因为该函数仅重置 base 变量本身,如果其原本指的是一个对象,则该对象将在内存中保持“挂起”状态,由于指针丢失而无法从程序访问。

仅当使用 delete 运算符释放了指针实例之后,才能空化该指针。此外,对于上面示例中的单独指针,使用赋值运算符来重置会更简单,这一点与任何其它简单变量(非复合变量)没有区别。对于复合对象和数组,最好使用 ZeroMemory

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

class Dummy : public Base
{
public:
   double data[]; // could also be multidimensional: ZeroMemory will work
   string s;
   Base *pointer// public pointer (dangerous)
   
public:
   Dummy()
   {
      ArrayResize(dataLIMIT);
      
      // due to subsequent application of ZeroMemory to the object
      // we'll lose the 'pointer'
      // and get warnings when the script ends
      // about undeleted objects of type Base
      pointer = new Base();
   }
   
   ~Dummy()
   {
      // due to the use of ZeroMemory, this pointer will be lost
      // and will not be freed
      if(CheckPointer(pointer) != POINTER_INVALIDdelete pointer;
   }
   
   virtual void print() const override
   {
      Base::print();
      ArrayPrint(data);
      Print(pointer);
      if(CheckPointer(pointer) != POINTER_INVALIDpointer.print();
   }
};

它包含带有 double 类型的动态数组、字符串以及 Base 类型指针的字段(这与该类所源自的类型相同,但在这里仅用于演示指针问题,以免描述另一个虚拟类)。当 ZeroMemory 函数空化 Dummy 对象时,pointer 处的对象丢失,无法在析构函数中释放。因此,在脚本终止后,这会导致其余对象中内存泄露的警告。

ZeroMemory 用于 OnStart 中以清除 Dummy 对象数组:

void OnStart()
{
   ...
   Print("Initial state");
   Dummy array[];
   ArrayResize(arrayLIMIT);
   for(int i = 0i < LIMIT; ++i)
   {
      array[i].print();
   }
   ZeroMemory(array);
   Print("ZeroMemory done");
   for(int i = 0i < LIMIT; ++i)
   {
      array[i].print();
   }

日志将输出类似以下内容(初始状态将不同,因为它打印 "dirty" 的内容,新分配的内存;这里是代码的一小部分):

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 函数不能处理字符串。

   string text[LIMIT] = {};
   // an algorithm populates and uses 'text'
   // ...
   // then you need to re-use the array
   // calling functions gives errors:
   // ArrayInitialize(text, NULL);
   //      `-> no one of the overloads can be applied to the function call
   // ArrayFill(text, 0, 10, NULL);
   //      `->  'string' type cannot be used in ArrayFill function
   ZeroMemory(text);               // ok

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

这个问题可以使用 ZeroMemory 函数解决。