命令基类必须声明虚析构函数,否则通过基类指针删除派生类对象将导致析构逻辑不执行、资源泄漏;撤销重做需清空重做栈、双向同步栈操作;命令须深拷贝图形数据,避免共享状态;信号连接需显式断开以防重复触发。

命令基类必须带虚析构,否则 delete 命令指针会内存泄漏
图形编辑器里命令对象常以 std::unique_ptr<command></command> 存在,如果基类 Command 没有虚析构函数,派生类(如 MoveShapeCommand、DeleteShapeCommand)的析构逻辑根本不会执行——资源没释放,临时状态没清理,撤销重做链一跑就崩。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 基类
Command析构函数声明为virtual ~Command() = default;,哪怕空实现也得加virtual - 避免用裸指针管理命令生命周期;统一用
std::unique_ptr<command></command>存入撤销栈std::stack或std::vector - 别在命令构造时直接修改画布;所有变更延迟到
execute(),确保撤销时能干净回退
撤销栈和重做栈要双向同步,不能只靠 pop/push 简单搬运
用户连续撤销两次再执行新操作,旧的重做项必须全部丢弃——这是图形编辑器最常出 bug 的地方。很多人写成“撤销时把命令从 undo 栈弹出、压进 redo 栈”,但漏掉“新操作进来时,redo 栈清空”这步,结果用户点几下重做,突然还原出十分钟前的废稿。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 定义明确状态:撤销栈
m_undoStack存待撤操作,重做栈m_redoStack只存刚被撤销的操作 - 新命令执行前,先调用
m_redoStack.clear()(或while (!m_redoStack.empty()) m_redoStack.pop();) - 撤销操作中,把
m_undoStack.top()调用undo()后,再移入m_redoStack;重做则反向,且重做后不保留原命令在 undo 栈
命令对象必须深拷贝关键数据,不能传引用或共享指针指向画布模型
比如拖动矩形命令,如果 MoveShapeCommand 里存的是 Shape* 或 std::shared_ptr<shape></shape>,那撤销时改回去的只是当前画布上那个对象的状态——而它可能已被其他命令修改过,或者已被删除。撤销变成“改一个已失效的地址”,结果不是崩溃就是画面错乱。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 命令构造时,对涉及的图形数据做深拷贝:位置、尺寸、颜色、锚点……所有影响渲染和交互的字段
- 用值语义,例如存
Rect m_oldBounds和Rect m_newBounds,而不是Shape& m_shape - 若图形数据过大(如含位图),可引入版本号 + 快照机制,但快照本身也要独立存储,不能依赖运行时画布实例
Qt 场景下 connect 信号到命令执行需断开旧连接,否则重复触发
图形编辑器常用 QGraphicsItem::itemChanged 或自定义信号触发命令生成。如果每次拖动都 new 一个 MoveShapeCommand 并 connect 到某个信号,但忘了 disconnect 上一个,就会导致一次鼠标松开触发 5 次 execute——撤销栈里塞进 5 个几乎一样的命令,重做时画面疯狂抖动。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 命令对象自己持有
QObject::connect()返回的QMetaObject::Connection,并在execute()后立即disconnect() - 更稳妥的做法:不在命令里绑定信号;由编辑器控制器统一监听、构造命令、执行,命令纯数据无副作用
- 测试时故意快速拖放几次再撤销,看是否出现“撤销一步却跳两帧”——这是连接泄漏的典型表现
命令模式真正难的不是结构,而是每个命令对“状态快照边界”的判断:哪些该存、哪些该算、哪些根本不能碰。画布缩放、图层可见性、选中状态……这些看似无关的上下文,一旦漏掉,撤销就变成薛定谔的还原。










