为啥可以从指向实例化基类对象的强制转换指针调用非静态派生类方法?

Posted

技术标签:

【中文标题】为啥可以从指向实例化基类对象的强制转换指针调用非静态派生类方法?【英文标题】:Why can a non-static derived class method be called from a casted pointer to an instantiated base class object?为什么可以从指向实例化基类对象的强制转换指针调用非静态派生类方法? 【发布时间】:2014-02-08 23:42:36 【问题描述】:

我不明白为什么下面的代码示例有效,希望得到一些澄清。在我看来,由于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++ 编码风格。这只是一个关于为什么我提供的代码示例有效的问题,因为它模仿了我在野外发现的代码。

【问题讨论】:

未定义的行为是未定义的,它会变得很奇怪:) @dreamlax,感谢您的建议。我刚刚尝试过,它编译并运行没有任何错误。 这是未定义的行为——它可以而且经常会做一些没有意义的事情。它不一定会崩溃,会导致错误。但同样重要的是,它很可能会导致崩溃、对其他事物的意外更改等等。它是未定义的,没有人说它应该做什么,由编译器编写者来“让它做某事”。 不幸的是,人们似乎出于各种原因对问题投了反对票。我相信在这种情况下,更多的是因为“您可以搜索该站点并找到有关此的某种信息”。但是,根据我的经验,甚至试图理解答案和问题都被否决的逻辑是完全没有意义的。例如,有人出于某种原因对我的回答投了反对票。 @Sam '相信你没看懂这个问题'我理解得很好!你真的不想开始烦恼;) ... 【参考方案1】:

当您使用强制转换时,您实际上是在对编译器说“将其设为Derived *”。因为编译器有义务做你所要求的[并且肯定有一些情况,使用更复杂的代码,你可能确实想要这样做,因为你知道指针确实是指向Derived的指针,只是目前你只有一个Base * 指针]。

然而,执行此操作的“正确”方法是使用dynamic_cast&lt;Derived *&gt;(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 位小数的 pi、播放音乐或崩溃。并且 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 条额外指令来检查某些东西的处理器的公司肯定不希望检查这个错误。

【讨论】:

【参考方案2】:

因为此时您有一个Derived* 指针。编译器怎么知道你没有?如果您想在运行时检查是否确实如此,您必须使用dynamic_cast。如果您将Base* 重新解释为Derived*,尽管就像在sn-p 中一样,您会得到未定义的行为,如果derivedMethod 访问Derived 的任何成员,程序可能会崩溃。

【讨论】:

@πάνταῥεῖ 我的意思是我问的唯一一个问题,也是我编辑的唯一一个问题:“为什么可以从指向实例化基类对象的强制转换指针调用非静态派生类方法?”。 【参考方案3】:

类方法的存储独立于实例化的对象,与对象的交互实际上只是与对象实现的接口的交互。所以调用类方法不会与类的实例化交互,直到它需要访问类中的一些存储数据(一个属性)。

通过将 Base 类指针转换为 Derived 类指针,它现在假定引用的对象实现了 Derived 类接口,并且非常乐意调用引用 Base 类对象的 Derived 类方法。

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

【讨论】:

这当然取决于编译器的实现。我同意所有已知的编译器确实以这种方式工作,但 C++ 标准并未规定它必须以这种方式工作。例如,可以将方法的代码存储在对象内。但在很多情况下会浪费很多空间...... ;) @MatsPetersson 甚至有可能指定像virtual void foo() = 0x00000042; 这样的硬连线函数调用条目并绑定一些函数体定义!

以上是关于为啥可以从指向实例化基类对象的强制转换指针调用非静态派生类方法?的主要内容,如果未能解决你的问题,请参考以下文章

将基类指针强制转换为子类指针后的疑惑

关于C++基类、派生类的引用和指针

用视图控制器类继承视图:新类会实例化基类的 UI 对象吗?

为啥指向基类的派生类指针可以调用派生类成员函数? [复制]

基类指针指向派生类对象&派生类指针指向基类对象

为啥我可以通过指向派生对象的基类指针访问派生私有成员函数?