享元对象必须不可变,仅保存内在状态(如字体名、字号),外在状态(如坐标、可见性)由客户端传入;工厂需线程安全,用std::unordered_map缓存,推荐返回std::shared_ptr。

享元对象必须是不可变的,否则共享会出问题
享元模式的核心是“共享”,但共享的前提是线程安全和状态一致。如果 Flyweight 对象内部可被修改(比如暴露了 setter、或成员变量非 const),多个地方同时调用就会互相干扰。常见错误是把本该由外部传入的“外部状态”硬塞进享元内部——比如把坐标、颜色、ID 等本该由客户端管理的数据,写进了享元类里。
正确做法是:享元类只保留可共享的“内在状态”(intrinsic state),例如字体名、字号、字符集等全局不变的配置;而位置、透明度、所属容器等“外在状态”(extrinsic state)必须由调用方在每次使用时显式传入。
-
Font可作为享元:名称 + 尺寸 + 是否粗体 → 全局唯一,只初始化一次 -
TextRenderUnit不应作为享元:它含position_x和visible,这些必须每次渲染时传入 - 所有享元成员变量建议声明为
const,构造函数用explicit防止隐式转换
对象池用 std::unordered_map 做键值缓存,别手写查找逻辑
手动维护一个 vector + for 循环查找重复对象,既慢又易错。C++ 中最直接的方式是用 std::unordered_map,以享元的内在状态为 key,对象指针或值为 value。key 类型推荐定义为结构体并重载 operator== 和特化 std::hash,避免拼接字符串做 key(性能差、易出错)。
注意:若享元是轻量值类型(如仅含两个 int),可直接存值;若较重(含 std::string 或动态资源),建议存 std::shared_ptr<const flyweight></const>,避免拷贝开销,也便于生命周期统一管理。
立即学习“C++免费学习笔记(深入)”;
- key 结构体字段必须覆盖全部内在状态,缺一不可,否则相同逻辑对象可能生成多个实例
- 不要用
std::map替代 ——std::unordered_map平均 O(1),std::map是 O(log n),高频创建场景下差异明显 - 池本身建议封装为单例或静态局部变量,避免跨模块重复初始化
工厂类的 get() 必须是线程安全的,尤其在游戏/渲染主线程+加载线程共存时
多线程环境下,两个线程几乎同时调用 FontFactory::get("Arial", 14, true),可能各自新建对象再写入 map,造成内存泄漏和逻辑不一致。不能依赖“程序启动时预热”来绕过——资源往往是按需加载的。
最稳妥的做法是:对 map 的读写加锁,但粒度要细。推荐用 std::shared_mutex(C++17)实现读多写少优化:读路径(绝大多数调用)用 lock_shared(),写路径(首次插入)用 lock()。别用 std::mutex 全局锁,会严重拖慢并发获取性能。
- 避免在
get()内做耗时操作(如磁盘加载字体文件),那应该拆到单独的load()流程里 - 如果确定单线程使用(如嵌入式小系统),可加编译宏开关跳过锁,减少无谓开销
- 返回类型建议统一为
std::shared_ptr<const flyweight></const>,语义清晰且自动管理生命周期
销毁时机很关键:享元池通常不应主动清空,除非明确知道所有引用已释放
很多人误以为“用完就从池里删掉能省内存”,这是危险操作。因为 std::shared_ptr 的引用计数机制决定了:只要还有任意一处持有该指针,对象就不能析构;而池中保存的也是 shared_ptr,删掉池中条目只是移除一个弱引用,不影响实际存活。
真正该关注的是:谁在长期持有着这些 shared_ptr?比如 UI 组件缓存了字体指针、粒子系统复用了材质享元……这些持有者必须在自身销毁时释放引用。池本身一般存活至程序结束,或按模块边界(如关卡卸载)批量 reset。
- 不要在析构函数里遍历池并
reset()所有指针——这会强制提前销毁,导致后续悬空访问 - 调试时可用
use_count()检查某个享元当前被多少处引用,快速定位泄露点 - 如果真需要控制内存峰值(如移动端),可加 LRU 机制,但淘汰条件必须是
use_count() == 1(只剩池里一个引用)
享元模式真正难的不是写工厂和对象,而是厘清哪些状态该内化、哪些必须外置;以及在多线程+长生命周期+资源卸载混合场景下,让引用计数、锁策略和销毁边界都刚好咬合。漏掉其中一环,节省的内存可能全变成隐蔽的 crash 或内存泄漏。


















