C++享元模式共享细粒度对象
享元模式:当C++程序里“重复造轮子”变成“共享螺丝钉”
写过图形编辑器或文字处理软件的人大概都踩过这个坑:画布上成百上千个字符、图标、小方块,每个对象都单独分配内存、各自维护状态——结果程序一开就吃掉几百MB,滚动卡顿得像老式投影仪。你查内存,发现90%的对象其实长得一模一样:都是字母‘a’,都是灰色圆点,都是12号宋体。这时候不是代码写得不够优雅,而是没把“可复用的细粒度对象”真正拎出来重用。
享元模式(Flyweight Pattern)就是干这事的。它不玄乎,核心就一条:把对象里能共享的部分抽成“内蕴状态”,把必须独立的部分留作“外蕴状态”,运行时再动态组合。听起来绕?举个真实例子:我们做一款支持百万级弹幕的直播客户端,每条弹幕包含字体、颜色、字号、内容、出现时间……但实际中,95%的弹幕用的是“思源黑体+白色+16px”,只有位置、时间、文本内容各不相同。如果为每条弹幕都 new 一个完整对象,内存和构造开销会指数级上涨;而用享元,只需维护几十个字体渲染器实例,其余靠传参注入即可。
C++实现的关键,在于明确划分两类状态:
- 内蕴状态(Intrinsic State):可被多个对象共享,必须是不可变的(const 或 immutable)。比如字体句柄、纹理ID、基础样式结构体。这部分通常存放在享元工厂的缓存池里。
- 外蕴状态(Extrinsic State):随上下文变化,不能共享,由客户端在调用时显式传入。比如坐标、透明度、生命周期、用户ID。它不属于享元对象本身,而是“用的时候才带上的行李”。
别急着写工厂类。先想清楚:你的对象里,哪些字段真的需要每个实例都存一份?比如一个 TextGlyph 类,如果 font_id、baseline_offset、is_bold 在整个会话中基本不变,那就该进享元;而 x, y, text_content, fade_alpha 必须剥离出去。
典型实现分三步:
第一步:定义享元接口,只暴露内蕴能力
class Glyph {
public:
virtual ~Glyph() = default;
// 只允许通过参数传入外蕴状态 —— 这是铁律
virtual void render(int x, int y, float alpha) const = 0;
// 不提供 getter/setter 暴露内部状态,避免误用
};
第二步:实现具体享元,所有成员变量必须是 const 或引用常量
class BitmapGlyph : public Glyph {
const FontTexture& m_texture; // 引用外部资源池中的只读纹理
const int m_width, m_height;
const uint8_t m_char_code;
public:
BitmapGlyph(const FontTexture& tex, int w, int h, uint8_t c)
: m_texture(tex), m_width(w), m_height(h), m_char_code(c) {}
void render(int x, int y, float alpha) const override {
// 使用 m_texture 渲染,x/y/alpha 全部来自参数
draw_textured_quad(m_texture, x, y, m_width, m_height, alpha);
}
};
第三步:享元工厂负责缓存与复用,避免重复构造
class GlyphFactory {
std::unordered_map<uint64_t, std::unique_ptr<Glyph>> m_cache;
public:
Glyph* get_glyph(const FontTexture& tex, int w, int h, uint8_t c) {
uint64_t key = hash_key(tex.id, w, h, c); // 自定义哈希,确保语义一致
auto it = m_cache.find(key);
if (it != m_cache.end()) return it->second.get();
auto ptr = std::make_unique<BitmapGlyph>(tex, w, h, c);
Glyph* raw_ptr = ptr.get();
m_cache.emplace(key, std::move(ptr));
return raw_ptr;
}
};
这里有个容易被忽略的细节:工厂返回裸指针,而非智能指针。因为享元对象的生命周期由工厂统一管理,客户端绝不应 delete 它——否则缓存就崩了。我们在文档和命名上要强硬约束:get_glyph() 返回的是“借用”,不是“拥有”。
实际项目中,我们曾把某嵌入式设备上的图标系统从每图标 2KB 内存压到平均 120B,靠的就是把 SVG 路径数据(内蕴)和屏幕坐标/缩放系数(外蕴)彻底解耦。上线后,设备在低内存下连续运行72小时无OOM——这不是模式的功劳,是对对象职责边界的诚实判断。
当然,享元不是万能膏药。如果对象状态90%都得动态传入,或者共享粒度太小(比如连 int 都要享元化),反而增加调用开销和理解成本。它最适合的场景很具体:高频创建、低频修改、高相似度、有明确内外状态边界的轻量对象。
最后提醒一句:别为了设计模式而模式。我见过有人给 std::string 做享元工厂——结果发现 std::string 本身已有小字符串优化(SSO),还额外加了一层哈希查找,性能反降30%。模式的价值,永远在于它帮你更清醒地看见:哪些东西本就不该重复存在。


还没有评论,来说两句吧...