对象类型转换:dynamic_cast 和 pointer void *
对象类型具有特定的转换规则,这些规则在源和目标变量类型不匹配时适用。内置类型的规则已经在第 2.6 章 类型转换中讨论过。复制时结构体类型转换的细节在 结构体布局和继承 一节中进行介绍。
对于结构体和类,是否允许类型转换主要取决于它们是否应在继承链上存在关联。继承体系中属于不同分支的类型或根本不相关的类型不能相互转换。
对象(值)和指针的类型转换规则不同。
对象
如果类型 B 拥有一个接受类型 A 参数的构造函数(有不同变体,比如按值、按引用或按指针,但通常形式为 B(const A &a)),则类型 A 的对象可以赋值给类型 B 的对象。这样的构造函数也称为转换构造函数。
在没有显式转换构造函数的情况下,编译器会尝试使用隐式拷贝赋值运算符,即 B::operator=(const B &b)。要使这种隐式转换(从 A 到 B)有效,类 A 和 B 必须在同一继承链中。如果 A 继承自 B(包括间接继承),则在拷贝到 B 时,A 中新增的属性将会消失。如果 B 继承自 A,则只会将 A 中存在的属性部分拷贝到 B。通常不推荐这种隐式转换。
此外,编译器也不一定提供隐式拷贝运算符。特别是,如果类具有带 const 修饰符的字段,则认为禁止拷贝(详见后文)。
在 ShapesCasting.mq5 脚本中,我们使用形状类层次结构来演示对象类型转换。在 Shape 类中,type 字段被特意设为常量,因此尝试将 Square 对象转换(赋值)为 Rectangle 对象会导致编译器错误,并给出详细解释:
attempting to reference deleted function 'void Rectangle::operator=(const Rectangle&)'
|
根据此消息可以看出,Rectangle::operator=(const Rectangle&) 拷贝方法已被编译器隐式移除(编译器提供了默认实现),因为它在 Shape::operator =(const Shape&) 基类中使用了类似的方法,而后者又因存在带有 const 修饰符的 type 字段而被移除。此类字段只能在创建对象时设置,而编译器不知道如何在这种限制下拷贝对象。
顺便说一句,“删除”方法的效果不仅对编译器有效,对程序员也有效:更多相关内容将在 继承控制:final 和 delete 一节中讨论。
可通过以下方法解决此问题:移除 const 修饰符,或自行提供各自的赋值运算符实现方式(其中不涉及 const 字段,并将内容与"Rectangle" 类型的说明一起保存):
Rectangle *operator=(const Rectangle &r)
|
请注意,此定义返回指向当前对象的指针,而编译器生成的默认实现为 void 类型(如错误消息中所示)。这意味着,编译器提供的默认赋值运算符不能在 x = y = z 链中使用。如果需要此功能,请显式重写 operator= 并返回所需的非 void 类型。
指针
最大的作用是将指针转换为不同类型的对象。
理论上,用于转换对象类型指针的所有选项可以简化为三种:
- 从基类到派生类的转换,这属于向下类型转换(向下转换),因为通常用倒置树来绘制类层次结构;
- 从派生类到基类的转换,这属于向上类型转换(向上转换);
- 在层次结构不同分支的类之间的转换,甚至不同家族的类之间的转换。
最后一种选项被禁止(我们会收到编译错误)。编译器允许前两种,但如果“向上转换”是自然且安全的,那么“向下转换”可能会导致运行时错误。
void OnStart()
|
当然,当使用指向基类对象的指针时,即使相应的对象位于指针处,也不能对其调用派生类的方法和属性。否则我们会遇到编译错误“未声明的标识符”。
但是,指针支持 显式强制转换 语法(参见 C 风格),该语法允许在表达式中将指针“动态”转换为所需类型,并对其进行取消引用,而无需创建中间变量。
Base *b;
|
这里,我们创建了一个派生类对象 (Derived) 和一个指向它的基类指针 (Base *)。若要访问派生类的 derivedMethod 方法,需要将指针临时转换为 Derived 类型。
星号指针类型必须用圆括号括起来。此外,强制转换表达式本身(包括变量名)也必须用另一对圆括号括起来。
在我们的测试中,还有另一个编译错误,提示“类型不匹配”-“类型不匹配”。这个错误出现在我们试图将一个 Rectangle 类型的指针强制转换成 Circle 类型的指针的那行代码上,因为这两个类处于不同的继承分支中。
c = r; // error: type mismatch |
当强制类型转换的指针与实际对象不匹配时,情况会更加糟糕(尽管它们的类型兼容,因此程序可以正常编译)。这样的运算会在程序执行阶段就以错误结束(也就是说,编译器无法捕获错误)。程序随后会被卸载。
例如,在 ShapesCasting.mq5 脚本中,我们描述了一个指向 Square 的指针,并为其赋予了一个指向 Shape 的指针,而 Shape 包含 Rectangle 对象。
Square *s2;
|
终端返回错误“指针强制转换不正确”。更具体的 Square 类型的指针无法指向父对象 Rectangle。
为了避免运行时问题并防止程序崩溃,MQL5 提供了一个特殊的语言结构 dynamic_cast。使用此结构,您可以“仔细”检查是否可以将指针转换为所需的类型。如果可以转换,则编译器会进行转换。如果不可转换,我们将得到一个空指针 (NULL),我们可以用特殊方式处理它(例如,使用 if 以某种方式初始化或中断函数的执行,而不是整个程序)。
dynamic_cast 的语法如下:
dynamic_cast< Class * >( pointer ) |
在我们的例子中,只需编写:
s2 = dynamic_cast<Square *>(p); // trying to cast type, and will get NULL if unsuccessful
|
程序将按预期运行。
具体来说,我们可以再次尝试将矩形转换为圆形,并确保结果为 0:
c = dynamic_cast<Circle *>(r); // trying to cast type, and will get NULL if unsuccessful
|
MQL5 中有一个特殊的指针类型,可以存储任何对象。此类型的表示法为:void *。
我们来演示一下变量 void * 如何与 dynamic_cast 配合使用。
void *v;
|
前三行将记录指针(同一对象的描述符)的值,后两行将输出 0。
现在,回到 指标 一节中的前向声明示例(参见文件 ThisCallback.mq5),其中 Manager 和 Element 类包含相互的指针。
使用指针类型 void *,您可以删除初始声明 (ThisCallbackVoid.mq5)。我们注释掉包含它的那一行,并将包含对象管理器指针的 owner 字段类型更改为 void *。在构造函数中,我们也会更改参数的类型。
// class Manager;
|
这种方法可以提供更大的灵活性,但需要更加小心,因为 dynamic_cast 可能会返回 NULL。建议尽可能使用语言自身提供的类型控制的标准调度机制(静态和动态)。
通常,仅在特殊情况下才有必要使用 void * 指针。而带有初步说明的“额外”行不属于特殊情况。它在这里仅被用作 void * 指针通用性的最简单示例。