一、为什么用享元模式
在开发一些资源密集型应用时,经常会遇到大量对象占用内存的问题。比如在绘制一片森林时,我们可能需要生成上千棵树,如果每棵树都包含完整的绘制信息、纹理数据,那么内存占用会非常高。
享元模式的核心思想是共享对象的内在状态,将可以复用的部分提取出来,多个外部对象共同使用,从而大幅减少内存占用。
二、场景说明
我们要实现一个森林绘制程序,要求:
种植多种树(松树、橡树、枫树、椰子树),并能绘制2000棵树。
每种树都有不同的形状和颜色。
每种树的绘制信息(包括占用大量内存的纹理数据)可以共享。
每棵树的位置是外部状态,不参与共享。
当窗口大小变化时,森林能够重新生成并适应新的尺寸。
在我们的场景中,每种树(松树、橡树、枫树、椰子树)都有固定的形状、颜色和大块纹理数据(4M),这些是可以共享的内在状态;而每棵树的坐标位置是外在状态,需要单独存储。
如果不使用享元模式,每棵树都占用大量内存,整个程序可能会因为2000棵树而消耗约8GB内存。
三、类图
四、C++代码实现
#include <QApplication>
#include <QPainter>
#include <QResizeEvent>
#include <QWidget>
#include <map>
#include <memory>
#include <random>
#include <vector>
// ================== 外在状态 ==================
struct Position {
int x, y;
Position(int x, int y) : x(x), y(y) {}
};
// ================== 享元接口 ==================
class TreeFlyweight {
public:
virtual void draw(QPainter& painter, const Position& pos) = 0;
virtual ~TreeFlyweight() = default;
};
// ================== 具体享元 ==================
class ConcreteTreeFlyweight : public TreeFlyweight {
private:
QColor color;
QString type;
// 模拟占用大内存的数据,比如树的纹理
std::vector<int> heavyData;
public:
ConcreteTreeFlyweight(const QString& type, const QColor& color)
: type(type), color(color) {
// 模拟占用内存:1百万个整数,大约 4MB
heavyData.resize(1000000, 42);
}
void draw(QPainter& painter, const Position& pos) override {
// 画树干
painter.setBrush(Qt::darkGray);
painter.drawRect(pos.x - 2, pos.y, 4, 20);
painter.setBrush(color);
painter.setPen(Qt::NoPen);
if (type == "松树") {
// 圆形树冠
painter.drawEllipse(pos.x - 10, pos.y - 20, 20, 20);
} else if (type == "橡树") {
// 椭圆形树冠
painter.drawEllipse(pos.x - 15, pos.y - 20, 30, 20);
} else if (type == "枫树") {
// 三角形树冠
QPolygon triangle;
triangle << QPoint(pos.x, pos.y - 25) << QPoint(pos.x - 15, pos.y - 5)
<< QPoint(pos.x + 15, pos.y - 5);
painter.drawPolygon(triangle);
} else if (type == "椰子树") {
// 半圆形树冠
painter.drawPie(pos.x - 15, pos.y - 20, 30, 30, 0, 180 * 16);
} else {
// 默认:圆形
painter.drawEllipse(pos.x - 10, pos.y - 20, 20, 20);
}
}
};
// ================== 享元工厂 ==================
class TreeFactory {
private:
std::map<QString, std::shared_ptr<TreeFlyweight>> pool;
public:
std::shared_ptr<TreeFlyweight> getTree(const QString& type,
const QColor& color) {
QString key = type + "_" + color.name();
if (pool.find(key) == pool.end()) {
pool[key] = std::make_shared<ConcreteTreeFlyweight>(type, color);
}
return pool[key];
}
int getPoolSize() const { return pool.size(); }
};
// ================== 客户端 ==================
class ForestWidget : public QWidget {
private:
struct Tree {
std::shared_ptr<TreeFlyweight> flyweight;
Position pos;
Tree(std::shared_ptr<TreeFlyweight> f, Position p) : flyweight(f), pos(p) {}
};
std::vector<Tree> trees;
TreeFactory& factory;
public:
ForestWidget(TreeFactory& f, QWidget* parent = nullptr)
: QWidget(parent), factory(f) {
resize(800, 600);
generateTrees(width(), height()); // 初始生成
}
void plantTree(const QString& type, const QColor& color, int x, int y) {
auto tree = factory.getTree(type, color);
trees.emplace_back(tree, Position(x, y));
}
// ================== 生成森林 ==================
void generateTrees(int w, int h) {
trees.clear(); // 清空旧的树
// 随机数引擎
std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution<int> distX(20, w - 20);
std::uniform_int_distribution<int> distY(50, h - 50);
std::uniform_int_distribution<int> distType(0, 3);
for (int i = 0; i < 2000; ++i) {
int t = distType(rng);
if (t == 0) {
plantTree("松树", Qt::green, distX(rng), distY(rng));
} else if (t == 1) {
plantTree("橡树", QColor(0, 128, 0), distX(rng), distY(rng));
} else if (t == 2) {
plantTree("枫树", QColor(255, 69, 0), distX(rng), distY(rng));
} else {
plantTree("椰子树", QColor(34, 139, 34), distX(rng), distY(rng));
}
}
}
protected:
void paintEvent(QPaintEvent*) override {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
for (auto& t : trees) {
t.flyweight->draw(painter, t.pos);
}
painter.setPen(Qt::black);
painter.drawText(10, 20,
QString("享元池对象数: %1, 实际种植树木数: %2")
.arg(factory.getPoolSize())
.arg(trees.size()));
}
// ================== 重写 resizeEvent ==================
void resizeEvent(QResizeEvent* event) override {
generateTrees(event->size().width(), event->size().height());
update(); // 触发重绘
QWidget::resizeEvent(event);
}
};
// ================== main ==================
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
TreeFactory factory;
ForestWidget forest(factory);
forest.show();
return app.exec();
}
五、森林效果展示
使用享元模式,由约8G内存缩小到约50M,✿✿ヽ(°▽°)ノ✿
六、总结
问题背景:在需要大量对象的场景下,内存消耗可能很大。
解决方案:享元模式将可共享的内在状态提取出来,多个对象复用,减少内存占用。
示例优势:
上千棵树只使用了 4~5 个享元对象(每种树一份大数据)。
内存消耗大幅下降,绘制效率高。
外部状态(位置)单独存储,实现灵活布局。
扩展思路:
可将树的纹理、3D 模型数据、图标等抽象为共享对象。
适用于游戏、地图渲染、图表绘制等高对象密度场景。
通过这个示例,读者可以直观感受到享元模式在节省内存、提高性能方面的实际价值。