一个由非虚函数导致的隐藏Bug:窗口显示异常问题排查与解决

一个由非虚函数导致的隐藏Bug:窗口显示异常问题排查与解决

问题背景

在基于FreeRTOS的嵌入式GUI系统中,从充电界面切换回普通界面时,菜单窗口无法正常显示。虽然日志显示 setVisible(TRUE) 被调用,但菜单界面仍然不可见。

问题现象

通过日志分析发现:

复制代码
onWinCtrl: MenuWin - final setVisible(1)

onWinCtrl 中菜单窗口被设置为可见,但界面未显示。进一步调试发现,MenuWindow::setVisible() 中的关键逻辑(恢复子菜单状态)没有被执行。

问题分析

代码结构

cpp 复制代码
// BaseWindow.h
class BaseWindow {
public:
    void setVisible(boolean bShow);  // 注意:不是虚函数
    // ...
};

// MenuWindow.h
class MenuWindow : public BaseWindow {
public:
    void setVisible(boolean bVisible);  // 重写基类方法
    // ...
};

// MenuWindow.cpp
void MenuWindow::setVisible(boolean bVisible) {
    BaseWindow::setVisible(bVisible);
    
    if (!bVisible) {
        // 隐藏所有子菜单
        // ...
        return;
    }
    
    // 关键逻辑:恢复当前选中的子菜单
    MenuSubWindow* target = m_pCurrentSubWin;
    if (target == nullptr) {
        target = GetSubWindowByType(s_s32CurSelectionId);
        m_pCurrentSubWin = target;
    }
    
    if (target != nullptr) {
        target->ShowImmediate();  // 这个调用没有被执行!
    }
}

调用路径

MainWindow::onWinCtrl() 中:

cpp 复制代码
BaseWindow **pstWin = gstWinCtrlMap[s32Loop].pstWin;  // BaseWindow** 类型
// ...
(*pstWin)->setVisible(bShowFlag);  // 通过基类指针调用

根本原因

C++ 多态机制要求:

  1. 基类方法必须是虚函数(virtual
  2. 通过基类指针或引用调用

BaseWindow::setVisible() 不是虚函数时:

  • 通过 BaseWindow* 指针调用,会调用 BaseWindow::setVisible()
  • 即使子类重写了该方法,也不会被调用
  • 这是函数隐藏(Function Hiding),不是多态

技术细节

cpp 复制代码
// 情况1:非虚函数(当前代码)
BaseWindow* pWin = new MenuWindow(...);
pWin->setVisible(TRUE);  // 调用 BaseWindow::setVisible()

// 情况2:虚函数(修复后)
virtual void setVisible(boolean bShow);  // 基类声明为虚函数
BaseWindow* pWin = new MenuWindow(...);
pWin->setVisible(TRUE);  // 调用 MenuWindow::setVisible()

调试过程

1. 日志分析

发现 onWinCtrl 中调用了 setVisible(1),但菜单未显示。

2. 添加调试日志

MenuWindow::setVisible() 中添加日志:

cpp 复制代码
void MenuWindow::setVisible(boolean bVisible) {
    SKLOG_I(("MenuWindow::setVisible(%d) called", bVisible));
    // ...
}

结果:该日志未出现,说明方法未被调用。

3. 确认调用路径

检查 onWinCtrl 中的调用:

cpp 复制代码
(*pstWin)->setVisible(bShowFlag);  // pstWin 是 BaseWindow**

由于 setVisible 不是虚函数,实际调用的是 BaseWindow::setVisible(),而不是 MenuWindow::setVisible()

4. 验证假设

对比两种调用方式:

cpp 复制代码
// 方式1:通过基类指针(当前代码)
(*pstWin)->setVisible(bShowFlag);  // 调用 BaseWindow::setVisible()

// 方式2:通过子类指针(验证用)
gpstMenuWin->setVisible(bShowFlag);  // 调用 MenuWindow::setVisible()

方式2可以正常工作,证实了问题。

解决方案

方案1:将基类方法改为虚函数(推荐)

cpp 复制代码
// BaseWindow.h
class BaseWindow {
public:
    virtual void setVisible(boolean bShow);  // 添加 virtual 关键字
    // ...
};

优点:

  • 符合面向对象设计原则
  • 支持多态,所有子类都能正确重写
  • 一劳永逸,不需要特殊处理

方案2:特殊处理(临时方案)

cpp 复制代码
// MainWindow.cpp
if (*pstWin == (BaseWindow*)gpstMenuWin) {
    gpstMenuWin->setVisible(bShowFlag);  // 直接调用子类方法
} else {
    (*pstWin)->setVisible(bShowFlag);
}

缺点:

  • 需要为每个重写了 setVisible 的子类添加特殊处理
  • 代码维护性差
  • 不符合开闭原则

经验总结

1. 虚函数的使用原则

在基类中,如果方法可能被派生类重写并需要多态行为,应声明为虚函数:

  • 析构函数:如果基类指针可能指向派生类对象,基类析构函数必须是虚函数
  • 需要多态的方法:如 setVisibleonDrawonEvent

2. 调试技巧

  • 在关键方法入口添加日志,确认是否被调用
  • 对比不同调用方式的行为差异
  • 理解C++多态机制,区分函数隐藏和函数重写

3. 代码审查要点

  • 检查基类中可能被重写的方法是否声明为虚函数
  • 检查通过基类指针调用的方法是否支持多态
  • 使用 override 关键字明确重写意图,让编译器检查

4. 最佳实践

cpp 复制代码
// 推荐写法
class BaseWindow {
public:
    virtual void setVisible(boolean bShow);  // 虚函数
    virtual ~BaseWindow();  // 虚析构函数
};

class MenuWindow : public BaseWindow {
public:
    void setVisible(boolean bVisible) override;  // 使用 override 明确意图
    ~MenuWindow() override;
};

技术要点

  1. C++多态机制:虚函数是实现运行时多态的基础
  2. 函数隐藏 vs 函数重写:非虚函数是隐藏,虚函数是重写
  3. 虚函数表(vtable):虚函数通过虚函数表实现动态绑定
  4. 性能考虑:虚函数调用有轻微性能开销,但在GUI框架中可接受

结论

这个bug源于对C++多态机制的理解不足。将 setVisible 改为虚函数后,问题得到解决。在面向对象设计中,需要多态的方法应声明为虚函数,这是基本设计原则。


关键词:C++多态、虚函数、函数隐藏、嵌入式GUI、FreeRTOS、窗口管理、面向对象设计

相关推荐
灯澜忆梦10 小时前
GO_并发编程---定时器
开发语言·后端·golang
-银雾鸢尾-10 小时前
C#中的StringBuilder相关方法
开发语言·c#
-银雾鸢尾-11 小时前
C#中结构体与类的区别;抽象类与接口的区别
开发语言·c#
大模型码小白11 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
段一凡-华北理工大学13 小时前
向量数据库实战:选型、调优与落地~系列文章12:文本分块策略实战:chunk_size 怎么选?重叠多少?
开发语言·数据库·后端·oracle·rust·工业智能体·高炉智能化
Ljwuhe14 小时前
C++——多态
开发语言·c++
心平气和量大福大14 小时前
C#-WPF-Window主窗体
开发语言·c#·wpf
从零开始的代码生活_16 小时前
C++ 继承详解:访问控制、对象模型、菱形继承与设计取舍
开发语言·c++·后端·学习·算法
云小逸16 小时前
【C++ 第七阶段:模板、泛型编程与工程综合详解】
开发语言·c++
GIS阵地16 小时前
QgsSingleBandPseudoColorRenderer 完整详解(QGIS 3.40.13 C++)
开发语言·前端·c++·qt·qgis