为什么可以从实例化基类对象的投射指针调用非静态派生类方法

Why can a non-static derived class method be called from a casted pointer to an instantiated base class object?

本文关键字:调用 指针 静态 类方法 派生 实例化 对象 基类 为什么      更新时间:2023-10-16

我不明白下面的代码示例为什么有效,我想澄清一下。在我看来,由于derivedMethodDerived的非静态方法,它应该只能从实例化的Derived对象(或指向对象的指针)中调用。但是,通过将指向实例化的Base对象的指针强制转换为指向-Derived的指针,可以调用derivedMethod。为什么?

代码:

// compiled with gcc
#include <iostream>
using namespace std;
struct Base { };
struct Derived : Base 
{ 
void derivedMethod() { cout << "foo" << endl; } 
};
int main()
{
Base *basePtr = new Base();
((Derived *)basePtr)->derivedMethod();
delete basePtr;
return 0;
}

输出:

foo

编辑:

在发布这个问题之前,我修改了Derived以包含一个整数成员,然后在derivedMethod中输出。它仍然编译并运行,没有任何错误。

编辑:

我意识到这不是一种好的C++编码风格。这只是一个关于我提供的代码示例为什么有效的问题,因为它模仿了我在野外发现的代码。

使用强制转换时,您说"Make this aDerived *"实际上是在欺骗编译器。由于编译器必须执行您要求的操作[当然,在某些情况下,对于更复杂的代码,您可能确实想要执行此操作,因为您知道指针实际上是指向Derived的指针,只是目前您只有一个Base *指针]。

然而,"正确"的方法是使用dynamic_cast<Derived *>(basePtr),如果转换不起作用,它将返回NULL。这样的东西:

Derived *dPtr = dynamic_cast<Derived *>(basePtr);
if (dPtr != NULL)
{
dPtr->derivedMethod();
}

现在,这是安全的,因为如果basePtr没有指向有效的Derived类,结果将是NULL,而不会进入调用derivedMethod的代码。

还要注意的是,就目前情况来看,根本不能保证在代码中调用derivedMethod时会发生什么。它可能崩溃,也可能"工作"。(在这个简单的例子中,编译器甚至可能检测到情况并给出错误,但它不必这样做,这是因为在更复杂的情况下,编译器无论如何都无法检测到它)。

此外,在Derived中使用成员变量可能会也可能不会导致可检测的问题。这完全取决于new Base()返回的Base对象的"后面"是什么——它可能是"未使用的空间"(在这种情况下,一切都"按预期工作"[就好像你实际上为Derived对象分配了额外的空间一样——但当然,这个值没有初始化,所以不要对std::string这样做,因为它需要构造才能安全使用],或者它可能是一些重要的东西,所以如果你写到那个位置,就会出现严重的错误(例如,delete basePtr崩溃,因为你的代码覆盖了delete需要的东西)。但我们谈论的是未定义的行为,编译器/运行时系统在这里几乎可以做任何事情,"没有什么"在技术上是错误的,无论多么奇怪。如果代码决定打印带有1000个小数的圆周率,则播放音乐或崩溃。C和C++都有很多情况,规范中说"在这种情况下发生的事情是未定义的"。这在很大程度上是因为检测情况并做一些"有意义"的事情可能很难/很昂贵[1]。

请注意,这可能"不起作用":

#include <iostream>
using namespace std;
struct Base { };
struct Derived : Base { int y; void derivedMethod() { cout << "foo" << endl; y = 77; } };
int main()
{
Base b;
int x;
Base *basePtr = &b;
x = 42;
((Derived *)basePtr)->derivedMethod();
cout << "x=" << x << endl;
return 0;
}

现在,它可能在这里显示x = 77。也有可能没有。具体取决于编译器的作用。

[1] 例如,在一些处理器中,编译器必须添加50条额外的指令来检查错误。在另一个处理器上,这是一条额外的指令,所以还不错。但是,生产第一个需要50条额外指令来检查某些东西的处理器的公司肯定不希望检查这个错误。

因为此时此刻您有一个Derived*指针。编译器应该如何知道你没有?如果您想在运行时检查是否真的是这样,您必须使用dynamic_cast。如果像代码片段中那样重新解释将Base*强制转换为Derived*,则会得到未定义的行为,并且如果derivedMethod访问Derived的任何成员,则程序可能会崩溃。

class方法独立于实例化的对象存储,与对象的交互实际上只是与对象实现的接口的交互。因此,在需要访问类(属性)中的一些存储数据之前,调用类方法不会与类的实例化交互。

通过将基类指针强制转换为派生类指针,它现在假设引用的对象实现了派生类接口,并且非常乐意调用引用基类对象的派生类方法。

然而,正如其他人所说,这会导致未定义的行为。由于基类没有实现派生类的接口,因此可以假设数据存在于它实际上不存在的地方。在您的示例中,这两个对象即使在实例化时,在技术上也是抽象的,因为没有与它们相关联的数据。如果它们各自具有不同的属性,那么方法可能会引用错误的属性或完全无关的内存。