享元模式(Flyweight):把"重复的"共享,把"独立的"外置
更新时间:2026-08-25。本文按"先看不用模式的困境 → 再用模式重构 → 类图对比分析"的结构,论述享元模式。可运行代码:demos/design-patterns/flyweight。
一、问题场景:海量细粒度对象把内存吃爆
文本编辑器 100 万字符的排版、游戏地图上 10 万棵树/敌人、UI 网格上百万个重复控件。对象数量极大,但其中大部分状态是重复的(同字体同颜色的字符、同模型的树)。
二、不使用模式:每个对象各持全部状态
cpp
// 每个字符对象都复制一份字体、颜色、样式——100 万字符 = 100 万份重复样式
struct Glyph {
wchar_t ch;
std::string font; // 重复字段:大量字符同字体
int size;
int color;
int x, y; // 每个字符不同的部分
};
std::vector<Glyph> doc; // 100 万份 → 内存爆炸| 缺陷 | 说明 |
|---|---|
| 内存浪费 | 重复的样式/模型数据每对象各存一份 |
| 创建开销 | 每次"画一棵树"都要 new 一个完整对象 |
| 缓存/命中率差 | 对象分散,共享资源无法池化复用 |
| 性能下降 | 内存占用大 → 换页、GC/分配压力大 |
不使用模式的类图

三、使用模式:内部状态共享、外部状态分离
享元模式把对象状态拆成两部分:内部状态(可共享、不变,如字体/模型)→ 放在享元对象中池化复用;外部状态(各实例不同,如坐标)→ 调用时作为参数传入。工厂维护享元池,相同内部状态的请求返回同一实例。
cpp
// flyweight.h
class GlyphStyle { // 享元:内部状态,共享
public:
std::string font; int size; int color;
bool operator==(const GlyphStyle& o) const {
return font == o.font && size == o.size && color == o.color;
}
};
struct GlyphStyleHash { size_t operator()(const GlyphStyle& s) const { /* ... */ } };
class GlyphFactory { // 享元工厂:池化
public:
std::shared_ptr<GlyphStyle> get(const GlyphStyle& key) {
auto it = pool_.find(key);
if (it != pool_.end()) return it->second;
auto p = std::make_shared<GlyphStyle>(key);
pool_[key] = p; return p; // 相同样式复用同一实例
}
private:
std::unordered_map<GlyphStyle, std::shared_ptr<GlyphStyle>, GlyphStyleHash> pool_;
};
// 渲染时:享元(共享样式)+ 外部状态(坐标)
void render(GlyphFactory& f, wchar_t ch, const GlyphStyle& style, int x, int y) {
auto s = f.get(style); // 同字体同颜色 → 同一实例
draw(ch, *s, x, y); // x,y 是外部状态,调用时传入
}使用模式的类图

内部/外部状态划分(本仓库内存视角)
| 状态 | 特征 | 存放位置 | 示例 |
|---|---|---|---|
| 内部状态 | 可共享、不变 | 享元对象内(池中一份) | 字体、颜色、模型 mesh |
| 外部状态 | 各实例不同、可变 | 调用参数/上下文 | 坐标、旋转角、当前血量 |
划分原则:问"这份数据在 100 万个实例间是否重复且不变"——重复且不变 → 内部状态(共享);每个实例各不相同 → 外部状态(外置)。100 万字符共享 10 种样式 = 内存降 10 万倍量级。
四、类图对比与分析
| 维度 | 不使用(各持全部) | 使用享元 |
|---|---|---|
| 内存 | 重复状态 × 对象数 | 共享状态一份 + 少量外部状态 |
| 创建开销 | 每实例新建 | 池化命中即复用 |
| 对象数 | 100 万 | 10~100(共享实例) |
| 状态一致性 | 各改各的 | 内部状态共享,改一处全局生效 |
| 结构成本 | 无 | 工厂 + 状态拆分 + 哈希查找 |
分析结论:享元用"共享 + 外置"把内存从 O(N×S) 降到 O(N_ext + S)。代价是外部状态需通过参数传递(对象不再自包含)、工厂增加一次哈希查找。本仓库的内存优化视角下,它正是"对象瘦身 + 数据局部性"的典型手段——但只在对象数量巨大且重复状态占比高时值得。
五、适用边界
| 适用 | 不适用 |
|---|---|
| 对象数量巨大(百万级)且内部状态重复 | 对象数量小(共享收益 < 哈希成本) |
| 内部状态可稳定共享(不可变) | 内部状态频繁变动(共享会互相污染) |
| 内存是瓶颈、想池化复用 | 代码可读性优先于内存优化 |
反模式警示:若共享的内部状态"可变",一个实例的修改会波及其他所有"共享者"——享元内部状态必须视为不可变。现代语言里享元的形态常是字符串驻留(string interning)、常量池、缓存。
一句话总结
享元模式把对象状态拆成"可共享的内部状态"(池化复用)与"各异的的外部状态"(调用时传入),内存从 O(N×S) 降到 O(N_ext+S);是海量细粒度对象的瘦身术,但只适用于"数量巨大、重复状态占比高、内部状态不可变"的场景。