备忘录模式的核心是封装不可变状态快照于memento对象中,由originator创建/恢复、caretaker持有而不窥探;memento需为originator友元、值语义存储、显式支持拷贝、避免裸指针与资源句柄。

备忘录模式的核心不是“存文件”,而是对象状态的内存快照
直接用 std::ofstream 写二进制或 JSON 不算备忘录模式——那是序列化。备忘录模式的关键在于:状态封装在独立、不可变的 Memento 对象里,原对象(Originator)只负责创建和恢复,而管理者(Caretaker)只负责持有,不窥探内容。
常见错误是把 Memento 设成 public 成员结构体,或者让 Caretaker 直接访问其字段——这破坏了封装,也失去了模式存在的意义。
-
Memento类必须是Originator的友元(或通过私有接口构造),外部无法构造或修改 - 状态字段应尽量精简,只存真正需要回滚的成员变量,避免拷贝整个对象或指针
- 不要在
Memento里存裸指针或依赖外部生命周期的对象(比如std::shared_ptr可以,但int*很危险)
如何写一个线程安全、可拷贝的 Memento 类
多数教程忽略的一个现实问题:C++ 中 Memento 常被塞进 std::vector<memento></memento> 或传给异步任务,所以它得支持拷贝甚至移动语义;同时如果 Originator 状态含互斥量或非 POD 类型,不能简单 memcpy。
正确做法是让 Memento 自己管理状态副本:
- 用值语义存储:优先选
std::string、std::vector<int></int>、std::optional<t></t>,而非const char*或引用 - 禁止默认拷贝构造/赋值时隐式调用浅拷贝——显式定义它们,或用
= default(前提是所有成员都可拷贝) - 若
Originator含std::mutex或std::thread,这些绝不能进Memento;状态快照只应包含数据,不包含资源句柄
示例片段(关键部分):
class Originator {
private:
std::string data_;
int version_;
public:
class Memento {
friend class Originator;
std::string data_;
int version_;
Memento(std::string d, int v) : data_(std::move(d)), version_(v) {}
public:
// 显式允许拷贝
Memento(const Memento&) = default;
Memento& operator=(const Memento&) = default;
};
Memento save() const { return Memento(data_, version_); }
void restore(const Memento& m) {
data_ = m.data_;
version_ = m.version_;
}
};
为什么不用 std::any 或 std::variant 存状态?
有人想偷懒,用 std::any 把整个 Originator 对象塞进去,或者用 std::variant<statea stateb></statea> 应对多态状态——这会带来三个实际问题:
- 类型擦除开销:每次
std::any_cast是运行时检查,且std::any内部可能堆分配 - 破坏编译期类型安全:你没法静态保证
restore()时状态结构没变,重构时容易静默出错 - 无法做增量快照:比如只想存
data_而忽略version_,std::any只能全存或全不存
真实项目中,状态字段往往随迭代增加。更稳妥的方式是把 Memento 设计为结构体,字段一一对应,并用注释标明“此字段用于 undo/redo 场景”,方便后续维护者理解取舍逻辑。
撤销栈(Caretaker)里存多少个 Memento 才合理?
没有固定答案,但常见误判是“越多越好”。实际上每个 Memento 都是一份完整状态拷贝,内存占用可能远超预期。
- 如果单个
Memento平均占 1KB,存 1000 个就是 1MB——对嵌入式或高频操作场景很敏感 - 建议设硬上限(如
max_history_ = 50),并用std::deque<memento></memento>管理,避免 vector 重分配时的临时双倍内存 - 更激进的优化:对连续小改动(比如文本编辑器里连打 10 个字符),可以合并成一个
Memento,而不是每键存一次
关键点在于:备忘录不是日志系统,它的目标是“可逆”,不是“可追溯”。只要用户按 Ctrl+Z 能回到上一步,中间过程是否精确还原,取决于业务需求,不是模式本身的要求。
最常被忽略的是深拷贝成本——比如 Memento 里存了一个 std::vector<bigstruct></bigstruct>,而 BigStruct 有 20 个 double 字段,这时候别只看代码行数,得跑 valgrind --tool=massif 或 VS 的内存分析器看看实际增长。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











