用栈实现Undo/Redo的核心是用std::stack存Command对象(含execute/undo/redo虚函数),Undo弹出执行undo()并压入Redo栈,Redo反之;需用unique_ptr管理内存、weak_ptr或值语义避免野指针,Qt中优先用QUndoStack。

用栈保存状态快照是最直接的 Undo/Redo 实现方式
核心思路是:每次用户操作后,把当前状态(或操作本身)压入 std::stack;Undo 就是弹出栈顶、恢复前一状态;Redo 则需要另一个栈暂存被撤销的操作。不推荐深拷贝整个对象树——开销大且易出错;更轻量的做法是只记录「变化差量」,比如 std::vector<:pair std::any>></:pair> 记录字段名和新值,或者封装成 Command 对象。
常见错误现象:std::stack 默认没有迭代器,无法预览历史;误用 std::queue 导致顺序颠倒;忘记在执行 Undo 后清空 Redo 栈(否则 Redo 会重放旧操作而非新操作)。
实操建议:
- 定义
class Command,含execute()、undo()、redo()三个虚函数,子类按需实现(如DeleteNodeCommand或MovePointCommand) - 用
std::stack<:unique_ptr>></:unique_ptr>存 Undo 历史,用std::stack<:unique_ptr>></:unique_ptr>存 Redo 历史 - 每次调用
execute()后,将该命令 push 到 Undo 栈,并清空 Redo 栈 - Undo 操作:pop Undo 栈 → 调用其
undo()→ push 到 Redo 栈 - Redo 操作:pop Redo 栈 → 调用其
redo()→ push 到 Undo 栈
避免内存泄漏的关键:Command 必须管理自身资源生命周期
很多初学者让 Command 持有原始指针(如 Node*),结果 Undo 时对象已被 delete,触发野指针访问;或者用 shared_ptr 却没考虑循环引用(比如命令里存了场景管理器的 shared_ptr,而场景又持有命令列表)。
立即学习“C++免费学习笔记(深入)”;
实操建议:
- 所有数据引用优先用值语义或
weak_ptr;若必须持原始指针,确保其生命周期长于 Command - Command 构造时捕获「操作前状态」,例如保存被删除节点的 ID 和父节点 ID,而不是节点指针
- 用
std::unique_ptr管理 Command 实例,保证析构自动释放;不要在栈上创建 Command 对象再取地址塞进栈 - 测试边界:连续 Undo 到栈空后继续 Undo —— 应静默失败,不崩溃
Qt 场景下可直接复用 QUndoStack 和 QUndoCommand
如果你在写 Qt GUI 应用,别自己造轮子。QUndoStack 内置了分组、文本描述、clean 标记、信号通知(如 canUndoChanged),还支持 beginMacro/endMacro 合并多个操作为单次 Undo 步骤。
实操建议:
- 继承
QUndoCommand,重写redo()和undo();构造函数传入text(用于菜单显示) - 调用
QUndoStack::push(new MyEditCommand(...)),它会自动管理内存并触发信号 - 连接
QUndoStack::canUndoChanged到QAction::setEnabled,实时更新菜单项状态 - 注意:
QUndoCommand的id()返回 -1 表示不可合并;若想支持 Ctrl+Z 连续修改同一字段时自动合并,需返回固定非负值并重写mergeWith()
性能敏感场景要限制 Undo 步数并避免深拷贝
图像编辑器或 CAD 类应用,一次操作可能涉及 MB 级像素或几何数据,全量快照必然 OOM。此时「记录差异」比「保存状态」更实际,但代价是 undo() 逻辑变复杂。
实操建议:
- 用
QUndoStack::setClean()或自定义 clean 标记,区分“已保存”和“已修改”状态 - 限制最大步数:每次
push()前检查size() > MAX_UNDO,则 pop 底部元素(可用std::deque替代 stack 实现两端操作) - 对大对象(如
std::vector<float></float>)只存 delta:例如用std::vector<:pair float>></:pair>记录被改写的索引和旧值 - 避免在
Command构造函数里做耗时计算(如序列化整个场景),改到execute()中按需生成


















