一个由非虚函数导致的隐藏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++ 多态机制要求:
- 基类方法必须是虚函数(
virtual) - 通过基类指针或引用调用
当 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. 虚函数的使用原则
在基类中,如果方法可能被派生类重写并需要多态行为,应声明为虚函数:
- 析构函数:如果基类指针可能指向派生类对象,基类析构函数必须是虚函数
- 需要多态的方法:如
setVisible、onDraw、onEvent等
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;
};
技术要点
- C++多态机制:虚函数是实现运行时多态的基础
- 函数隐藏 vs 函数重写:非虚函数是隐藏,虚函数是重写
- 虚函数表(vtable):虚函数通过虚函数表实现动态绑定
- 性能考虑:虚函数调用有轻微性能开销,但在GUI框架中可接受
结论
这个bug源于对C++多态机制的理解不足。将 setVisible 改为虚函数后,问题得到解决。在面向对象设计中,需要多态的方法应声明为虚函数,这是基本设计原则。
关键词:C++多态、虚函数、函数隐藏、嵌入式GUI、FreeRTOS、窗口管理、面向对象设计