
1. 备忘录模式再探不只是“撤销”那么简单提到备忘录模式很多C开发者第一反应就是“哦那个用来做撤销功能的模式”。这个理解没错但太浅了。我干了十几年C从桌面应用到游戏引擎备忘录模式的应用场景远比一个“撤销”按钮丰富得多。它本质上解决的是一个核心矛盾如何在保持对象封装性的前提下安全地捕获和恢复其内部状态。这个矛盾在需要状态回滚、事务管理、状态快照对比甚至是游戏存档、配置管理、断点续传等场景下都会冒出来。为什么这个问题棘手因为良好的面向对象设计强调封装——把数据藏起来只通过公开的接口方法来操作。但当你需要把对象“拍个快照”保存下来过会儿再“时光倒流”回去时你就得想办法把那些私有的、受保护的数据一股脑儿复制出来。直接暴露所有内部变量那封装就形同虚设了类变得脆弱不堪任何依赖其内部结构的代码都会像蜘蛛网一样缠在一起牵一发而动全身。备忘录模式的精妙之处就在于它设计了一个“授权通道”让对象自己来决定如何暴露状态并且只暴露给一个它信任的“备忘录”对象。在C里实现这个模式我们有几个得天独厚的武器友元friend、嵌套类、const正确性、移动语义还有智能指针管理生命周期。用好这些特性你能写出既安全又高效的备忘录。这篇文章我们就抛开那些教科书式的定义从实战角度深入C的细节看看备忘录模式怎么玩出花来以及有哪些你绝对不想再踩第二次的坑。2. 核心角色与C实现剖析备忘录模式通常涉及三个核心角色原发器Originator、备忘录Memento和负责人Caretaker。在C的语境下我们可以给每个角色注入更具体的实现考量。2.1 原发器Originator状态的拥有者原发器是那个拥有宝贵状态、需要被保存的对象。它的职责很明确第一能生成自己状态的快照创建备忘录第二能根据一个备忘录恢复自己的状态。// 一个简单的文本编辑器类作为原发器 class TextEditor { private: std::string text_; int cursorX_; int cursorY_; std::vectorstd::string formattingStack_; // 比如粗体、斜体等格式状态 public: TextEditor() : text_(), cursorX_(0), cursorY_(0) {} void type(const std::string words) { text_.insert(cursorX_, words); cursorX_ words.length(); // ... 可能触发格式化更新 } void moveCursor(int x, int y) { cursorX_ x; cursorY_ y; } // 关键方法1创建备忘录快照 // 注意返回类型一个专属的、不透明的Memento类型 std::unique_ptrclass TextEditorMemento createMemento() const; // 关键方法2从备忘录恢复状态 void restoreFromMemento(const class TextEditorMemento memento); // 其他业务方法... const std::string getText() const { return text_; } std::pairint, int getCursor() const { return {cursorX_, cursorY_}; } };这里的设计要点在于createMemento和restoreFromMemento的接口暴露了原发器与备忘录交互的意愿但具体的备忘录类TextEditorMemento对外部除了原发器应该是不可见的。我们用了前向声明class TextEditorMemento并返回std::unique_ptr这暗示了备忘录对象的生命周期将由调用者管理并且其具体定义被隐藏了。2.2 备忘录Memento状态的保险箱备忘录对象就是那个存储原发器状态快照的“保险箱”。它的核心设计原则是不可变性Immutable和访问控制。一旦创建其内部状态就不应被修改除了由原发器恢复时。在C中我们常用嵌套类结合友元来实现严格的访问控制。// 在TextEditor类内部或头文件中但对外隐藏细节 class TextEditor { // ... 其他成员同上 ... // 关键将Memento定义为原发器的私有嵌套类 class TextEditorMemento { private: // 备忘录存储原发器状态的副本 const std::string savedText_; const int savedCursorX_; const int savedCursorY_; const std::vectorstd::string savedFormattingStack_; // 构造函数设为私有且只允许原发器友元创建 TextEditorMemento(std::string text, int x, int y, std::vectorstd::string formatting) : savedText_(std::move(text)) , savedCursorX_(x) , savedCursorY_(y) , savedFormattingStack_(std::move(formatting)) {} // 声明原发器为友元这是实现封装的关键 friend class TextEditor; public: // 备忘录通常不提供任何修改其内部状态的公共接口 // 可以提供只读的访问器给负责人用于元数据查询如快照时间但绝不能暴露状态本身 // 例如 // std::chrono::system_clock::time_point getSnapshotTime() const { return snapshotTime_; } }; public: // 现在实现这两个关键方法 std::unique_ptrTextEditorMemento createMemento() const { // 使用std::make_unique并调用私有构造函数 // 注意由于TextEditorMemento是TextEditor的私有嵌套类外部无法直接调用make_uniqueTextEditor::TextEditorMemento // 所以通常需要提供一个公共的创建函数或者使用一个中间工厂。这里更常见的做法是返回一个不透明指针由原发器内部创建。 // 修正更优雅的实现是返回std::unique_ptrMementoInterface但为了清晰我们先展示直接返回嵌套类指针的方法。 // 实际上外部代码无法知晓TextEditorMemento类型所以我们需要一个公共基类或接口。我们将在2.3节优化。 return std::unique_ptrTextEditorMemento( new TextEditorMemento(text_, cursorX_, cursorY_, formattingStack_) ); } void restoreFromMemento(const TextEditorMemento memento) { // 因为是友元可以直接访问memento的私有成员 text_ memento.savedText_; cursorX_ memento.savedCursorX_; cursorY_ memento.savedCursorY_; formattingStack_ memento.savedFormattingStack_; // 可能还需要触发一些UI更新或事件通知 } };这个实现有几个精妙之处嵌套类TextEditorMemento是TextEditor的私有成员。这意味着在TextEditor类的外部你根本无法声明这个类型的变量更别说创建它了。这从语法层面保证了备忘录只能由原发器创建。友元friendTextEditor被声明为TextEditorMemento的友元。这打破了封装吗并没有因为这是有严格限定的“特权访问”。只有TextEditor能读写TextEditorMemento的私有数据其他任何类包括负责人都不行。这正体现了“将状态暴露给可信中介”的思想。const成员备忘录的成员变量都是const且在构造函数初始化列表里一次性设好。这确保了备忘录对象一旦诞生其状态就冻结了实现了不可变性。移动语义构造函数参数使用值传递并结合std::move避免了不必要的拷贝对于可能较大的状态如std::vectorstd::string能提升性能。注意上面createMemento的返回类型在实际工程中会有问题。因为TextEditorMemento是私有的外部代码无法声明std::unique_ptrTextEditor::TextEditorMemento。通用的解决方案是引入一个公共的、不包含任何状态访问方法的接口类如IMemento让TextEditorMemento私有地继承它。负责人只持有IMemento的指针。我们稍后会优化。2.3 负责人Caretaker历史的保管员负责人不关心备忘录里具体存了什么它只负责两件事在合适的时机向原发器“索要”快照并把快照存起来在需要的时候把某个快照“还给”原发器让它恢复。// 负责人历史记录管理器 class EditorHistory { private: // 存储备忘录的栈。使用基类指针以实现多态存储。 std::stackstd::unique_ptr/* IMemento 接口 */ history_; // 注意这里需要IMemento接口我们稍后定义 TextEditor* editor_; // 关联的原发器注意这里用原始指针实际项目需考虑所有权 public: explicit EditorHistory(TextEditor* editor) : editor_(editor) { // 确保editor不为空 assert(editor_ ! nullptr); } void snapshot() { if (editor_) { // 问题editor_-createMemento() 返回的是 unique_ptrTextEditor::TextEditorMemento // 我们需要一个公共接口来存储它。让我们先定义一个公共基类。 } } void undo() { if (!history_.empty()) { auto memento std::move(history_.top()); history_.pop(); if (editor_ memento) { // 问题如何调用restore需要公共接口。 // editor_-restoreFromMemento(*memento); } } } bool canUndo() const { return !history_.empty(); } };这里暴露了之前提到的问题负责人需要一种方式来存储和传递备忘录但又不能知道备忘录的具体类型。解决方案是引入一个纯虚基类接口。3. 实战优化引入接口与处理复杂状态让我们重构代码解决类型隐藏和接口统一的问题。3.1 定义备忘录接口// memento_interface.h #pragma once #include memory // 备忘录的公共接口。不暴露任何状态只可能提供元数据访问如时间戳、操作名。 class IMemento { public: virtual ~IMemento() default; // 通常负责人只能通过这个接口进行删除、查询元数据等操作。 // 例如 // virtual std::string getDescription() const 0; // virtual std::chrono::system_clock::time_point getTimestamp() const 0; };这个接口就像一个“黑盒子”的盖子负责人只能看到盖子看不到里面的东西。3.2 重构原发器与私有备忘录// text_editor.h #pragma once #include memento_interface.h #include string #include vector #include memory class TextEditor { private: // 前向声明私有备忘录实现类 class TextEditorMementoImpl; std::string text_; int cursorX_; int cursorY_; std::vectorstd::string formattingStack_; public: TextEditor(); void type(const std::string words); void moveCursor(int x, int y); // 创建备忘录返回公共接口指针 std::unique_ptrIMemento createMemento() const; // 恢复状态。参数是公共接口但内部需要向下转型。 void restoreFromMemento(const IMemento memento); // 用于内部转换的私有方法或友元 private: const TextEditorMementoImpl castToImpl(const IMemento memento) const; }; // text_editor.cpp #include text_editor.h #include cassert // 私有备忘录实现类的定义放在.cpp文件中彻底对外隐藏。 class TextEditor::TextEditorMementoImpl : public IMemento { private: const std::string savedText_; const int savedCursorX_; const int savedCursorY_; const std::vectorstd::string savedFormattingStack_; // 可以添加元数据如时间戳 // const std::chrono::system_clock::time_point timestamp_; // 构造函数私有只有TextEditor能创建 TextEditorMementoImpl(std::string text, int x, int y, std::vectorstd::string formatting) : savedText_(std::move(text)) , savedCursorX_(x) , savedCursorY_(y) , savedFormattingStack_(std::move(formatting)) // , timestamp_(std::chrono::system_clock::now()) {} // TextEditor是唯一的朋友 friend class TextEditor; public: // 可以在这里实现IMemento的元数据接口 // std::string getDescription() const override { return TextEditor Snapshot; } }; // TextEditor成员函数实现 TextEditor::TextEditor() : text_(), cursorX_(0), cursorY_(0) {} void TextEditor::type(const std::string words) { text_.insert(cursorX_, words); cursorX_ words.length(); } void TextEditor::moveCursor(int x, int y) { cursorX_ x; cursorY_ y; } std::unique_ptrIMemento TextEditor::createMemento() const { // 使用new创建私有类实例因为make_unique无法访问私有构造函数。 // 由于TextEditorMementoImpl继承自IMemento可以向上转型。 return std::unique_ptrIMemento( new TextEditorMementoImpl(text_, cursorX_, cursorY_, formattingStack_) ); } // 关键的安全转换将IMemento引用安全地转换为我们知道的私有实现类引用 const TextEditor::TextEditorMementoImpl TextEditor::castToImpl(const IMemento memento) const { // 使用dynamic_cast进行运行时类型检查确保安全。 // 这要求IMemento至少有一个虚函数析构函数已满足。 auto* impl dynamic_castconst TextEditorMementoImpl*(memento); // 如果转换失败说明传入的备忘录不是本原发器创建的这是严重的编程错误。 assert(impl ! nullptr Invalid memento type for this originator!); return *impl; } void TextEditor::restoreFromMemento(const IMemento memento) { // 安全转换到具体实现类 const auto impl castToImpl(memento); // 恢复状态 text_ impl.savedText_; cursorX_ impl.savedCursorX_; cursorY_ impl.savedCursorY_; formattingStack_ impl.savedFormattingStack_; }这个实现更加健壮和工程化完全隐藏TextEditorMementoImpl的实现细节完全藏在.cpp文件里外部头文件看不到。这符合“PImpl”惯用法减少了编译依赖。接口隔离负责人只操作IMemento*或std::unique_ptrIMemento完全不知道下面具体是什么。安全转换restoreFromMemento通过一个私有辅助函数进行安全的dynamic_cast并加入断言防止误传其他类型的备忘录。扩展性如果需要支持多种原发器如图形编辑器、数据库连接每种都可以有自己的私有备忘录实现类它们都继承自公共的IMemento。负责人可以用一个统一的容器如std::vectorstd::unique_ptrIMemento管理所有类型的快照只要它知道哪个快照对应哪个原发器。3.3 重构负责人// editor_history.h #pragma once #include memento_interface.h #include memory #include stack class TextEditor; // 前向声明 class EditorHistory { private: std::stackstd::unique_ptrIMemento history_; TextEditor* editor_; // 非拥有关系也可以用shared_ptr/weak_ptr public: explicit EditorHistory(TextEditor* editor); void saveState(); // 改个更贴切的名字 void undo(); bool canUndo() const; void clear(); }; // editor_history.cpp #include editor_history.h #include text_editor.h EditorHistory::EditorHistory(TextEditor* editor) : editor_(editor) { assert(editor_ ! nullptr); } void EditorHistory::saveState() { if (editor_) { history_.push(editor_-createMemento()); } } void EditorHistory::undo() { if (canUndo() editor_) { auto memento std::move(history_.top()); history_.pop(); // 现在可以安全地调用restore了因为参数是IMemento editor_-restoreFromMemento(*memento); } } bool EditorHistory::canUndo() const { return !history_.empty(); } void EditorHistory::clear() { // 清空栈。unique_ptr会自动释放内存。 while (!history_.empty()) { history_.pop(); } }现在负责人EditorHistory的代码干净多了。它只依赖IMemento接口和TextEditor的前向声明完全不知道TextEditorMementoImpl的存在。这实现了完美的解耦。4. 性能、内存与深拷贝陷阱备忘录模式最被人诟病的就是性能和内存开销。每次保存快照都是一次全状态拷贝如果原发器状态很大比如一张高分辨率图片、一个庞大的场景图频繁的快照会迅速消耗大量内存。4.1 优化策略差异化存储与引用计数对于状态庞大的对象全量拷贝不可取。可以考虑以下策略增量快照只保存自上次快照以来发生变化的部分。这需要原发器能识别出状态的“脏”区域。例如在文本编辑器中如果只修改了一行可以只保存这一行的旧值和新值而不是整个文档。class IncrementalTextMemento : public IMemento { private: size_t changeStartPos_; size_t changeEndPos_; std::string oldTextFragment_; // ... 其他增量信息 };恢复时原发器需要有能力根据增量信息进行“打补丁”。这大大增加了原发器和备忘录的复杂度但节省了内存。共享不可变数据如果状态中包含大量不可变或很少变化的数据比如只读的资源配置文件、纹理数据可以用std::shared_ptr在备忘录和原发器之间共享而不是拷贝。class GameLevelMemento : public IMemento { private: std::shared_ptrconst TerrainData terrain_; // 共享地形数据 PlayerState playerState_; // 只拷贝玩家状态 // ... };这要求共享的数据本身是线程安全的或者至少在快照/恢复期间不被修改。序列化到磁盘对于超大的状态或者需要持久化的场景如游戏存档直接将快照序列化到文件或数据库内存中只保留一个轻量级的标识符如文件名、数据库ID。恢复时再从外部存储加载。这用空间换时间IO操作适用于不要求毫秒级恢复的场景。4.2 深拷贝与浅拷贝的抉择C中拷贝对象时必须明确是深拷贝还是浅拷贝。对于备忘录绝大多数情况需要深拷贝。// 危险示例浅拷贝导致的问题 class Document { private: std::vectorstd::unique_ptrParagraph paragraphs_; // 包含独占指针的容器 public: // 错误的createMemento默认的拷贝构造函数只会浅拷贝unique_ptr导致所有权混乱 std::unique_ptrIMemento createMemento() const { // 假设DocumentMemento内部有一个Document state_成员。 // 如果DocumentMemento的拷贝构造函数是编译器生成的它会对paragraphs_进行浅拷贝 // 两个对象原发器和备忘录中的unique_ptr指向同一块内存后续操作会导致双重释放或未定义行为。 return std::make_uniqueDocumentMemento(*this); // 隐患 } };正确做法为包含动态资源或指针的类实现自定义的拷贝构造函数/拷贝赋值运算符或者让备忘录的构造函数执行深拷贝逻辑。class DocumentMementoImpl : public IMemento { private: std::vectorstd::unique_ptrParagraph paragraphs_; public: // 深拷贝构造函数 DocumentMementoImpl(const std::vectorstd::unique_ptrParagraph src) { paragraphs_.reserve(src.size()); for (const auto para : src) { // 假设Paragraph是可克隆的 paragraphs_.push_back(std::make_uniqueParagraph(*para)); } } // ... 或者Paragraph提供clone虚函数 ... };踩坑实录我曾在一个图形编辑器项目里备忘录直接存储了std::vectorShape*而Shape是多态对象。创建快照时只是拷贝了指针恢复时原发器直接使用了这些指针。结果当用户删除某个图形时不仅原发器里的图形被删历史记录里所有指向该图形的快照都变成了悬空指针一执行撤销就崩溃。教训对于多态对象组成的集合备忘录必须进行“深克隆”即遍历集合对每个基类指针调用clone()虚函数需要每个派生类实现创建全新的对象副本。5. 与其他模式的联合作战备忘录模式很少单打独斗它常和其他模式配合解决更复杂的问题。5.1 备忘录 命令模式实现可撤销的命令队列这是最经典的组合。每个命令对象在执行execute()之前先通过原发器创建一个备忘录并保存起来。当需要撤销(undo())时命令对象将保存的备忘录交还给原发器进行恢复。class AbstractCommand { protected: TextEditor* receiver_; std::unique_ptrIMemento backup_; public: explicit AbstractCommand(TextEditor* editor) : receiver_(editor) {} virtual ~AbstractCommand() default; virtual void execute() 0; virtual void undo() { if (backup_ receiver_) { receiver_-restoreFromMemento(*backup_); } } void saveBackup() { if (receiver_) { backup_ receiver_-createMemento(); } } }; class InsertTextCommand : public AbstractCommand { private: std::string textToInsert_; int position_; public: InsertTextCommand(TextEditor* editor, std::string text, int pos) : AbstractCommand(editor), textToInsert_(std::move(text)), position_(pos) {} void execute() override { saveBackup(); // 执行前保存状态 // 执行具体的插入操作这会改变receiver_的状态 receiver_-moveCursor(position_, 0); receiver_-type(textToInsert_); } // undo() 已由基类实现 }; // 使用 TextEditor editor; EditorHistory history(editor); auto cmd std::make_uniqueInsertTextCommand(editor, Hello, 0); cmd-execute(); history.saveState(); // 或者命令执行后自动入队 // ... 用户点击撤销 cmd-undo(); // 恢复到执行前的状态在这种组合下负责人命令历史管理器管理的是命令对象的队列/栈而每个命令对象自己持有一个备忘录。这比负责人直接管理一堆备忘录更清晰因为每个备忘录都和导致状态变化的命令绑定在一起。5.2 备忘录 原型模式快速创建状态副本如果原发器的状态结构相对简单或者本身就是通过原型模式Prototype来创建新实例的那么备忘录可以直接存储原发器的一个克隆原型副本。class GraphicObject : public IPrototypeGraphicObject { // 实现clone()方法 public: std::unique_ptrGraphicObject clone() const override { return std::make_uniqueGraphicObject(*this); // 假设有正确的深拷贝 } }; class GraphicEditor { std::unique_ptrGraphicObject currentObject_; public: std::unique_ptrIMemento createMemento() const { // 备忘录直接存储克隆体 return std::make_uniqueGraphicObjectMemento(currentObject_-clone()); } void restoreFromMemento(const IMemento m) { const auto gm dynamic_castconst GraphicObjectMemento(m); currentObject_ gm.getState()-clone(); // 再次克隆避免别名问题 } };这简化了备忘录的实现因为它不需要知道原发器内部的具体结构只需要调用clone()方法。但前提是原型克隆本身是高效且正确的深拷贝。5.3 备忘录用于状态机或游戏AI在游戏开发中备忘录模式可以用于保存和恢复游戏实体的状态实现“悔棋”、AI搜索树的回溯如棋类游戏的Minimax算法、或者状态机的历史记录。class ChessPiece { Position pos_; PieceType type_; bool hasMoved_; // ... public: std::unique_ptrIMemento createMemento() const { return std::make_uniqueChessPieceMemento(pos_, type_, hasMoved_); } void restoreFromMemento(const IMemento m) { const auto cm dynamic_castconst ChessPieceMemento(m); pos_ cm.pos_; // type_ 通常不变hasMoved_ 恢复 hasMoved_ cm.hasMoved_; } }; class ChessBoard { std::vectorstd::vectorstd::unique_ptrChessPiece board_; Color currentTurn_; // ... public: class BoardMementoImpl : public IMemento { // 存储棋盘所有棋子的状态快照 BoardState savedState_; // 一个包含所有棋子状态的复杂结构 Color savedTurn_; friend class ChessBoard; }; // AI在模拟走棋时可以快速保存/恢复棋盘状态评估不同走法。 std::unique_ptrIMemento createMemento() const { // 深拷贝整个棋盘状态 return std::make_uniqueBoardMementoImpl(deepCopyBoardState(board_), currentTurn_); } void restoreFromMemento(const IMemento m) { const auto bm dynamic_castconst BoardMementoImpl(m); board_ std::move(bm.savedState_); // 假设实现了移动赋值 currentTurn_ bm.savedTurn_; } };这里的关键是备忘录使得状态的捕获和恢复成为了一个原子操作这对于需要反复尝试和回溯的算法非常有用。6. C实现中的高级议题与避坑指南6.1 多线程环境下的备忘录如果原发器可能在多线程中被修改而快照创建和恢复也可能发生在不同线程那么就需要考虑线程安全。创建快照时的竞态条件createMemento()需要读取原发器的所有状态。如果在这个过程中另一个线程修改了状态你得到的可能是一个不一致的快照比如文本内容更新了但光标位置没变。常见的做法是在创建快照时加锁。std::unique_ptrIMemento TextEditor::createMemento() const { std::lock_guardstd::mutex lock(editorMutex_); // 假设原发器有一个mutex return std::unique_ptrIMemento(new TextEditorMementoImpl(text_, cursorX_, cursorY_)); }同样restoreFromMemento也需要加锁以防止在恢复过程中状态被其他线程修改。备忘录本身的线程安全备忘录对象一旦创建应该是只读的不可变因此从多个线程读取同一个备忘录对象是安全的。但是如果负责人如EditorHistory的栈被多个线程同时修改压入或弹出就需要对栈操作进行同步。6.2 智能指针与所有权管理我们一直使用std::unique_ptrIMemento这明确了备忘录的唯一所有权属于负责人或命令对象。这是推荐的做法因为它清晰且能避免内存泄漏。std::shared_ptr何时用只有当多个对象需要共享同一个备忘录的读取权限且备忘录生命周期不确定时才考虑使用std::shared_ptr。例如一个“预览历史状态”的功能可能需要同时持有当前状态和某个历史状态进行对比。但要注意共享备忘录违背了备忘录通常的“一次性恢复”语义需谨慎设计。循环引用如果备忘录通过某种方式引用了原发器例如为了恢复方便备忘录里存了一个原发器的弱引用或原始指针而原发器又通过某种方式持有备忘录的shared_ptr就可能产生循环引用导致内存泄漏。使用std::weak_ptr可以打破这种循环。6.3 性能分析与优化点剖析热点使用性能分析工具如perf,VTune,valgrind --toolcallgrind确定内存拷贝的瓶颈在哪里。是std::vector的重新分配还是多态对象的深拷贝使用移动语义确保备忘录的构造函数和原发器的状态获取方法充分利用移动语义std::move避免不必要的拷贝。TextEditorMementoImpl(std::string text, std::vectorstd::string formatting) : savedText_(std::move(text)) // 移动而非拷贝 , savedFormattingStack_(std::move(formatting)) // 移动 {}考虑自定义分配器如果频繁创建/销毁大量小型备忘录对象可以使用内存池或自定义分配器来减少堆分配的开销。6.4 常见问题排查FAQ问题恢复状态后程序行为异常某些关联对象如观察者没有收到更新通知。排查restoreFromMemento仅仅复制了原始数据。如果原发器的状态改变通常伴随着一些副作用如通知观察者、更新UI、写入日志你需要在restoreFromMemento方法的最后手动触发这些副作用。例如调用一个notifyStateChanged()私有方法。void TextEditor::restoreFromMemento(const IMemento memento) { const auto impl castToImpl(memento); text_ impl.savedText_; cursorX_ impl.savedCursorX_; cursorY_ impl.savedCursorY_; // 恢复完成触发更新 notifyTextChanged(); // 内部可能调用观察者的更新方法 updateCursorDisplay(); }问题撤销/重做栈变得非常深内存占用过高。排查这是备忘录模式的固有缺点。解决方案包括设置历史深度限制只保留最近N个状态丢弃更早的。增量存储如前所述只存储差异。压缩存储对存储的状态进行压缩例如对于文本使用diff算法存储差异对于二进制数据使用zlib等库压缩。序列化到磁盘将不常用的历史状态交换到磁盘文件。问题dynamic_cast失败断言触发。排查这几乎总是因为将错误的备忘录对象传给了原发器。确保负责人管理的备忘录栈里的每个备忘录都是由对应的那个原发器创建的。不要混用不同原发器创建的备忘录。在调试时可以在IMemento接口中添加一个getOriginatorTypeId()之类的虚函数用于在运行时检查类型匹配。问题使用备忘录后发现对象的某些“派生状态”或“缓存”没有正确恢复。排查备忘录只保存了你在createMemento中显式序列化的成员变量。如果对象有一些由这些基本状态计算出来的缓存值例如文档的总字数、渲染后的位图这些缓存需要在恢复基本状态后重新计算。不要在备忘录里保存缓存因为缓存可能很快过时且恢复时重新计算通常比序列化/反序列化缓存更高效。void TextEditor::restoreFromMemento(const IMemento memento) { // ... 恢复 text_, cursorX_ 等基本状态 ... // 清除并重新计算派生状态 totalWordCountCache_.reset(); // 标记缓存失效 renderedPreview_.clear(); // 清空渲染缓存 // 下次访问 totalWordCount() 或 getPreview() 时会自动重新计算 }备忘录模式在C中的实现是对语言特性封装、友元、const、智能指针和软件设计原则单一职责、开闭原则的一次综合演练。它教会我们如何在“需要暴露内部状态”这一特殊情况下依然最大限度地维护对象的封装城墙。理解其精髓你就能在需要状态回溯、事务、配置管理的任何地方优雅地解决“状态快照”这个经典难题。