87、STL与备忘录模式:状态保存、恢复机制、快照管理

备忘录模式,说白了就是给对象拍个快照。

哪天你想回到过去,直接把快照恢复就行。我最早接触这个模式,是在做一个游戏存档系统的时候。那时候还没用STL,自己手写链表存状态,别提多痛苦了。后来发现STL的vector、stack配合备忘录模式,简直绝配。

什么是备忘录模式

备忘录模式有三个角色:

  • 发起人(Originator):需要保存状态的对象
  • 备忘录(Memento):保存状态快照的对象
  • 管理者(Caretaker):负责管理备忘录的保存和恢复

你想想看,这不就是游戏里的存档系统吗?玩家是发起人,存档文件是备忘录,存档管理器是管理者。

核心思想:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。以后可以将对象恢复到原先保存的状态。

STL如何助力备忘录模式

我个人习惯用STL容器来管理备忘录。vector适合做快照列表,stack适合做撤销栈。我项目中遇到过需要保存用户操作历史的功能,就是用stack实现的。

#include <iostream>
#include <vector>
#include <string>
#include <memory>

// 备忘录:保存状态快照
class Memento {
private:
    std::string state_;
public:
    explicit Memento(const std::string& state) : state_(state) {}
    std::string getState() const { return state_; }
};

// 发起人:需要保存状态的对象
class Originator {
private:
    std::string state_;
public:
    void setState(const std::string& state) {
        std::cout << "设置状态: " << state << std::endl;
        state_ = state;
    }
    
    std::string getState() const {
        return state_;
    }
    
    // 创建备忘录
    std::unique_ptr<Memento> createMemento() {
        std::cout << "创建快照: " << state_ << std::endl;
        return std::make_unique<Memento>(state_);
    }
    
    // 从备忘录恢复
    void restoreFromMemento(const Memento& memento) {
        state_ = memento.getState();
        std::cout << "恢复状态: " << state_ << std::endl;
    }
};

// 管理者:管理快照
class Caretaker {
private:
    std::vector<std::unique_ptr<Memento>> mementos_;
public:
    void addMemento(std::unique_ptr<Memento> memento) {
        mementos_.push_back(std::move(memento));
    }
    
    const Memento& getMemento(int index) const {
        return *mementos_[index];
    }
    
    size_t size() const { return mementos_.size(); }
};

int main() {
    Originator originator;
    Caretaker caretaker;
    
    originator.setState("状态1");
    caretaker.addMemento(originator.createMemento());
    
    originator.setState("状态2");
    caretaker.addMemento(originator.createMemento());
    
    originator.setState("状态3");
    
    // 恢复到状态2
    originator.restoreFromMemento(caretaker.getMemento(1));
    
    return 0;
}

用stack实现撤销功能

撤销操作,说白了就是后进先出。STL的stack正好干这个。我曾经写过一个文本编辑器,每次输入都push一个快照,Ctrl+Z就pop恢复。

#include <iostream>
#include <stack>
#include <string>

class UndoManager {
private:
    std::stack<std::string> history_;
public:
    void saveState(const std::string& state) {
        history_.push(state);
        std::cout << "保存: " << state << std::endl;
    }
    
    std::string undo() {
        if (history_.empty()) {
            std::cout << "没有可撤销的操作" << std::endl;
            return "";
        }
        std::string state = history_.top();
        history_.pop();
        std::cout << "撤销到: " << state << std::endl;
        return state;
    }
    
    bool canUndo() const {
        return !history_.empty();
    }
};

小技巧:用stack做撤销时,记得限制栈的深度。不然用户操作几万次,内存就爆了。我一般限制100步,超过就pop掉最老的。

快照管理的高级用法

实际项目中,快照往往不是简单的字符串。可能是整个对象图。这时候可以用序列化。我习惯把快照存成JSON或二进制流,用vector<char>保存。

#include <iostream>
#include <vector>
#include <map>
#include <algorithm>

// 复杂对象的快照
class Snapshot {
private:
    std::map<std::string, int> data_;
    std::string timestamp_;
public:
    Snapshot(const std::map<std::string, int>& data, 
             const std::string& timestamp)
        : data_(data), timestamp_(timestamp) {}
    
    void print() const {
        std::cout << "快照时间: " << timestamp_ << std::endl;
        for (const auto& [key, value] : data_) {
            std::cout << "  " << key << ": " << value << std::endl;
        }
    }
};

// 快照管理器
class SnapshotManager {
private:
    std::vector<Snapshot> snapshots_;
    static constexpr size_t MAX_SNAPSHOTS = 10;
public:
    void addSnapshot(const Snapshot& snap) {
        if (snapshots_.size() >= MAX_SNAPSHOTS) {
            snapshots_.erase(snapshots_.begin());
            std::cout << "快照已满,删除最旧的" << std::endl;
        }
        snapshots_.push_back(snap);
    }
    
    const Snapshot& getSnapshot(size_t index) const {
        return snapshots_[index];
    }
    
    size_t count() const { return snapshots_.size(); }
};

避坑指南

我曾经踩过的坑:

  • 快照里存了指针,恢复时指针指向的内存已经释放了。记住:备忘录模式要求保存的是值,不是引用。
  • 大对象频繁创建快照,性能崩了。解决方案:增量快照,只保存变化的部分。
  • 多线程环境下,快照的读写没加锁。我建议用std::shared_mutex做读写锁。

性能对比

容器 适用场景 时间复杂度 内存开销
std::vector 随机访问快照 O(1)访问 连续内存,较低
std::stack 撤销/重做 O(1)压栈/弹栈 动态增长
std::deque 双向操作 O(1)两端操作 分段内存
std::list 频繁插入删除 O(1)插入删除 每个节点额外开销

备忘录模式的核心流程图

备忘录模式核心流程 发起人 Originator createMemento() 备忘录 Memento addMemento() 管理者 Caretaker getMemento() 返回备忘录 restoreFromMemento() 恢复后的发起人 状态已恢复 快照管理(vector存储) 快照1 状态A 快照2 状态B 快照3 状态C 当前快照 状态D ... 更多

实际项目中的应用

我记得有个项目是做数据库的事务管理。每次事务开始前,保存一份数据快照。事务回滚时,直接恢复快照。用vector存快照,用map做索引,查找快照的时间复杂度是O(log n)。

我的建议:快照不要存整个对象,存变化的部分。比如只存修改过的字段。这样内存占用小,恢复也快。我一般用std::unordered_map存字段名和旧值。

嗯,备忘录模式配合STL,说白了就是给对象拍照片,存到相册里。想回到过去,翻出照片看看就行。vector、stack、deque,选哪个容器取决于你的需求。我个人偏爱vector,因为随机访问快,而且内存连续,缓存友好。


公众号:蓝海资料掘金营,微信deep3321