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)插入删除 | 每个节点额外开销 |
备忘录模式的核心流程图
实际项目中的应用
我记得有个项目是做数据库的事务管理。每次事务开始前,保存一份数据快照。事务回滚时,直接恢复快照。用vector存快照,用map做索引,查找快照的时间复杂度是O(log n)。
我的建议:快照不要存整个对象,存变化的部分。比如只存修改过的字段。这样内存占用小,恢复也快。我一般用std::unordered_map存字段名和旧值。
嗯,备忘录模式配合STL,说白了就是给对象拍照片,存到相册里。想回到过去,翻出照片看看就行。vector、stack、deque,选哪个容器取决于你的需求。我个人偏爱vector,因为随机访问快,而且内存连续,缓存友好。
公众号:蓝海资料掘金营,微信deep3321