享元模式的核心是分离内/外状态,将不变的内蕴状态(如字体、颜色)集中管理并共享,使对象仅保留变化的外蕴状态(如坐标),需通过不可变结构和合理键设计实现高效缓存。

享元模式的核心不是“共享对象”,而是分离内/外状态
直接 new 出一堆 Character 或 Cell 对象,内存爆炸的根源往往不是对象本身大,而是重复字段(比如字体、颜色、字号)被每实例都存一份。享元要解决的正是这个——把可复用的、不变的部分(内蕴状态)抽出来集中管理,让每个实例只保留变化的、上下文相关的部分(外蕴状态)。
典型场景:文本编辑器里上百万个字符,每个 char 都带 font、size、color 字段?没必要。这些属性实际按段落/样式分类只有几十种组合。
- 内蕴状态必须不可变(
const成员、无 setter),否则共享就出问题 - 外蕴状态不存储在享元对象里,由客户端在调用时传入(比如
draw(x, y)中的坐标) - 工厂类负责缓存和复用——用
std::map或std::unordered_map按内蕴状态键值索引已有实例
手写享元工厂时,键的设计决定能否真正去重
很多人写了个 FontFactory,但发现缓存没生效,原因常出在键(key)构造上。C++ 里不能靠指针地址或临时对象做 key,得用能比较、能哈希的值语义类型。
例如字体信息:struct FontStyle { std::string font_name; int size; Color color; }; 必须手动实现 operator(用于 <code>std::map)或特化 std::hash<fontstyle></fontstyle>(用于 std::unordered_map),否则编译不过或行为未定义。
- 别用
std::shared_ptr<fontstyle></fontstyle>当 key——不同指针即使内容相同也不相等 - 避免把
std::string直接塞进 tuple 当 key,除非确认编译器支持其 hash;更稳妥是自己定义operator==和hash - 如果内蕴状态字段很多,考虑用
std::tuple<:string int uint32_t></:string>代替自定义 struct,天然支持比较和哈希
std::shared_ptr 是享元的基础设施,但不是享元本身
有人误以为“用 std::shared_ptr 包裹对象就是享元”,这是常见误解。shared_ptr 解决的是生命周期管理,不是逻辑上的状态分离。它只是帮你安全地共享一个对象,但如果你没把内/外状态拆开,shared_ptr 只会让内存泄漏更隐蔽。
正确做法是:享元类本身设计为轻量、不可变;工厂返回 std::shared_ptr<const flyweight></const>;客户端持有该 ptr,并在每次操作时补全外蕴状态。
- 享元类构造函数应为
explicit,禁止隐式转换干扰缓存逻辑 - 工厂的
get()方法建议加const限定符,强调只读查询 - 不要在享元内部访问全局状态或单例——这会破坏内蕴状态的纯性,导致不可预测的共享副作用
性能陷阱:缓存查找开销可能超过内存节省
享元不是银弹。当对象总数少(
实测过一个渲染系统:原本每帧新建 5000 个 Particle,改用享元后内存降了 60%,但帧时间涨了 8%——瓶颈卡在 std::unordered_map::find() 的哈希计算和桶遍历上。
- 高频短命对象(如粒子、临时几何体)优先考虑对象池(object pool),而非享元
- 若内蕴状态枚举有限(如只有 16 种材质),用数组索引代替 map 查找,速度提升明显
- 启用
-O2以上优化后,std::make_shared的构造开销通常可接受,但 debug 模式下 map 查找可能成为热点
享元真正起效的地方,是那些生命周期长、复用率高、内蕴状态离散但数量可控的场景——比如 GUI 控件模板、地图瓦片配置、协议消息头元数据。拿不准时,先 profile 内存分布,再决定要不要拆。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











