从基类到派生类的 `dynamic_cast`

14

是的,我知道使用 dynamic_cast 进行向下转型如果 Base 不具备多态性将无法编译,但我的问题并不是关于这个。

class Base {
    public:
        virtual void bar()
        {
            cout << "bar\n";
        }
};

class Derived: public Base {
    public:
        void foo()
        {
            cout << "foo\n";
        }
};

int main()
{
    Base *pb;
    Derived *pd;

    pb = new Derived;  //Base* points to a Derived object
    pd = dynamic_cast<Derived*>(pb); 
    pd->foo();  //outputs foo

    pb = new Base;  //Base* points to a Base object
    pd = dynamic_cast<Derived*>(pb);  
    pd->foo();  //outputs foo, too. Why?
}

我原以为当 pb = new Derived; 时,pb 实际上指向堆中的一个 Derived 对象。在执行 pd = dynamic_cast<Derived*>(pb); 后,pd 也指向该 Derived 对象,因此 pd->foo() 应该是可以的。

但是当 pb = new Base; 时,pb 指向堆中的一个 Base 对象,那么在执行 pd = dynamic_cast<Derived*>(pb); 后,pd->foo() 怎么可能起作用呢?难道 dynamic_cast 将堆中的 Base 对象变成了一个 Derived 对象吗?

4个回答

20
在C++中,每个类的实例都有自己的数据类型版本,但是除了内联函数之外,所有类在内存中共享相同的函数。在您的情况下,当您说像这样的话时:
pd->foo();

你本质上是在调用Derived::foo,它是内存中的一个函数,编译器知道它在哪里。问题是,它与pd没有任何关系。不过,如果你有这样的代码:

class Derived : public Base {
    private:
        int a;

    public:
        Derived() { a = 100; }

        void foo() {
            std::cout<<a<<std::endl;
        }
 };

那么,pd->foo() 将导致一个分段错误。在这里,您的动态转换失败了,当调用 Derived::foo 时,将 0 作为 this 对象传递。在之前的情况下,由于从未使用 this 对象,所以它很好。然而,在第二种情况下,它被使用并因此导致了分段错误。


当我说pd->foo();时,无论pd是否为NULLfoo()都会被调用吗? - Alcott
@Alcott 是的,但是 this 参数将会被传递为 NULL(因为 pd 的值就是 NULL)。因此,在 Rohan 的例子中,当你访问 a 时,会出现预期的崩溃。 - littleadv
1
它在很大程度上取决于编译器,正如@Luchian Grigore所提到的,任何事情都可能发生。因此,在大多数情况下是可以的,但这不是你可以依赖的东西。 - Rohan Prabhu
根据您的回答,您实际上是在调用Derived::foo与pd无关,因此如果pd == NULL,那么pd->foo()应该就像NULL->foo()一样,对吗?那么编译器怎么知道我想调用Derived::foo()而不是其他任何foo()呢?只是因为pd的类型是Derived *吗? - Alcott
是的。方法绑定到它们所属的类类型,而不是实例。可以这样想。当您进行像 pd->foo() 这样的调用时,编译器执行的是类似于 Runtime.invoke(Derived.foo, pd) 的操作。这并不是真正发生的事情,但我试图展示的是,方法的查找是基于类型而不是实例完成的。实例变得只是传递给方法的“参数”,它可能是 null - Rohan Prabhu

8
在你的foo中,你没有访问this,在这种情况下应该是NULL。当转换无法完成时,dynamic_cast返回的就是这个值。
基本上,你现在处于“未定义行为”的领域。

我+1了你的答案,因为它是正确的。但是,我仍然不明白这样的做法如何是幸运的。你可能会遇到一些难以追踪的恶意程序错误,因为程序没有在应该崩溃的时候崩溃。 - Luchian Grigore
1
@LuchianGrigore Luck(n)1. 指某人偶然发生的事情,偶然事件。 - Potatoswatter

7

您遇到了未定义的行为。您应该检查dynamic_cast的返回类型。

pd = dynamic_cast<Derived*>(pb);  

这会返回null,并且你在一个NULL指针上调用函数。任何事情都有可能发生。


@Alcott 这是未定义行为。任何事情都有可能发生。 - Luchian Grigore
1
@Alcott 从技术上讲,Luchian的回答是正确的。但实际上,原因是编译器开发人员更喜欢忽略这个问题,因为他们被允许这样做(因为标准没有定义在这种情况下该怎么做),而不是在每次指针访问时实现复杂和性能消耗的运行时检查。因此,无论发生什么事情-都是正确的。 - littleadv
@Alcott 所有未定义行为都是不要这么做。 - Luchian Grigore
@Alcott 写调用未定义行为的代码?是的,确实如此。编译器不负责教你语言 :) 编译器必须遵守标准,而标准并没有定义在这种情况下该怎么做,因此从编译器的角度来看,它可以自由地执行任何操作。作为程序员,你有责任在可能出现 NULL 的地方检查 NULL。 - littleadv

1

在尝试使用强制转换之前,请始终优先检查转换是否成功。我想这是使用强制转换的优点。您可以检查它是否成功。

pd = dynamic_cast<Derived*>(pb);  
if(pd!=NULL)
  pd->foo();

如果转换失败,pd 的值为 NULL。除非您确定它有值,否则不要使用 pd。即使有值,也只能对其进行解引用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接