command模式实现undo/redo的关键在于:命令须携带完整上下文(如deletecommand保存被删子串)、栈用std::unique_ptr管理生命周期、undo/redo必须可逆且不依赖外部状态,且execute成功后才入栈。

Command 模式实现 Undo/Redo 不是“套个接口就能跑”,关键在三点:命令是否携带足够上下文、撤销栈和重做栈的生命周期管理是否严格、undo() 和 redo() 是否真正可逆且不依赖外部状态。直接上能编译运行的最小可行结构,避开 Qt 或 MFC 封装层干扰。
用 std::stack<:unique_ptr>></:unique_ptr> 管理命令生命周期
裸指针或原始 new + delete 极易内存泄漏,尤其在频繁 undo/redo 场景下。栈中存 std::unique_ptr<command></command> 是底线要求。
常见错误现象:Segmentation fault 出现在第二次 undo() 后,本质是某次 undo 执行后又重复释放了同一块内存。
-
Command基类析构函数必须为virtual ~Command() = default;,否则派生类资源不会被正确清理 - 每次
execute()成功后,才把新命令push到m_undoStack;失败则直接丢弃该unique_ptr,不入栈 -
m_redoStack在执行新命令时必须清空——用户一旦开始新操作,所有“未来”重做路径即失效
InsertCommand 和 DeleteCommand 必须保存完整上下文
只记位置和文本内容不够。比如 deleteText(5, 3),若原字符串长度不足 8,执行会越界;但撤销时若只靠 m_position 和 m_length 直接调用 insertText(5, "abc"),可能因中间其他操作导致位置偏移而插错地方。
所以每个具体命令必须捕获「执行前快照」:
-
InsertCommand保存插入前的m_text.substr(m_position, 0)(即空字符串)没意义,应保存插入点前后各 1–2 个字符,或更稳妥地——保存整个旧文本 + 插入位置 + 插入内容 -
DeleteCommand必须显式保存被删子串(m_deletedText),不能只靠m_text.substr(pos, len)在undo()时现场计算——因为m_text已被改过 - 所有成员变量用
const或std::string值语义,避免引用或指针悬空
为什么 redo() 不等于再调用一次 execute()
表面看两者逻辑一致,但实际场景中极易出错。例如旋转操作:rotate(45) 执行后对象朝向是 45°,redo() 应恢复到 45°,而不是再加 45° 变成 90°。
根本原因在于:命令对象封装的是「目标状态」或「差分操作」,不是「增量动作」。
- 若命令表达的是绝对状态(如
setState("saved")),则redo()和execute()可复用 - 若命令表达的是相对变化(如
incrementCounter()),则redo()必须等价于execute(),而undo()必须是其严格逆操作(decrementCounter()) - 多数编辑器操作属后者,因此
InsertCommand::redo()就是再插入一遍,DeleteCommand::redo()就是再删除一遍——前提是删除位置和长度在当前状态下依然有效;否则需校验或抛异常
CommandManager 的 canUndo() 和 canRedo() 别只看栈空不空
仅检查 m_undoStack.empty() 是常见疏漏。真实系统中,用户可能执行了 100 步操作,但只允许保留最近 50 步历史——这时 m_undoStack.size() 是 50,但第 1 步已不可达。
更隐蔽的问题是:某次 execute() 失败(如磁盘满导致保存失败),但命令仍被压入栈,后续 undo() 会尝试撤销一个未真正生效的操作,引发状态不一致。
- 建议在
executeCommand()内部加返回值:bool表示是否真实影响了接收者状态;仅当为true才入栈 -
canUndo()应同时检查栈非空 且 栈顶命令的execute()曾成功返回 - 调试时在
undo()前打印m_undoStack.top()->m_debugId(每条命令带自增 ID),能快速定位“撤销了谁却没反应”的问题
InsertCommand 对象里多存 20 字节的快照,可能省掉你三天调试光标跳错位置的排查时间。C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











