一个由非虚函数导致的隐藏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、窗口管理、面向对象设计

相关推荐
Tony Bai22 分钟前
高并发后端:坚守 Go,还是拥抱 Rust?
开发语言·后端·golang·rust
wjs202442 分钟前
Swift 类型转换
开发语言
秃了也弱了。1 小时前
python实现定时任务:schedule库、APScheduler库
开发语言·python
weixin_440730501 小时前
java数组整理笔记
java·开发语言·笔记
Thera7772 小时前
状态机(State Machine)详解:原理、优缺点与 C++ 实战示例
开发语言·c++
niucloud-admin2 小时前
java服务端——controller控制器
java·开发语言
夏幻灵3 小时前
JAVA基础:基本数据类型和引用数据类型
java·开发语言
cike_y3 小时前
Spring-Bean的作用域&Bean的自动装配
java·开发语言·数据库·spring
十八度的天空4 小时前
第01节 Python的基础语法
开发语言·python
yue0084 小时前
C# 字符串倒序
开发语言·c#