精读C++20设计模式——结构型设计模式:享元模式

精读C++20设计模式------结构型设计模式:享元模式

前言

​ 现在我们来仔细学习一下享元模式:Flyweight,但是我觉得好像叫 Token 或者是 Cookie更加的合适(原书并列的说,笔者认为后两个说辞我显然更加能接受和理解),它主要是尝试解决一种性能问题------我们可不可以复用一些已经有的东西呢(注意我们没有在复用抽象,而是在复用数据,更加像是期待用引用取代值)

一个例子:文本显示

​ 假设我们现在正在打开一本超级无敌大的书,大约几千万个字吧!但是我们知道,常用的汉字本身可能也就不到 4000 个。比如说,在整本书中,大量出现了"我"、"你"、"吧"等高频词汇。那问题来了:我们真的有必要在内存里反复存储一大堆一模一样的字形数据吗?显然没有必要。

​ 我们完全可以不存储字本身的完整信息,而是存储指向这个字的索引。换句话说,就是把"字"放到一个共享的字典池里,文本本身只记录"引用"。这样,当需要显示时,再根据索引去取对应的字形。这种方式就避免了冗余存储,从而极大节省了内存空间。

那为什么 ASCII 文本中我们没有这么做呢?原因在于代价问题。ASCII 编码本身只占一个字节,而指针或索引往往需要 2~4 个字节(甚至更多),这样一来就得不偿失了。换句话说,ASCII 字符已经足够小,继续压缩反而增加了额外负担。

​ 不过,事情并没有到此为止。让我们继续思考:如果不仅仅是单个字频繁出现,还有一些字的组合或搭配也非常常见,比如"你好"、"谢谢"、"没事吧"。在整本书的文本里,这些词组也会反复出现。那我们是不是也可以用同样的方法,把它们放到共享池里,然后文本只存储索引?

​ 这样一来,我们就不仅减少了对单字的重复存储,还进一步减少了对常见词组的重复存储。随着共享池的规模合理扩展,我们就能用有限的资源,存储和渲染出极其庞大的文本。这种思想,其实就是**享元模式(Flyweight Pattern)**在文本显示场景下的一个具体应用。

cpp 复制代码
#include <iostream>
#include <unordered_map>
#include <memory>
#include <string>
#include <vector>

// 享元对象:表示一个"字"或"词组"
class Glyph {
public:
    explicit Glyph(const std::string& content) : content_(content) {}
    void draw() const {
        std::cout << content_;
    }
private:
    std::string content_;
};

// 享元工厂:管理所有的共享对象
class GlyphFactory {
public:
    std::shared_ptr<Glyph> getGlyph(const std::string& content) {
        auto it = pool_.find(content);
        if (it != pool_.end()) {
            return it->second; // 已存在,直接复用
        }
        auto glyph = std::make_shared<Glyph>(content);
        pool_[content] = glyph;
        return glyph;
    }
private:
    std::unordered_map<std::string, std::shared_ptr<Glyph>> pool_;
};

// 客户端:文本渲染
class Document {
public:
    void addWord(const std::shared_ptr<Glyph>& glyph) {
        text_.push_back(glyph);
    }
    void render() const {
        for (auto& g : text_) {
            g->draw();
        }
        std::cout << std::endl;
    }
private:
    std::vector<std::shared_ptr<Glyph>> text_;
};

int main() {
    GlyphFactory factory;
    Document doc;

    // 假设整本书有很多 "你", "好", "吧"
    auto g1 = factory.getGlyph("你");
    auto g2 = factory.getGlyph("好");
    auto g3 = factory.getGlyph("吧");

    // 多次重复使用,不会创建新的对象
    doc.addWord(g1);
    doc.addWord(g2);
    doc.addWord(g3);
    doc.addWord(g1);
    doc.addWord(g2);

    doc.render(); // 输出:你好吧你好
}
棋盘游戏
cpp 复制代码
#include <iostream>
#include <unordered_map>
#include <memory>

// 享元对象:棋子
class ChessPiece {
public:
    ChessPiece(const std::string& color, const std::string& type)
        : color_(color), type_(type) {}
    void draw(int x, int y) const {
        std::cout << color_ << type_ << " 放在 (" << x << "," << y << ")\n";
    }
private:
    std::string color_; // 内部状态:颜色
    std::string type_;  // 内部状态:棋子类型
};

// 工厂:保证相同的棋子只创建一次
class ChessFactory {
public:
    std::shared_ptr<ChessPiece> getChess(const std::string& color, const std::string& type) {
        std::string key = color + type;
        if (pool_.count(key)) return pool_[key];
        auto piece = std::make_shared<ChessPiece>(color, type);
        pool_[key] = piece;
        return piece;
    }
private:
    std::unordered_map<std::string, std::shared_ptr<ChessPiece>> pool_;
};

int main() {
    ChessFactory factory;
    auto blackPawn = factory.getChess("黑", "卒");
    auto redPawn   = factory.getChess("红", "兵");

    // 外部状态:位置
    blackPawn->draw(2, 3);
    redPawn->draw(5, 6);
    blackPawn->draw(2, 4);
}

​ 我们可以得到输出

复制代码
黑卒 放在 (2,3)
红兵 放在 (5,6)
黑卒 放在 (2,4)

即使棋盘上有 16 个卒,它们在内存中只会存在两个共享对象(黑卒、红兵)。


数据库连接池
cpp 复制代码
#include <iostream>
#include <unordered_map>
#include <memory>

// 享元对象:连接配置
class DBConfig {
public:
    DBConfig(const std::string& host, int port) : host_(host), port_(port) {}
    void show() const {
        std::cout << "DBConfig: " << host_ << ":" << port_ << "\n";
    }
private:
    std::string host_;
    int port_;
};

// 工厂:共享相同配置
class DBConfigFactory {
public:
    std::shared_ptr<DBConfig> getConfig(const std::string& host, int port) {
        std::string key = host + ":" + std::to_string(port);
        if (pool_.count(key)) return pool_[key];
        auto cfg = std::make_shared<DBConfig>(host, port);
        pool_[key] = cfg;
        return cfg;
    }
private:
    std::unordered_map<std::string, std::shared_ptr<DBConfig>> pool_;
};

int main() {
    DBConfigFactory factory;
    auto c1 = factory.getConfig("127.0.0.1", 3306);
    auto c2 = factory.getConfig("127.0.0.1", 3306);
    auto c3 = factory.getConfig("192.168.1.10", 5432);

    c1->show();
    c2->show();
    c3->show();
}
咋用啊?
  • 打印店字体库:一台电脑装了一份宋体字库,所有人都用这份库。没有必要为每份文档都单独保存字形。
  • 地铁 IC 卡:内部状态是固定的芯片结构,外部状态是余额、有效期等随用户变化的数据。
  • 视频游戏贴图:相同的树木、草丛、石头模型在地图里出现成百上千次,但只加载一份纹理资源,实例化时传入不同的位置和缩放参数。

总结

我们要解决什么问题?

当系统中存在大量相似对象 时,内存占用可能急剧膨胀,性能下降。

但这些对象其实往往存在可复用的内部状态(如字形、纹理、连接配置),如果能共享这部分,就能显著节省资源。

享元模式要解决的核心问题就是:如何在保证功能的前提下,尽量减少内存中的冗余对象。


我们如何解决?

享元模式通过将对象的状态拆分为两类:

  1. 内部状态(Intrinsic State)
    • 对象本身不会随环境变化的、不变的、可被共享的部分。
    • 例如:汉字的字形、棋子的颜色与类型、数据库连接参数。
  2. 外部状态(Extrinsic State)
    • 与上下文相关的、每次使用时才决定的部分。
    • 例如:汉字在文档中的位置、棋子在棋盘上的坐标、连接池中的使用次数。

通过共享内部状态对象 + 外部状态由调用方提供,就能极大减少对象数量。

享元模式优缺点
优点 缺点
显著减少内存占用 系统复杂度增加,需要区分内外部状态
提升对象复用率 不适合对象完全不同、共享价值不大的场景
便于维护共享资源池 外部状态需要额外传递,增加调用方负担
相关推荐
EQ-雪梨蛋花汤2 小时前
【Unity笔记】Unity XR 模式下 Point Light 不生效的原因与解决方法
笔记·unity·xr
NiKo_W3 小时前
C++ 反向迭代器模拟实现
开发语言·数据结构·c++·stl
qianmo20213 小时前
基于deepseek学习三角函数相关
学习·算法
YA10JUN3 小时前
C++版搜索与图论算法
c++·算法·图论
劲镝丶3 小时前
顺序队列与环形队列的基本概述及应用
数据结构·c++
奔跑吧邓邓子4 小时前
【C++实战(53)】C++11线程库:开启多线程编程新世界
c++·实战·多线程·c++11·线程库
l1t4 小时前
编译Duckdb机器学习插件QuackML
数据库·c++·人工智能·机器学习·插件·duckdb
十五年专注C++开发4 小时前
通信中间件 Fast DDS(三) :fastddsgen的安装与使用
linux·c++·windows·中间件·跨平台
tpoog5 小时前
[C++项目组件]Etcd的简单介绍和使用
开发语言·c++·etcd