指针
正如我们在 类定义 一节中所述,MQL5 中的指针是对象的一些描述符(唯一编号),而不是像 C++ 中那样的内存地址。对于自动对象,我们通过在其名称前添加一个与号 (&) 来获取指针(在这种上下文中,与号是“取地址”运算符)。因此,在以下示例中,变量 p 指向自动对象 s。
Shape s; // automatic object
Shape *p = &s; // a pointer to the same object
s.draw(); // calling an object method
p.draw(); // doing the same
|
在前面的小节中,我们学习了如何通过使用 new 动态创建对象来获取指向该对象的指针。此时,不需要使用与号来获取描述符:指针的值本身就是描述符。
MQL5 API 提供了函数 GetPointer,它的作用与与号运算符 '&' 相同,即返回对象的指针:
void *GetPointer(Class object);
|
使用这两种方式中的哪一种取决于个人偏好。
指针常用于将对象链接在一起。我们来演示一下创建从属对象的思路,这些从属对象接收指向其对象创建程序 (ThisCallback.mq5) 的 this 的指针。我们在关于 this关键字的章节中提到过这个技巧。
现在我们尝试利用这个技巧来实现一个方案,以便不定期向“创建程序”通知从属对象中执行的计算完成百分比:我们之前使用 函数指针实现了类似的功能。Manager 类负责控制计算,实际计算(很可能使用不同的公式)则在单独的类中执行。本例中展示了其中一个类,即 Element。
class Manager; // preliminary announcement
class Element
{
Manager *owner; // pointer
public:
Element(Manager &t): owner(&t) { }
void doMath()
{
const int N = 1000000;
for(int i = 0; i < N; ++i)
{
if(i % (N / 20) == 0)
{
// we pass ourselves to the method of the control class
owner.progressNotify(&this, i * 100.0f / N);
}
// ... massive calculations
}
}
string getMyName() const
{
return typename(this);
}
};
class Manager
{
Element *elements[1]; // array of pointers (1 for demo)
public:
Element *addElement()
{
// looking for an empty slot in the array
// ...
// passing to the constructor of the subclass
elements[0] = new Element(this); // dynamic creation of an object
return elements[0];
}
void progressNotify(Element *e, const float percent)
{
// Manager chooses how to notify the user:
// display, print, send to the Internet
Print(e.getMyName(), "=", percent);
}
};
|
从属对象可以使用接收到的链接向“上级”通知工作进度。计算完成时,会向控制对象发送一个信号,表明可以删除计算器对象,或者让另一个计算器对象开始工作。当然,Manager 类中固定的单一元素数组可能不太起眼,但作为演示,足以说明问题。Manager 类不仅管理计算任务的分发,还提供了一个抽象层以实现用户通知功能:不仅可以将消息输出到日志,它可以写入单独文件、显示在屏幕上或发送到互联网。
顺便说一句,请注意在 Element 类定义之前对 Manager 类进行了前置声明。这样做是必要的,为的是在 Element 类中声明一个指向 Manager 类的指针,而 Manager 类会出现在后面的代码中进行定义。如果省略前向声明,我们会遇到错误:“'Manager' - 意外的标记,可能缺少类型?”。
当两个类通过其成员相互引用时,就需要使用前向声明:在这种情况下,无论我们以何种顺序排列这两个类的定义,都无法完整地定义其中任何一个。前向声明允许我们在没有完整定义的情况下保留一个类型名称。
指针的一个基本特性是,指向基类的指针可用来指向任何派生类的对象。这是 多态性的一种体现。这种行为之所以可行,是因为派生类对象包含了父类的内置“子对象”,就像层层嵌套的俄罗斯套娃一样。
特别是对于我们的形状绘制任务,可以很容易地描述一个指向 Shape 指针的动态数组,并根据用户的请求向其中添加不同类型的对象。
类的数量将扩展到五个 (Shapes2.mq5)。除了 Rectangle 和 Ellipse,我们还将添加 Triangle,并创建一个派生自 Rectangle 且表示正方形的类 (Square),以及一个派生自 Ellipse 且表示圆形的类 (Circle)。显然,正方形是边长相等的矩形,而圆形是长轴半径和短轴半径相等的椭圆。
为了沿着继承链传递字符串类名,我们在 Rectangle 和 Ellipse 类的 protected 部分添加带有额外字符串参数 t 的特殊构造函数:
class Rectangle : public Shape
{
protected:
Rectangle(int px, int py, int sx, int sy, color back, string t) :
Shape(px, py, back, t), dx(sx), dy(sy)
{
}
...
};
|
然后,在创建正方形时,我们不仅设置相等的边长,还从 Square 类传递 typename(this):
class Square : public Rectangle
{
public:
Square(int px, int py, int sx, color back) :
Rectangle(px, py, sx, sx, back, typename(this))
{
}
};
|
此外,我们将 Shape 类中的构造函数移至 protected 部分:这一操作将禁止创建 Shape 对象自身,它只能作为其后代类的基类。
我们指定 addRandomShape 函数来生成形状,该函数返回指向新创建对象的指针。为了便于演示,此函数现在将实现随机生成形状:类型、位置、大小和颜色都是随机的。
支持的形状类型在 SHAPES 枚举中进行汇总:这些形状对应于五个已实现的类。
给定范围内的随机数由 random 函数返回(使用内置函数 rand,每次调用时返回一个 0 到 32767 范围内的随机整数)。形状的中心坐标在 0 到 500 像素的范围内生成,形状的大小最大为 200。颜色由三个 RGB 分量组成(参见 颜色 一节),每个分量的范围从 0 到 255。
int random(int range)
{
return (int)(rand() / 32767.0 * range);
}
Shape *addRandomShape()
{
enum SHAPES
{
RECTANGLE,
ELLIPSE,
TRIANGLE,
SQUARE,
CIRCLE,
NUMBER_OF_SHAPES
};
SHAPES type = (SHAPES)random(NUMBER_OF_SHAPES);
int cx = random(500), cy = random(500), dx = random(200), dy = random(200);
color clr = (color)((random(256) << 16) | (random(256) << 8) | random(256));
switch(type)
{
case RECTANGLE:
return new Rectangle(cx, cy, dx, dy, clr);
case ELLIPSE:
return new Ellipse(cx, cy, dx, dy, clr);
case TRIANGLE:
return new Triangle(cx, cy, dx, clr);
case SQUARE:
return new Square(cx, cy, dx, clr);
case CIRCLE:
return new Circle(cx, cy, dx, clr);
}
return NULL;
}
void OnStart()
{
Shape *shapes[];
// simulate the creation of arbitrary shapes by the user
ArrayResize(shapes, 10);
for(int i = 0; i < 10; ++i)
{
shapes[i] = addRandomShape();
}
// processing shapes: for now, just output to the log
for(int i = 0; i < 10; ++i)
{
Print(i, ": ", shapes[i].toString());
delete shapes[i];
}
}
|
我们生成 10 个形状并将它们输出到日志(由于是随机选择类型和属性,结果可能会有所不同)。别忘了使用 delete 删除对象,因为这些对象是动态创建的(这里在同一个循环中完成删除是因为这些形状之后不再使用;在实际程序中,形状数组很可能会以某种方式存储到文件中,以便稍后加载并继续处理图像)。
0: Ellipse 241 38
1: Rectangle 10 420
2: Circle 186 38
3: Triangle 27 225
4: Circle 271 193
5: Circle 293 57
6: Rectangle 71 424
7: Square 477 46
8: Square 366 27
9: Ellipse 489 105
|
这些形状已成功创建,并输出了它们的属性信息。
现在我们已经准备好访问我们类的 API,也就是 draw 方法。