【问题标题】:incorrect behaviour while downcasting c++向下转换 C++ 时的错误行为
【发布时间】:2013-07-26 18:05:24
【问题描述】:

这是我编写的一段代码,用于查看向下转换期间的行为。

#include <iostream>
using namespace std;

class base {
public :
    void function()
    {
        cout << "\nInside class Base";
    }
};

class derived : public base {
public :
    void function()
    {
        cout << "\nInside class Derived.";
    }
};

int main()
{
    base * b1 = new base();
    base * b2 = new derived();
    derived * b3 = (derived*)b1 ;
    b1 -> function();
    b2 -> function();
    b3 -> function(); // print statement 3
    static_cast<derived*>(b2) -> function();
    static_cast<derived*>(b1) -> function(); // print statement 5
    return 0;
}

输出如下。

Inside class Base
Inside class Base
Inside class Derived.
Inside class Derived.
Inside class Derived.

我觉得 print statement 3 和 print statement 5 应该显示“Inside class base”。

有人可以解释一下我在这里可能缺少什么吗?

【问题讨论】:

  • 你想要virtual函数。通常,函数是根据调用它的静态(编译时)类型来选择的(即,你在 derived* 上调用它,你会得到 derived 版本)。如果您创建函数virtual,您将根据需要获得“实际”(或动态)类型的版本。例如见herehere

标签: c++ inheritance downcast


【解决方案1】:

两者都是未定义行为的情况。将 b1 转换为 derived* 无效。

但是,如果您说base* b1 = new derived(),您将有相同的行为。由于这两个函数都没有标记virtual,所以它只在编译时检查对象类型。

所以第一种情况会打印“Inside class Base”,尽管它实际上是一个派生指针。

【讨论】:

  • 看起来3和5都是UB。它们几乎相同。
  • 对此不确定,但我有一种暗示,因为它们是标准布局类(实际上是空的,尽管这并不意味着sizeof(base)==0derived)。 IE。在某些情况下,您可以使用令人讨厌的强制转换来做每个人在 c 中所做的事情。
  • @BoBTFish:虽然它在这种情况下有效,但主要出于您陈述的原因,它不必:5.2.9p11 ...如果类型为“指向 cv1 的指针”的纯右值B” 指向实际上是 D 类型对象的子对象的 B,生成的指针指向 D 类型的封闭对象。否则,强制转换的结果是未定义的。
  • 我花了一些时间来挖掘标准以找到正确的术语,但我将直接引导您到 this,并提及“布局兼容类型”这句话. (虽然它提到了reinterpret_cast,而不是static_cast。)
  • @BoBTFish:啊,我不知道他们说两种布局兼容类型的reinterpret_cast 是有效的。所以它不一定像乍一看那样不正确。
【解决方案2】:

您需要将基本方法void function() 定义为virtual

virtual void function()
{
    cout << "\nInside class Base";
}

结果输出是:

Inside class Base
Inside class Derived.
Inside class Base
Inside class Derived.
Inside class Base

在 OP 中,第 5 种情况可能不是参考1 中所述的未定义行为,并且内联成员函数内存不会像 here 中所述的数据成员那样存储,以确保在静态转换为派生类型后,派生成员函数被称为:

任何标准转换序列的倒数(第 4 条)不是 包含一个左值到右值(4.1),数组到指针(4.2), 函数指针 (4.3)、空指针 (4.10)、空成员指针 (4.11) 或布尔 (4.12) 转换可以显式执行 使用 static_cast。

1 工作草案,C++ 编程语言标准,5.2.9 静态转换 - 7

【讨论】:

  • 我知道如果你声明它为虚拟的,那么可以看到上面的输出。但我怀疑派生类函数根本不应该存在于 b1 的内存布局中。那么如何调用内存中不存在的函数呢?
【解决方案3】:

函数在编译时调度,仅基于静态类型,因为它们不是虚拟的。

print 语句中 b3 的静态类型是 Derived *,因此是“Inside class Derived”。打印语句 5 将 Base * 转换为 Derived *,因此打印输出相同。

在 Base 中的 function() 定义中添加 virtual 并再次检查以查看会发生什么。

【讨论】:

  • 但是 b1 应该只包含显示“Inside class base”的函数,因为它指向一个 base 类型的对象。派生类函数根本不应该存在于 b1 的内存布局中。那怎么可能指向呢?
【解决方案4】:

这是预期的行为。
非虚方法由对象的类型在编译时调用。
您使用了静态转换,因此编译器将其视为“派生”类。

如果您将方法声明为virtual,那么它将为该函数创建一个虚拟查找表,并根据实际的run-time类型调用该方法。

【讨论】:

  • 就我个人而言,我希望它会打印出紫色的独角兽,或者其他任何感觉:即使您所描述的与许多实现一致,预期的未定义行为也具有误导性。
  • @Yakk 只有第 3 行定义不明确。但它真正做的是编译器会将对象视为您所说的对象(就像 reinterpert_cast 一样)。因为对象是相关的,编译器将添加正确的偏移量以到达派生类的基类。然后它将调用派生类的函数。因为任何一个类都没有成员偏移量为 0。
  • @Yakk 仅仅因为它在 C++ 标准中没有很好地定义并不意味着如果你知道编译器是如何工作的,它就不是预期的行为。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-26
相关资源
最近更新 更多