You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 20:55:32