将对象和数组清零
通常,初始化或填充变量和数组并不会导致问题。因此,对于简单变量,我们可以配合 初始化在定义语句中直接使用运算符 '=',或者在之后赋予所需的值。
聚合视图初始化适用于结构体(参见章节 定义结构体):
Struct struct = {value1, value2, ...};
|
但是,仅当结构体中没有动态数组和字符串时才适用。此外,聚合初始化句法不能再用于清除结构体。相反,要么必须单独为每个字段赋值,要么在程序中保留空结构体的实例,并将其复制到可清除的实例。
如果我们所说的数组同时还是结构体数组,则源代码会因其辅助但却必要的指令而快速增加。
对于数组,有 ArrayInitialize和ArrayFill 函数,但它们只支持数值类型:字符串或结构体数组不能使用这两个函数填充。
在此类情况下,便可以使用 ZeroMemory 函数。虽然这个函数不是万能的(在范围上有很大限制),但了解它也有好处。
void ZeroMemory(void &entity)
该函数可应用于广泛的不同实体:简单类型或对象类型变量及其数组(固定、动态或多维数组)。
变量获取 0 值(对于数字)或其等效值(对于字符串和指针,为 NULL)。
对于数组,所有其元素均设置为零。别忘了,元素可以是对象,而对象又能包含对象。换言之,ZeroMemory 函数在单次调用中执行深度内存清理。
然而,对有效对象存在限制。只有满足以下条件的结构和类对象才能用零填充:
- 仅包含公共字段(即不包含访问类型为 private 或 protected 的数据)
- 不包含具有 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(data, LIMIT);
}
};
|
在 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", &this, x);
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。
该函数允许你处理类层次结构的对象。例如,我们可以描述源自 Base 的 Dummy 类的派生类:
class Dummy : public Base
{
public:
double data[]; // could also be multidimensional: ZeroMemory will work
string s;
Base *pointer; // public pointer (dangerous)
public:
Dummy()
{
ArrayResize(data, LIMIT);
// 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_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 处的对象丢失,无法在析构函数中释放。因此,在脚本终止后,这会导致其余对象中内存泄露的警告。
ZeroMemory 用于 OnStart 中以清除 Dummy 对象数组:
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();
}
|
日志将输出类似以下内容(初始状态将不同,因为它打印 "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 如何能够解决字符串数组初始化的问题。ArrayInitialize 和 ArrayFill 函数不能处理字符串。
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 函数解决。