享元模式,用Qt/C++绘制森林

一、为什么用享元模式

在开发一些资源密集型应用时,经常会遇到大量对象占用内存的问题。比如在绘制一片森林时,我们可能需要生成上千棵树,如果每棵树都包含完整的绘制信息、纹理数据,那么内存占用会非常高。

享元模式的核心思想是共享对象的内在状态,将可以复用的部分提取出来,多个外部对象共同使用,从而大幅减少内存占用。

二、场景说明

我们要实现一个森林绘制程序,要求:

  • 种植多种树(松树、橡树、枫树、椰子树),并能绘制2000棵树。

  • 每种树都有不同的形状和颜色。

  • 每种树的绘制信息(包括占用大量内存的纹理数据)可以共享。

  • 每棵树的位置是外部状态,不参与共享。

  • 当窗口大小变化时,森林能够重新生成并适应新的尺寸。

在我们的场景中,每种树(松树、橡树、枫树、椰子树)都有固定的形状、颜色和大块纹理数据(4M),这些是可以共享的内在状态;而每棵树的坐标位置是外在状态,需要单独存储。

如果不使用享元模式,每棵树都占用大量内存,整个程序可能会因为2000棵树而消耗约8GB内存。

三、类图

四、C++代码实现

cpp 复制代码
#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 模型数据、图标等抽象为共享对象。

    • 适用于游戏、地图渲染、图表绘制等高对象密度场景。

通过这个示例,读者可以直观感受到享元模式在节省内存、提高性能方面的实际价值。

相关推荐
TechNomad8 天前
设计模式:享元模式(Flyweight Pattern)
设计模式·享元模式
pengzhuofan17 天前
Java设计模式-享元模式
java·设计模式·享元模式
蝸牛ちゃん1 个月前
设计模式(十二)结构型:享元模式详解
设计模式·系统架构·软考高级·享元模式
牛奶咖啡132 个月前
学习设计模式《十九》——享元模式
学习·设计模式·享元模式·认识享元模式·享元模式的优缺点·何时选用享元模式·享元模式的使用示例
大飞pkz2 个月前
【设计模式&C#】享元模式(用于解决多次创建对象而导致的性能问题)
开发语言·设计模式·c#·享元模式
花好月圆春祺夏安2 个月前
基于odoo17的设计模式详解---享元模式
设计模式·享元模式
DKPT2 个月前
Java享元模式实现方式与应用场景分析
java·笔记·学习·设计模式·享元模式
on the way 1233 个月前
结构性设计模式之Flyweight(享元)
java·设计模式·享元模式
秋名RG3 个月前
深入理解享元模式:用Java实现高效对象共享
java·开发语言·享元模式