C++中的虚函数没有在子类中被调用

7

考虑这个简单的情况:

A.h

class A {
public:
    virtual void a() = 0;
};

B.h

#include <iostream>

class B {
public:
    virtual void b() {std::cout << "b()." << std::endl;};
};

C.h

#include "A.h"
#include "B.h"

class C : public B, public A {
public:
    void a() {std::cout << "a() in C." << std::endl;};
};

int main() {
    B* b = new C();
    ((A*) b)->a();    // Output: b().

    A* a = new C();
    a->a();           // Output:: a() in C.

   return 0;
}

换句话说:
- A 是一个纯虚类。
- B 是一个没有超类和一个非纯虚函数的类。
- C 是 A 和 B 的子类,并覆盖了 A 的纯虚函数。
令我惊讶的是第一个输出,即:
((A*) b)->a();    // Output: b().

尽管在代码中调用了a(),但最终执行的是b()。我的猜测是因为变量b是指向B类的指针,该类并不是A类的子类。但运行时类型仍然是指向C实例的指针。

从Java的角度来看,这种奇怪的行为背后的确切C++规则是什么?


2
简单来说:B不是A!它们完全没有关系,但你(不好使用!)C风格的转换无论如何都不会在意。dynamic_cast将正确地遍历您的层次结构。当您转换不相关的指针类型时,您会得到未定义的行为。这意味着任何事情都可能发生,从看起来工作正常到炸毁您的计算机。 - GManNickG
不要忘记鼻妖。 - Anon.
8个回答

24
你正在使用C样式转换b无条件地转换为A*类型。编译器不会阻止你这么做;你说它是一个A*,那么它就是一个A*。因此,它将指向的内存视为A实例。由于a()是在A的虚表中列出的第一个方法,而b()是在B的虚表中列出的第一个方法,当你在一个实际上是B的对象上调用a()时,你会得到b()
你很幸运,因为对象的布局相似。但不能保证一定如此。
首先,你不应该使用C样式转换。你应该使用C++转换运算符,这些运算符更加安全(但你仍然可能踩到坑里,所以请仔细阅读文档)。
其次,除非你使用dynamic_cast<>,否则你不应该依赖这种行为。

如果使用dynamic_cast,您可以完全依赖于交叉转换。它百分之百保证可行,否则退款。 - coppro
如果你使用dynamic_cast,是的。我试图强调不要依赖于C风格转换可以正常工作。我更新了我的最后一句话,以使其更清楚。 - i_am_jorf

11

在跨越多重继承树进行类型转换时,不要使用 C 风格的强制类型转换。如果使用 dynamic_cast,则会得到预期的结果:

B* b = new C();
dynamic_cast<A*>(b)->a();

@tehMick,这将是未定义的行为。 - Trent
2
它进行运行时类型检查。并且它看到bC的一个实例,而C继承自A - Anon.
@jeffamaphone:没错,一旦尝试在空对象上调用a(),就会导致运行时错误。 - Eric Mickelsen
1
请注意,dynamic_cast<>仅在启用运行时类型信息(/GR在Windows编译器上)的情况下才有效。 - i_am_jorf
1
@tehMick,dynamic_cast 知道对象类型是 C。由于 C 派生自 A,因此它能够进行转换,而无需运行时类型检查找到任何可抱怨的内容。 - Mark Ransom
显示剩余5条评论

5

您正在使用B*开始并将其转换为A*。由于这两者不相关,所以您正在涉及未定义行为的领域。


2
AB之间没有继承关系,这意味着指向B的指针不能通过向上或向下转换变为指向A的指针。
由于ABC的两个不同的基类,所以你正在尝试进行一次交叉转换。在C++语言中,唯一能执行交叉转换的转换是dynamic_cast。如果您确实需要它(需要吗?),则必须在此情况下使用它。
B* b = new C(); 
A* a = dynamic_cast<A*>(b);
assert(a != NULL);
a->a();    

2

((A*) b) 是一种显式的C风格转换,无论指向的类型是什么都是允许的。但是,如果您尝试取消引用此指针,则将出现运行时错误或不可预测的行为。这是后者的一个实例。您观察到的输出绝不安全或保证。


1
以下行是一个reinterpret_cast,它指向相同的内存,但“假装”它是不同类型的对象:
((A*) b)->a();

你真正想要的是 dynamic_cast,它可以检查 b 真正的对象类型并调整指向内存中的位置。
dynamic_cast<A*>(b)->a()

正如jeffamaphone所提到的,这两个类的相似布局导致了调用错误的函数。

1
在C++中,几乎从来没有一种情况需要或者正当使用C-style cast(或其C++等价的reinterpret_cast<>)。每当你发现自己想要使用其中之一时,应该怀疑你的代码和/或设计。

0

我认为你在从B*转换为A*时存在一个微妙的错误,并且行为是未定义的。避免使用C风格的转换,而是优先使用C++转换 - 在这种情况下使用dynamic_cast。由于编译器对数据类型和虚函数表条目的存储方式,你最终找到了不同函数的地址。


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