C++ 备忘录模式(Memento Pattern)
一、模式基础概述
1.1 定义
备忘录模式属于行为型设计模式 ,在不破坏对象封装性 的前提下,捕获并保存一个对象的内部状态,后续可将对象恢复到之前保存的状态。常用来实现撤销、重做、存档、状态回滚等功能。
1.2 核心思想
- 分离状态保存 、状态持有 、状态恢复三类职责;
- 对象内部状态对外隐藏,仅自身可读写,保证数据安全;
- 通过中间备忘录对象完成状态快照,负责人统一管理历史记录。
1.3 设计原则
遵循封装原则 、单一职责原则,不对外暴露原发器私有成员。
二、核心角色与职责
备忘录模式包含三大核心角色,角色间访问权限严格隔离:
| 角色名称 | 英文名称 | 核心职责 | 访问权限 | 关键方法 |
|---|---|---|---|---|
| 原发器 | Originator | 业务主体对象; 1. 创建备忘录,保存自身当前状态 2. 从备忘录恢复历史状态 | 可读写备忘录内部数据 | createMemento() restore(Memento*) |
| 备忘录 | Memento | 纯数据容器,仅存储原发器状态快照 | 仅原发器 可访问内部成员; 外部类无法创建、修改 | 私有成员变量(状态数据) 私有构造函数 |
| 负责人 | Caretaker | 管理备忘录集合; 1. 保存历史快照 2. 提供撤销、重做、清空历史功能 | 仅持有备忘录指针/对象,不能读取/修改内部状态 | save() undo() redo() clearHistory() |
2.1 角色协作流程
- 原发器调用
createMemento(),生成包含当前状态的备忘录对象; - 负责人将备忘录存入历史列表,完成状态存档;
- 执行撤销/重做时,负责人取出对应备忘录;
- 原发器调用
restore(),依据备忘录数据还原自身状态; - 全程负责人只做存储与调度,不触碰状态数据。
2.2 依赖关系
- 原发器 ↔ 备忘录:友元关系,唯一合法访问通道;
- 负责人 → 备忘录:仅持有引用,无访问内部权限;
- 负责人 → 原发器:调用原发器方法完成存档与恢复。
三、核心实现要点(C++ 专属)
- 友元(friend)
备忘录将原发器声明为友元,保证只有原发器能读写私有状态,严格保护封装。 - 私有构造函数
备忘录构造函数设为private,外部无法实例化,只能由原发器创建。 - 禁用拷贝/赋值
防止备忘录对象被意外复制、篡改,使用= delete禁用拷贝构造与赋值运算符。 - 智能指针管理
使用std::unique_ptr/std::shared_ptr管理备忘录生命周期,避免内存泄漏。 - 深拷贝
若状态包含指针、容器等复杂结构,必须做深拷贝,避免浅拷贝引发数据错乱。
四、完整 C++ 代码实现
4.1 业务场景
模拟游戏角色状态存档/读档:角色拥有生命值、攻击力、位置坐标,实现存档、多步撤销回档功能。
cpp
#include <iostream>
#include <vector>
#include <memory>
#include <cmath>
// 前向声明
class GameRole;
// ===================== 1. 备忘录 Memento(状态快照) =====================
class RoleMemento
{
// 友元:仅游戏角色可以访问内部状态
friend class GameRole;
private:
// 角色状态数据
int hp_; // 生命值
int attack_; // 攻击力
double posX_; // X坐标
double posY_; // Y坐标
// 私有构造函数:外部无法创建备忘录
RoleMemento(int hp, int atk, double x, double y)
: hp_(hp), attack_(atk), posX_(x), posY_(y) {}
// 禁用拷贝构造与赋值,防止外部篡改
RoleMemento(const RoleMemento&) = delete;
RoleMemento& operator=(const RoleMemento&) = delete;
};
// ===================== 2. 原发器 Originator(游戏角色) =====================
class GameRole
{
private:
// 当前角色状态
int hp_;
int attack_;
double posX_;
double posY_;
public:
// 构造:初始化角色属性
GameRole(int hp, int atk, double x, double y)
: hp_(hp), attack_(atk), posX_(x), posY_(y) {}
// 角色行为:受到伤害
void hurt(int damage)
{
hp_ = std::max(0, hp_ - damage);
std::cout << "角色受到 " << damage << " 点伤害\n";
}
// 角色行为:移动位置
void move(double x, double y)
{
posX_ = x;
posY_ = y;
std::cout << "角色移动到坐标 (" << x << ", " << y << ")\n";
}
// 创建备忘录:保存当前完整状态
std::unique_ptr<RoleMemento> createMemento() const
{
return std::make_unique<RoleMemento>(hp_, attack_, posX_, posY_);
}
// 从备忘录恢复状态
void restore(const RoleMemento* memento)
{
if (!memento) return;
hp_ = memento->hp_;
attack_ = memento->attack_;
posX_ = memento->posX_;
posY_ = memento->posY_;
}
// 打印当前角色状态
void showStatus() const
{
std::cout << "【角色状态】生命值:" << hp_
<< " 攻击力:" << attack_
<< " 坐标:(" << posX_ << ", " << posY_ << ")\n\n";
}
};
// ===================== 3. 负责人 Caretaker(存档管理器) =====================
class SaveManager
{
private:
// 存储所有历史存档快照
std::vector<std::unique_ptr<RoleMemento>> saveList_;
int currentIndex_ = -1; // 当前所处存档位置
const int MAX_SAVE = 10; // 最大存档数量,限制内存占用
public:
// 保存当前状态为新存档
void save(GameRole& role)
{
// 撤销后重新操作,删除后续无效存档
if (currentIndex_ < (int)saveList_.size() - 1)
{
saveList_.erase(saveList_.begin() + currentIndex_ + 1, saveList_.end());
}
// 限制最大存档数
if ((int)saveList_.size() >= MAX_SAVE)
{
saveList_.erase(saveList_.begin());
currentIndex_--;
}
saveList_.push_back(role.createMemento());
currentIndex_ = (int)saveList_.size() - 1;
std::cout << ">>> 状态已存档\n";
}
// 撤销(回退到上一存档)
bool undo(GameRole& role)
{
if (currentIndex_ <= 0)
{
std::cout << ">>> 无法撤销,已到最早记录\n";
return false;
}
currentIndex_--;
role.restore(saveList_[currentIndex_].get());
std::cout << ">>> 撤销成功\n";
return true;
}
// 重做(前进到下一存档)
bool redo(GameRole& role)
{
if (currentIndex_ >= (int)saveList_.size() - 1)
{
std::cout << ">>> 无法重做,已到最新记录\n";
return false;
}
currentIndex_++;
role.restore(saveList_[currentIndex_].get());
std::cout << ">>> 重做成功\n";
return true;
}
// 清空所有存档
void clearAll()
{
saveList_.clear();
currentIndex_ = -1;
std::cout << ">>> 所有存档已清空\n";
}
};
// ===================== 客户端测试 =====================
int main()
{
// 创建游戏角色 + 存档管理器
GameRole hero(100, 50, 0.0, 0.0);
SaveManager saveMgr;
std::cout << "===== 初始状态 =====" << std::endl;
hero.showStatus();
saveMgr.save(hero);
// 第一次操作:受伤
hero.hurt(30);
hero.showStatus();
saveMgr.save(hero);
// 第二次操作:移动位置
hero.move(10.5, 20.8);
hero.showStatus();
saveMgr.save(hero);
// 执行撤销
std::cout << "===== 执行第一次撤销 =====" << std::endl;
saveMgr.undo(hero);
hero.showStatus();
std::cout << "===== 执行第二次撤销 =====" << std::endl;
saveMgr.undo(hero);
hero.showStatus();
// 执行重做
std::cout << "===== 执行重做 =====" << std::endl;
saveMgr.redo(hero);
hero.showStatus();
// 尝试超出边界撤销
saveMgr.undo(hero);
saveMgr.undo(hero);
return 0;
}
4.2 运行输出
===== 初始状态 =====
【角色状态】生命值:100 攻击力:50 坐标:(0, 0)
>>> 状态已存档
角色受到 30 点伤害
【角色状态】生命值:70 攻击力:50 坐标:(0, 0)
>>> 状态已存档
角色移动到坐标 (10.5, 20.8)
【角色状态】生命值:70 攻击力:50 坐标:(10.5, 20.8)
>>> 状态已存档
===== 执行第一次撤销 =====
>>> 撤销成功
【角色状态】生命值:70 攻击力:50 坐标:(0, 0)
===== 执行第二次撤销 =====
>>> 撤销成功
【角色状态】生命值:100 攻击力:50 坐标:(0, 0)
===== 执行重做 =====
>>> 重做成功
【角色状态】生命值:70 攻击力:50 坐标:(0, 0)
>>> 无法撤销,已到最早记录
>>> 无法撤销,已到最早记录
五、适用场景与禁用场景
5.1 适用场景
- 需要状态回滚
编辑器、绘图软件、APP 操作记录,实现撤销/重做功能。 - 数据存档与恢复
游戏存档、配置快照、用户表单临时保存、系统运行快照。 - 事务回滚
业务操作异常时,恢复到操作前状态。 - 需要保护封装
需保存对象内部状态,但不允许外部直接访问私有成员。
5.2 不适用场景
- 对象状态极其庞大,频繁快照会造成严重内存占用;
- 状态简单、无需历史记录,引入模式属于过度设计;
- 状态频繁变更且无回滚需求。
六、模式优缺点
6.1 优点
- 封装性极佳
对象内部状态完全对外隐藏,仅自身可读写,数据安全。 - 职责划分清晰
原发器处理业务,备忘录存储数据,负责人管理历史记录,各司其职。 - 灵活实现多级回滚
可自由保存多份历史快照,支持无限级撤销、重做。 - 状态恢复可靠
由原发器自主完成恢复逻辑,避免外部篡改导致异常。
6.2 缺点
- 内存开销大
每一次存档都会生成新备忘录对象,状态复杂时内存压力显著。 - 拷贝成本高
包含容器、指针、复杂嵌套对象时,深拷贝会消耗 CPU 性能。 - 历史记录难以清理
不限制存档数量会导致内存持续上涨。 - 类数量增多
新增备忘录、负责人类,小幅提升代码复杂度。
七、C++ 工程实践规范与优化方案
7.1 编码规范
- 必加虚析构
若存在继承关系,基类添加虚析构函数,防止智能指针析构泄漏。 - 严格控制访问权限
备忘录构造、成员变量全部设为private,仅通过友元开放权限。 - 禁用拷贝与赋值
备忘录默认禁止拷贝,防止状态被意外复制篡改。 - 优先使用智能指针
全程使用std::unique_ptr管理备忘录,减少裸指针。
7.2 性能优化方案
- 限制最大存档数
负责人设置上限,自动淘汰最早记录,控制内存大小。 - 增量快照(进阶)
仅保存变更部分状态,而非全量拷贝,降低拷贝开销。 - 延迟销毁
低频操作场景可缓存常用备忘录,减少重复创建。 - 多线程防护
多线程环境下,对存档列表、状态读写加互斥锁,保证线程安全。
7.3 复杂状态处理
- 状态含指针/动态容器:必须执行深拷贝;
- 状态为只读配置:可复用备忘录对象,不必每次新建。
八、相关模式对比
8.1 备忘录模式 VS 命令模式
| 对比项 | 备忘录模式 | 命令模式 |
|---|---|---|
| 核心目标 | 保存对象状态快照,按状态回滚 | 封装操作行为,反向执行操作实现撤销 |
| 存储内容 | 数据状态 | 操作指令、执行者 |
| 撤销逻辑 | 直接还原历史状态 | 执行反向操作 |
| 适用场景 | 整体状态回滚(存档、编辑器) | 动作序列、任务队列、批量操作 |
8.2 备忘录模式 VS 原型模式
- 原型:侧重对象快速克隆,用于对象创建;
- 备忘录:侧重状态快照与回滚,用于历史记录,二者用途完全不同。
九、总结
备忘录模式是状态快照与回滚 的专用设计模式,核心亮点是在不破坏封装的前提下实现状态保存与恢复。
- 核心结构:原发器 + 备忘录 + 负责人 三者配合;
- C++ 实现关键:友元、私有构造、禁用拷贝、智能指针、深拷贝;
- 使用建议:适合撤销、存档、回滚类场景,务必控制快照数量,规避内存与性能问题;
- 选型提醒:简单状态回滚优先使用本模式,复杂动作撤销可结合命令模式。