直接用 std::stack 实现 Undo/Redo 易出错,因命令对象生命周期与执行状态不一致:裸指针导致野指针访问、外部状态未完全逆向引发漂移;核心要求命令自包含、值语义保存快照、weak_ptr 避免循环引用、双栈管理 redo、幂等性控制重复执行。

为什么直接用 std::stack 实现 Undo/Redo 容易出错
因为命令对象生命周期和执行状态不一致:比如 Command 持有原始指针或引用,在撤销时对象已被销毁,redo() 就会访问野指针;或者命令执行后修改了外部状态,但 undo() 未完全逆向,导致状态漂移。
核心矛盾在于——命令必须是「自包含」的:它得记录足够的上下文(如修改前值、目标地址、操作类型),而不是依赖外部变量实时读取。
- 避免裸指针:用
std::shared_ptr或值语义(如int、std::string)保存快照数据 - 禁止在
execute()中修改非局部可变状态(如全局变量、单例内部状态),除非你同时在undo()中精确还原 -
Command析构前必须确保所有资源已释放,尤其涉及动态分配或文件句柄时
Command 基类怎么设计才支持安全撤销
基类只定义接口,不管理内存,也不做任何具体操作。重点是让派生类能明确区分「执行」「撤销」「重做」三阶段的边界。
典型错误是把 execute() 和 undo() 写成互逆函数却不校验前置条件,比如对空指针调用 undo() —— 应该在 canUndo() 中提前判断。
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
virtual void redo() { execute(); } // 默认重做即再执行
virtual bool canUndo() const { return true; }
virtual bool canRedo() const { return true; }
};
- 不要在基类里存
std::stack,那是控制器的事;基类只管“自己能不能被撤销” - 派生类构造时应完成所有必要数据捕获(例如
TextEditorDeleteCommand(std::string old_content, size_t pos, size_t len)) - 如果命令依赖外部对象(如
Document*),建议用weak_ptr避免循环引用
UndoManager 如何防止 Redo 栈在 Undo 后失效
常见 bug 是每次 undo() 后清空 redo_stack,但没考虑用户可能连续 undo 多次再 redo —— 这时 redo 栈应保留「从当前点往后所有已撤销过的命令」。
正确做法是双栈结构:undo_stack 存待撤销命令,redo_stack 存刚被 undo 的命令;每次 undo() 弹出一个并压入 redo_stack,每次 redo() 弹出一个并压回 undo_stack。
- 执行新命令时,必须清空
redo_stack(否则用户编辑后点 redo,会误恢复旧操作) - 检查栈空再操作,
if (!undo_stack.empty()) { ... },否则top()或pop()会崩溃 - 用
std::stack<:unique_ptr>>管理所有权,避免裸指针泄漏
实际写一个 TextEditorInsertCommand 示例要注意什么
插入操作看似简单,但 undo 时不能只删最后几个字符——得按原位置删,否则光标错位、多线程下内容错乱。
关键点:命令必须记录「插入位置」和「插入内容」,而不是当前文档内容快照(太重),也不是仅靠长度推算(不可靠)。
class TextEditorInsertCommand : public Command {
std::weak_ptr<textdocument> doc_;
size_t pos_;
std::string text_;
public:
TextEditorInsertCommand(std::shared_ptr<textdocument> doc, size_t pos, std::string text)
: doc_(doc), pos_(pos), text_(std::move(text)) {}
void execute() override {
if (auto d = doc_.lock()) {
d->insert(pos_, text_);
}
}
void undo() override {
if (auto d = doc_.lock()) {
d->erase(pos_, text_.length()); // 精确按原位置删除
}
}
};
</textdocument></textdocument>
- 不用
std::string存整个文档快照,只存差异部分(节省内存、避免拷贝大文本) - 用
std::weak_ptr防止 Command 持有 Document 导致其无法析构 - 插入位置
pos_必须是执行前的状态,不能在execute()里重新计算(比如用cursor_pos()),否则 undo 时位置偏移
最常被忽略的是命令的幂等性:同一个 Command 实例不应被 execute() 多次,否则插入会重复。应在 execute() 开头加标记或用状态机控制执行次数。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











