在C++程序开发中,经常会遇到需要创建大量相似细粒度对象的场景,比如游戏中的大量粒子、文本编辑器中的字符对象等。这些对象往往存在大量重复的不可变属性,如果每个对象都单独存储这些属性,会造成严重的内存浪费。享元模式正是针对这类问题提出的解决方案,通过共享对象的内在状态,大幅减少对象实例的数量,实现内存优化。

享元模式的核心概念
享元模式将对象的状态分为两类,这是实现共享的基础:
- 内在状态:对象的固有、不可变的属性,这些属性可以被多个对象共享,不会因为使用场景的变化而改变。
- 外在状态:对象的上下文相关、可变的属性,这些属性不能被共享,需要由客户端在使用时传入。
享元模式的核心就是维护一个享元池,所有已经创建的内在状态对应的享元对象都存储在这个池中,当需要创建对象时,先检查池中是否已经有对应内在状态的对象,如果有就直接返回,没有再创建新的对象并放入池中。
享元模式的角色组成
一个完整的享元模式通常包含以下几个角色:
- 抽象享元类:定义享元对象的公共接口,声明对外在状态的操作方法。
- 具体享元类:实现抽象享元类的接口,存储内在状态,内在状态必须是不可变的。
- 享元工厂类:负责创建和管理享元对象,维护享元池,确保相同内在状态的对象只被创建一次。
- 客户端:维护外在状态,在需要的时候从享元工厂获取享元对象,并传入对应的外在状态。
C++实现享元模式示例
下面以文本编辑器中的字符对象为例,实现一个简单的享元模式。字符的字体、大小属于内在状态,可以共享;字符的位置、颜色属于外在状态,由客户端维护。
1. 抽象享元类定义
#include <iostream>
#include <string>
// 抽象享元类:字符接口
class Character {
public:
virtual ~Character() = default;
// 显示字符,需要传入外在状态:位置x、位置y、颜色
virtual void display(int x, int y, const std::string& color) = 0;
protected:
// 内在状态:字符内容、字体、大小
char char_content;
std::string font;
int font_size;
};
2. 具体享元类实现
// 具体享元类:具体字符实现
class ConcreteCharacter : public Character {
public:
// 构造函数初始化内在状态,内在状态一旦初始化不可修改
ConcreteCharacter(char c, const std::string& f, int size) {
char_content = c;
font = f;
font_size = size;
}
void display(int x, int y, const std::string& color) override {
std::cout << "字符: " << char_content
<< ", 字体: " << font
<< ", 大小: " << font_size
<< ", 位置: (" << x << "," << y << ")"
<< ", 颜色: " << color << std::endl;
}
};
3. 享元工厂类实现
#include <unordered_map>
#include <memory>
// 享元工厂类,管理享元池
class CharacterFactory {
private:
// 享元池,key为内在状态的标识,value为享元对象指针
std::unordered_map<std::string, std::shared_ptr<Character>> character_pool;
// 生成内在状态的唯一标识
std::string generateKey(char c, const std::string& font, int font_size) {
return std::string(1, c) + "_" + font + "_" + std::to_string(font_size);
}
public:
// 获取享元对象,不存在则创建
std::shared_ptr<Character> getCharacter(char c, const std::string& font, int font_size) {
std::string key = generateKey(c, font, font_size);
// 检查池中是否已有对应对象
if (character_pool.find(key) == character_pool.end()) {
// 没有则创建新的享元对象并放入池中
character_pool[key] = std::make_shared<ConcreteCharacter>(c, font, font_size);
std::cout << "创建新的字符对象: " << key << std::endl;
} else {
std::cout << "复用已有字符对象: " << key << std::endl;
}
return character_pool[key];
}
// 获取当前享元池中的对象数量
size_t getPoolSize() const {
return character_pool.size();
}
};
4. 客户端使用示例
int main() {
CharacterFactory factory;
// 创建多个相同内在状态的字符对象,只会创建一次
auto char1 = factory.getCharacter('A', "宋体", 12);
char1->display(10, 20, "红色");
auto char2 = factory.getCharacter('A', "宋体", 12);
char2->display(30, 40, "蓝色");
auto char3 = factory.getCharacter('B', "宋体", 12);
char3->display(50, 60, "绿色");
auto char4 = factory.getCharacter('A', "宋体", 12);
char4->display(70, 80, "黑色");
std::cout << "享元池中对象数量: " << factory.getPoolSize() << std::endl;
return 0;
}
内存优化效果分析
假设我们需要创建10000个字符对象,其中只有100种不同的内在状态组合:
- 不使用享元模式:需要创建10000个对象实例,每个对象存储内在状态和外在状态,内存占用为10000 * 单对象大小。
- 使用享元模式:只需要创建100个享元对象存储内在状态,外在状态由客户端在使用时传入,内存占用为100 * 享元对象大小 + 外在状态的存储开销,内存节省比例超过99%。
从示例的运行结果也可以看到,多次获取相同内在状态的字符对象时,只会创建一次,后续都是复用已有的对象,这就是享元模式优化内存的核心逻辑。
使用注意事项
虽然享元模式能有效优化内存,但使用时需要注意以下几点:
- 享元对象的内在状态必须是不可变的,否则修改一个对象的内在状态会影响到所有使用该对象的地方,引发逻辑错误。
- 适合使用享元模式的场景是存在大量细粒度对象,且这些对象的内在状态重复率高,否则维护享元池的开销可能会抵消内存优化的收益。
- 外在状态需要由客户端自行维护,这会增加客户端的复杂度,需要在设计中做好外在状态的管理。
享元模式通过共享不可变的内在状态,将对象实例数量从与对象使用次数正相关,转变为与内在状态种类数正相关,从而在大量细粒度对象的场景下实现显著的内存优化,是C++中处理内存密集型场景的常用设计模式之一。