C++多继承下运行时多态调用异常:调用非预期重写函数
问题分析与解决方案
核心问题原因
你用C风格强制转换(Visibility*)pRect直接把BaseNode*转成Visibility*是完全错误的。在多继承场景下,Rectangle对象的内存布局里,BaseNode和Visibility的虚表指针位置并不重叠,C风格强制转换不会自动计算并调整指针的偏移量,导致调用Draw()时访问到了错误的虚表条目,这才出现了执行BasicFunc()的诡异现象——本质是未定义行为,只是刚好表现成了这个结果。
正确解决方法
方法1:用dynamic_cast做安全跨类型转换
C++专门提供了dynamic_cast来处理多继承下的类型转换,它会自动计算正确的指针偏移,还能在转换失败时返回nullptr(针对指针类型),是最安全的方式:
#include <iostream> class BaseNode { public: virtual void BasicFunc() { std::cout << "BaseNode BasicFunc()" << std::endl; } }; class Visibility { public: virtual void Draw() { std::cout << "Visibility Draw()" << std::endl; } }; class Rectangle : public BaseNode, public Visibility { public: void BasicFunc() override { // 加override明确标记重写,避免拼写错误 std::cout << "Rectangle BasicFunc()" << std::endl; } void Draw() override { std::cout << "Rectangle Draw()" << std::endl; } }; int main() { BaseNode* pRect = new Rectangle; if (auto visPtr = dynamic_cast<Visibility*>(pRect)) { visPtr->Draw(); // 正确调用Rectangle::Draw() } else { std::cout << "转换失败:当前节点不支持Visibility接口" << std::endl; } delete pRect; return 0; }
注意:dynamic_cast要求基类至少有一个虚函数(你的代码已经满足),且编译器需要开启RTTI(绝大多数编译器默认开启)。
方法2:基于接口设计的无RTTI方案(更贴合Godot引擎风格)
如果想彻底避免类型转换,同时摆脱冗长的switch判断,可以把Visibility设计成纯虚接口,然后在BaseNode中提供一个获取该接口的虚方法:
#include <iostream> // 纯虚接口:所有可绘制节点必须实现Draw class Visibility { public: virtual void Draw() = 0; virtual ~Visibility() = default; // 基类必须加虚析构,避免内存泄漏 }; class BaseNode { public: virtual void BasicFunc() { std::cout << "BaseNode BasicFunc()" << std::endl; } // 虚方法:返回当前节点的Visibility接口(默认返回nullptr表示不支持) virtual Visibility* AsVisibility() { return nullptr; } virtual ~BaseNode() = default; }; class Rectangle : public BaseNode, public Visibility { public: void BasicFunc() override { std::cout << "Rectangle BasicFunc()" << std::endl; } void Draw() override { std::cout << "Rectangle Draw()" << std::endl; } // 重写接口获取方法,返回自身的Visibility指针 Visibility* AsVisibility() override { return this; } }; int main() { BaseNode* pRect = new Rectangle; if (auto visPtr = pRect->AsVisibility()) { visPtr->Draw(); // 正确调用 } delete pRect; return 0; }
这种模式不需要依赖RTTI,是游戏引擎中常用的接口设计思路,能优雅地替代类型判断的switch逻辑。
方法3:确定类型时用static_cast(仅限100%确定类型的场景)
如果你能保证pRect一定指向Rectangle对象,可以先把BaseNode*转成Rectangle*,再转成Visibility*——static_cast会处理多继承的指针偏移:
static_cast<Visibility*>(static_cast<Rectangle*>(pRect))->Draw();
但这种方式完全不安全,如果指针指向的不是Rectangle,会直接触发未定义行为,所以仅适合类型绝对明确的场景。
关于你测试现象的解释
- 移除
Visibility::Draw()的virtual关键字后调用Visibility::Draw():此时Draw()不是虚函数,编译器会直接根据指针类型调用函数,C风格转换后的指针虽然偏移错误,但刚好能访问到Visibility的成员函数(本质还是未定义行为,只是巧合符合预期)。 void*调用函数执行第一个继承类的对应函数:void*转成其他类型指针时不会调整偏移,指针指向的是BaseNode部分的虚表,调用函数时会匹配虚表的第一个条目,也就是BasicFunc()。
内容的提问来源于stack exchange,提问作者CreeperCrafter979
相关产品推荐
相关产品推荐

