享元模式仅在对象含大量重复只读属性且实例数达千级时才节省内存;需严格区分const内部状态与传入的外部状态,避免将坐标等误作内部状态,并用工具验证内存热点。

享元模式在C++里到底省多少内存?先看关键判断
如果你的对象有大量重复的、只读的属性(比如字体名、颜色值、纹理ID),且实例数成千上万,flyweight 模式才值得引入;否则加一层工厂和共享指针反而增加间接访问开销和缓存不友好。别为“设计模式”而用——先用 sizeof 和 valgrind --tool=massif 确认对象本身是内存热点。
如何区分内部状态(intrinsic)和外部状态(extrinsic)?
内部状态必须完全独立于上下文,能安全跨实例共享;外部状态则每次使用时传入。常见误判是把坐标、ID、时间戳当内部状态——它们几乎总是外部的。
-
std::string font_name可以内置(只读字符串字面量或统一管理的std::string_view) -
int x, y必须由调用方传入,不能存在享元对象里 - 如果某个字段在构造后会被修改(哪怕只改一次),它就不属于内部状态
- 多线程下,内部状态必须是
const或原子类型,否则需额外同步
用 std::unordered_map 做享元工厂时的三个坑
直接拿 std::make_shared<flyweight>(args...)</flyweight> 往 map 里塞,看似简单,但容易踩空:
- 键类型未定义
operator==和哈希特化?编译不过;建议用std::tuple或自定义结构体 + 显式特化std::hash - 传入的参数含浮点数(如
float size)?用std::round(size * 100) / 100归一化,否则0.1f + 0.2f != 0.3f导致重复创建 - map 的 value 类型是
std::shared_ptr<flyweight></flyweight>还是std::unique_ptr<flyweight></flyweight>?前者支持多持有,后者需配合weak_ptr防循环引用;多数场景选shared_ptr更稳妥
对象池(Object Pool)和享元(Flyweight)混用时注意什么?
享元解决“相同配置对象复用”,对象池解决“频繁 new/delete 开销”,二者目标不同,强行合并反而模糊职责。真要结合,只在享元工厂内部用池管理底层资源(如纹理句柄、字体缓存块),而非享元实例本身:
- 享元类本身应是轻量、无状态(除 const 内部字段)、无虚函数表(避免 vptr 开销)
- 若你把
std::vector<char></char>缓冲区放进享元里,就违背了享元初衷——把它抽到单独的TexturePool中,享元只存int texture_id - C++20 起可考虑用
std::pmr::monotonic_buffer_resource给享元工厂分配器定制,比手写池更安全
最常被忽略的一点:享元的“共享”本质是引用语义,但 C++ 没有自动弱引用回收机制。如果内部状态依赖外部资源(比如一个已卸载的 OpenGL 纹理 ID),享元实例不会自动失效——得靠使用者保证生命周期,或加一层 handle + registry 做运行时校验。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











