游乐游手机版
首页/编程语言/文章详情

C++享元模式实现教程 内部状态共享与工厂管理源码解析

时间:2026-05-06 19:00
享元模式的核心:分离内部状态与外部状态 享元模式的核心精髓,其实可以归结为一句话:把能共享的抽出来,不能共享的传进来。 这听起来简单,但实践中却是个容易踩坑的地方。关键在于严格区分两种状态:内部状态(intrinsic state)必须是只读、不可变、与上下文无关的;而外部状态(extrinsic

享元模式的核心:分离内部状态与外部状态

C++实现简单的享元模式 _ 内部状态共享与工厂管理【源码】

享元模式的核心精髓,其实可以归结为一句话:把能共享的抽出来,不能共享的传进来。 这听起来简单,但实践中却是个容易踩坑的地方。关键在于严格区分两种状态:内部状态(intrinsic state)必须是只读、不可变、与上下文无关的;而外部状态(extrinsic state)则需要在每次使用时,由客户端主动传入。一个常见的误区就是把positioncolor这类随场景变化的字段塞进享元类里——这直接导致共享失效,甚至可能引发棘手的并发写冲突。

正确的做法是什么呢?享元类应该只保留像glyph(字符)、font_family(字体族)、font_size(字号)这类真正可复用的属性;至于xy坐标、is_selected(是否被选中)等,必须由调用方在render()这类方法时作为参数传入。

享元对象必须分离内部状态和外部状态

让我们通过一个代码片段来具体感受一下:

class CharacterFlyweight {
private:
    char m_glyph;
    std::string m_font_family;
    int m_font_size; // 内部状态:构造时固定,不修改
public:
    CharacterFlyweight(char g, const std::string& f, int s)
        : m_glyph(g), m_font_family(f), m_font_size(s) {}
        
    void render(int x, int y, bool is_selected) const { // 外部状态走参数
        std::cout << "Draw '" << m_glyph << "' at (" << x << "," << y << ")"
                  << (is_selected ? " [selected]" : "") << "\n";
    }
};

看,render方法清晰地展示了外部状态是如何“流动”进来的,而享元对象自身则保持恒定不变。

享元工厂要用键值唯一标识享元实例

接下来是工厂。工厂的作用可不是简单地“缓存任意对象”,它的核心任务是:根据内部状态的组合生成一个唯一键,然后通过查表来实现复用。 这里的关键在于键的选择和生成逻辑。如果键类型选错或者拼接逻辑有歧义,就会导致该复用的没复用,或者不该复用的却复用了。举个例子,使用std::tuple作为键,通常就比直接拼接字符串要安全得多——这能有效避免像"11""1,1"这类潜在的歧义问题。

此外,工厂还必须考虑线程安全。当然,并非所有场景都需要复杂的同步机制。对于小型项目,使用static std::map配合std::call_once进行初始化通常就足够了;而在高并发场景下,则可能需要考虑std::shared_mutex甚至无锁哈希表。

这里有三个关键点需要牢记:

  • get_flyweight()方法只应接受内部状态字段作为参数,绝不能混入外部状态。
  • 键的生成逻辑必须是幂等的,且不能有副作用(比如,不要在构造键的过程中去创建新对象)。
  • 工厂应返回引用或智能指针,绝对禁止返回栈上对象的地址。

立即学习“C++免费学习笔记(深入)”;

客户端调用时必须显式传递外部状态

这是整个模式中最容易被忽略的一环。很多开发者精心设计了享元类和工厂,却在渲染循环里不小心把坐标之类的信息硬编码或设置到享元对象内部,这就让之前的努力前功尽弃了。请记住,享元对象的render()hit_test()layout()等方法,所有依赖上下文的参数都必须由外部传入。

来看一个典型的误用示例:

// ❌ 错误:把 x/y 存进享元,破坏共享性
flyweight->set_position(x, y); // 不该存在的方法
flyweight->render(); // 无法复用

而正确的方式应该是这样的:

// ✅ 正确:每次调用带上下文
auto& fw = factory.get_flyweight('A', "Consolas", 12);
fw.render(cursor_x, cursor_y, is_focused);
fw.render(line_x + 10, line_y + 20, false);

如果外部状态参数特别多(比如超过5个),可以考虑将它们封装成一个结构体传入,但要注意,这个结构体本身也不应该被享元对象持有或缓存。

C++ 中 shared_ptr 是享元工厂的合理返回类型

在C++实现中,使用std::shared_ptr作为工厂的返回类型,通常比使用裸指针或引用更为合理。这样做既能避免复杂的生命周期管理问题,又能很好地支持多线程环境下的安全共享。工厂内部可以使用std::map>来存储享元对象,新对象只构造一次,后续请求直接返回已存在的shared_ptr副本——开销极小,并且内存管理是自动的。

为什么不推荐返回引用呢?因为工厂内部容器(如map)的扩容或重组操作可能导致引用失效。同样,也不推荐直接返回值(即复制整个对象),因为这直接违背了享元模式减少对象数量的初衷。

这里有一个小陷阱需要注意:如果享元类涉及继承并有虚函数,务必记得将析构函数声明为virtual,否则通过shared_ptr来持有派生类对象时,派生类的资源可能无法被正确释放。

最后,还有一个更复杂的情况需要警惕:如果外部状态本身涉及资源(例如纹理句柄、GPU缓冲区ID等),这些资源仍然需要由客户端统一管理,享元对象绝对不应该持有它们。这一点在图形渲染或游戏开发等场景中尤其容易被忽略,结果往往是表面上共享了字符对象,背地里每个对象却还在绑定独立的纹理,共享的优势大打折扣。

来源:https://www.php.cn/faq/2324254.html
上一篇如何解决dmesg日志中的CPU高温警告问题 下一篇nohup命令与系统守护进程协同工作的原理与实践指南
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
如何在ThinkPHP中实现定时任务与命令行调度方法
编程语言 · 2026-07-04

如何在ThinkPHP中实现定时任务与命令行调度方法

用ThinkPHP实现定时任务时,很多开发者第一步就卡在命令行报错上,直接输入php think your:command却无法识别——这种情况绝大多数是因为命令类的注册方式存在问题。下面先梳理几个核心要点。 ThinkPHP 6 中 think 命令如何正确触发自定义指令 直接运行 php thi

ThinkPHP API接口防重放攻击实现方法
编程语言 · 2026-07-04

ThinkPHP API接口防重放攻击实现方法

先说几个核心判断:API防重放攻击这件事,做对了是道防火墙,做错了就是个心理安慰。很多开发者到踩坑了才明白——验签这东西,放错位置、漏掉字段、存错nonce,每一环都能让整个安全体系直接归零。 验签必须放在中间件里,不能在控制器里写 ThinkPHP 的请求生命周期中,中间件是唯一能在路由匹配、参数

ThinkPHP文件上传必须验证扩展名安全必要性分析
编程语言 · 2026-07-04

ThinkPHP文件上传必须验证扩展名安全必要性分析

在使用ThinkPHP进行文件上传时,ext扩展名验证通常是开发者首先接触的关键环节。但你真的了解它的实际工作原理吗?它仅比对文件名后缀,而不读取文件内容,甚至对空格和大小写都极其敏感。更为重要的是——它是TP文件上传验证五层防线中不可忽视的第一道关卡,一旦配置遗漏,整个validate验证链将直接

ThinkPHP关联模型自动写入与更新使用教程
编程语言 · 2026-07-04

ThinkPHP关联模型自动写入与更新使用教程

需要明确的是,ThinkPHP关联模型并没有提供所谓的“自动写入 更新”魔法开关。所谓的“自动”功能,实际上都需要开发者手动编写配置逻辑才能生效。核心原则在于:主模型和从模型必须分开独立处理,时间戳字段和业务字段需依靠修改器或钩子接管;批量操作则要规规矩矩地绕过模型逻辑来执行——只有理解透彻这些要点

BoxLayout中仅居中一个组件其他默认左对齐
编程语言 · 2026-07-04

BoxLayout中仅居中一个组件其他默认左对齐

在 Java Swing 中使用 BoxLayout 的 Y_AXIS 方向布局时,很多初学者容易掉进一个常见陷阱:希望将某个组件单独设置为中心对齐,但当调用 `setAlignmentX(CENTER_ALIGNMENT)` 后,却发现其他组件也跟着发生了偏移,完全达不到预期效果。实际上,关键之处