C++备忘录模式保存恢复状态

2026-04-11 23:10:28 434阅读 0评论

C++备忘录模式:别让对象“失忆”,手把手存档+回滚状态

写过游戏存档功能吗?改完配置又想一键退回上一步?或者调试时反复试错,却总得手动重置一堆变量?这些场景背后,其实都在呼唤一个轻量但关键的设计模式——备忘录(Memento)。它不炫技,不造轮子,就是老老实实帮你把对象某一刻的状态“拍张照”,需要时再原样还原。C++里实现它,既不用依赖第三方库,也不必大动架构,关键是搞清谁该负责拍照、谁保管底片、谁来冲洗

先说个常见误区:很多人一上来就想着用 std::vector<char>std::string 把整个对象内存 dump 出来。这看似省事,实则埋雷——成员变量顺序、对齐填充、指针有效性、动态分配资源全被忽略。一旦类里有 std::stringstd::vector 或自定义析构逻辑,直接 memcpy 就是给自己挖坑。

真正靠谱的做法,是让对象自己决定“哪些状态值得存”。比如一个文本编辑器类:

class TextEditor {
private:
    std::string content_;
    size_t cursor_pos_;
    bool is_dirty_;

public:
    // 备忘录类:只暴露读取接口,禁止外部修改
    class Memento {
        friend class TextEditor;
        std::string content_;
        size_t cursor_pos_;
        explicit Memento(std::string c, size_t pos) 
            : content_(std::move(c)), cursor_pos_(pos) {}
    public:
        // 只读访问,不提供 setter
        const std::string& content() const { return content_; }
        size_t cursor_position() const { return cursor_pos_; }
    };

    // 创建快照:只保存核心状态,跳过临时计算字段(如 is_dirty_)
    Memento save() const {
        return Memento{content_, cursor_pos_};
    }

    // 恢复状态:用快照重建内部数据
    void restore(const Memento& m) {
        content_ = m.content();
        cursor_pos_ = m.cursor_position();
        is_dirty_ = true; // 恢复后视为已修改
    }

    // 其他业务方法...
    void insert(char c) { /* ... */ }
    void move_cursor(size_t pos) { /* ... */ }
};

注意三个细节:

  • Memento 是 TextEditor 的嵌套类,且构造函数私有,只有 TextEditor 能创建它;
  • Memento 不暴露任何修改接口,避免外部篡改快照;
  • save() 里刻意没存 is_dirty_——它属于派生状态,恢复时重新设为 true 更合理。

那怎么管理多个快照?比如支持 Ctrl+Z 多步撤销。这时候加个简单的栈就行:

class EditorHistory {
private:
    std::stack<TextEditor::Memento> snapshots_;

public:
    void push(const TextEditor::Memento& m) {
        snapshots_.push(m);
    }

    bool can_undo() const { return !snapshots_.empty(); }

    TextEditor::Memento pop() {
        auto m = std::move(snapshots_.top());
        snapshots_.pop();
        return m;
    }
};

用的时候很直白:

TextEditor editor;
EditorHistory history;

editor.insert('H'); editor.insert('e'); // "He"
history.push(editor.save()); // 存档点1

editor.insert('l'); editor.insert('l'); // "Hell"
history.push(editor.save()); // 存档点2

editor.insert('o'); // "Hello"
if (history.can_undo()) {
    editor.restore(history.pop()); // 回退到 "Hell"
}

这里有个实用技巧:快照不必每次都深拷贝全部数据。如果 content_ 很长但多数时候不变,可以考虑用 std::shared_ptr<const std::string> 来共享只读内容,save() 时只增加引用计数——既省内存,又避免重复分配。当然,前提是确认字符串不会被意外修改(const 保证 + 移动语义配合)。

再聊个真实痛点:带资源的对象怎么存? 比如一个音频播放器持有 ALuint buffer_id(OpenAL 音频缓冲区)。这种句柄不能直接序列化,恢复时得重新绑定资源。正确做法是:快照里只存逻辑状态(如当前播放时间、音量、曲目ID),资源句柄由播放器在 restore() 时按需重建。把“状态”和“资源”解耦,模式才真正健壮。

最后提醒一句:备忘录不是银弹。频繁调用 save() 会吃内存,尤其状态庞大时。实际项目中,建议结合业务节奏做节流——比如编辑器只在用户显式触发(Ctrl+S)或每5秒自动存一次,而不是每次按键都快照。

写完这段代码,我顺手给本地一个配置解析器加了备忘录支持。昨天误删了一段 YAML 后,三秒就从历史快照里捞回来了。没有魔法,只是把“那一刻的样子”稳稳接住,等你回头要它的时候,它还在那儿。

备忘录模式的价值,从来不在多酷,而在于让你敢改、敢试、敢推倒重来——因为你知道,那个“之前”,一直被好好存着。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
验证码
评论列表 (暂无评论,434人围观)

还没有评论,来说两句吧...

目录[+]