命令模式核心是将请求封装为可存储、传递、排队的Command对象,统一接口为execute()和undo(),Command持Receiver弱引用并委托执行,避免循环引用与值语义切片。

命令模式的核心:把 do_something() 变成 Command 对象
不是写个函数就完事,而是让每个请求(比如“保存文件”“撤销上一步”)都变成一个可存储、可传递、可排队的独立对象。关键在抽象出统一接口:execute() 和可选的 undo(),背后封装具体接收者(Receiver)和动作逻辑。
常见错误是直接在 Command 里写业务代码,导致无法复用或组合。正确做法是让 Command 持有对 Receiver 的弱引用(如指针或 std::shared_ptr),把执行逻辑委托出去。
- Receiver 负责真正干活(如
Document::save()),Command 只负责“喊一声” - 构造
Command时传入 Receiver 实例,避免在execute()里硬编码创建新对象 - 如果命令需携带参数(如“保存为
path”),应在构造时捕获,而非通过全局变量或静态成员传入
如何避免 std::shared_ptr 循环引用导致内存泄漏
当 Invoker 持有 Command,而 Command 又持有 Receiver(且 Receiver 反向持有 Invoker 或 Command)时,std::shared_ptr 容易锁死生命周期。
典型场景:GUI 中按钮(Invoker)绑定一个 SaveCommand,而 SaveCommand 持有 Editor(Receiver),Editor 又通过信号槽连回按钮 —— 这时用 std::shared_ptr 就危险。
立即学习“C++免费学习笔记(深入)”;
- Receiver 在 Command 中优先用裸指针(
Receiver*)或std::weak_ptr,尤其当 Receiver 生命周期不由 Command 控制时 - 若必须用
std::shared_ptr,确保 Receiver 不反向持有任何指向 Command 或 Invoker 的shared_ptr - 调试时加日志,在
~Command()和~Receiver()里打印,确认析构顺序是否符合预期
std::function + lambda 能替代传统 Command 类吗?
能,但只适合简单、一次性、无状态的请求封装。比如 std::function<void> cmd = [&](){ doc.save(); };</void> 看起来很轻量,但很快会暴露问题。
真实项目中,命令常需序列化、撤销重做、权限检查、延迟执行、与 UI 控件联动 —— 这些都要求类型明确、可反射、可继承。lambda 是闭包类型,每个实例类型都不同,无法放进同一种容器(如 std::vector<:unique_ptr>></:unique_ptr>)。
- 用
std::function快速原型可以,但上线前建议回归虚函数接口 + 具体子类 - 若坚持用
std::function,至少包装一层基类(如class Command { public: virtual void execute() = 0; };),再用std::function实现子类,保持多态性 - lambda 捕获
this后,容易引发悬空指针 —— 比裸指针更难排查
撤销栈(std::stack<:unique_ptr>></:unique_ptr>)为什么不能直接存 Command 值
因为 Command 是抽象基类,不能实例化;即使派生类可实例化,值语义会导致切片(slicing):拷贝时丢失派生类特有字段和虚函数表,execute() 调用会崩或行为异常。
另一个坑是误用 std::stack<command></command>,编译可能过(靠隐式转换),但运行时调用的是基类空实现,或者触发未定义行为。
- 必须用智能指针:首选
std::unique_ptr<command></command>(撤销后命令通常不再需要) - 不要用
std::shared_ptr存栈,除非多个地方共享同一命令实例(极少见) - 撤销操作后记得清空 redo 栈,并在
execute()成功后把新命令压入 undo 栈 —— 这些逻辑不在 Command 内部,而在 Invoker 或 CommandManager 中
execute(),而是 Receiver 和 Command 的所有权边界划在哪——一不留神,Command 就成了内存管理黑洞。










