C++跨DLL内存所有权问题探幽(一)DLL提供的全局单例模式
最近在开发的时候,特别是遇到关于跨DLL申请对象、指针、内存等问题的时候遇到了这么一个问题。
问题 跨DLL能不能调用到DLL中提供的单例?
问题比较简单,就是我现在有一个进程A,有DLL B DLL C,这两个DLL都依赖DLL D的单例,这个时候如果A调用了DLLB 和 DLL C,那么B和C能否正确引用到这个指定的单例?
其实这个问题我心中想的是可以使用这个进程的,但是在实际开发中我们却有了争议,有的人认为可以,有的人认为不可以,但是我对这个内存的所有权一直不太清楚,所以这也算是给自己一个解惑,一个交代。这篇文章也是我边查资料边写的,有引用很多文章,主要是为了引出结论和一些别的问题。
聊C/C++的动态内存管理的内容之前,我们先来了解一下进程中的内存分区,如上所示。我们可以通过查看程序的完整启动过程去看这些内存分区。
内存分区
在C++中,将操作系统分配给程序的内存空间按照用途划分了代码段、数据段、栈、堆几个不同的区域,每个区域都有其独特的内存管理机制。
代码区
只存代码本身,几乎不需要考虑
代码区是用于存储程序代码的区域,代码段在程序真正执行前就被加载到内存中,在程序执行期间,代码区内存不会被修改和释放。由于代码区是只读的,所以会被多个进程共享。在多个进程同时执行同一个程序时,操作系统只需要将代码段加载到内存中一次,然后让多个进程共享这个内存区域即可。
数据段
纯粹的数据,几乎不用考虑
数据段用于存储静态全局变量、静态局部变量和静态常量等静态数据。在程序运行期间,数据段的大小固定不变,但其内容可以被修改。按照变量是否被初始化。数据段可分为已初始化数据段和未初始化数据段。
栈
存放局部变量和函数调用。局部变量访问出错和函数死循环往往会导致stacks overflow。
C++中函数调用以及函数内的局部变量的使用,都是通过栈这个内存分区实现的。栈分区由操作系统自动分配和释放,是一种"后进先出"的一种内存分区。每个栈的大小是固定的,一般只有几MB,所以如果栈变量太大,或者函数调用嵌套太深,容易发生栈溢出(stack overflow)。
堆
一般就是我们new出来的指针,是一些内存空间。当内存地址所有权不明或者内存出错就容易出现堆损坏问题。
用于存放在程序运行时被动态分配的内存段。堆的大小不固定,可以动态增加和减少。使用malloc()等函数动态分配内存到堆上,使用free()等函数释放对应的动态分配内存。堆的最大容量受限于系统中有效的虚拟内存。
栈和堆?
区别:
1、申请方式:栈的空间由操作系统自动分配和释放,堆上的空间需要程序员使用malloc\free手动分配和释放。如果不释放会造成内存泄漏。
2、申请大小限制和效率:栈的空间时有限的,在linux中,使用 ulimit -s 指令 ,可以看到看到栈的容量为8M。栈区以先进后出的方式自动分配时连续的内存单元,效率高。
堆的大小受限与系统中有效的虚拟内存大小,系统是用链表来存储空闲的内存块,是不连续的。因此,堆的空间分配比较灵活,但容易产生内存碎片,相对来讲对效率低。
对于一个程序而言,大概的模型是这样的
以启动Windows系统中的exe程序为例
当我们双击某个exe程序或者通过桌面等快捷方式去启动一个exe程序时,就进入exe程序的启动过程。
程序启动时,系统首先会将exe主程序依赖的所有的dll库文件(包括exe程序自带的dll以及exe程序依赖的系统dll)都加载到进程空间中,这些dll二进制文件中存放的是可执行的二进制机器代码(即汇编代码,机器码与汇编代码等价的,汇编代码是机器码的助记符),都加载到进程的代码段的内存区中。
待所有依赖的dll模块都加载到进程空间后,最后才会将exe主程序加载到进程空间中。然后去启动C/C++的运行时库,紧接着去给全局变量分配内存并执行全局变量的初始化操作,此处对应的就是全局内存区。然后才会进入到main函数,程序才能真正的启动并运行起来。
进入到函数中,就会从所在线程的栈内存上给函数的局部变量分配栈内存,这就是我们讲的栈内存。当执行到malloc或new等代码时,申请的内存就是堆内存。
回到问题
这个问题其实实际上还没有涉及到内存所有权问题,但是我觉得可以当作一个引子:
static变量的唯一性是动态库级别的,不同库包含同一截代码的话就会有多份static实例。出现问题的情况往往是,这截代码是实现在头文件里的,所以被不同模块引用时就生成了多份代码。解决办法就是按照一般DLL导出接口的规则,在获取static变量的地方:
1.对于实现单例的dll,使用dllexport。对于MSVC来说就是__declspec(dllexport),对于GCC/clang就是__attribute__ ((dllexport))
2.对于引用单例的dll,使用dllimport。对于MSVC/clang来说就是__declspec(dllimport),对于GCC/clang就是__attribute__ ((dllimport))这样就只有一份了。
所以跨DLL的单例模式是可行的,这个静态变量(单例对象)必须导出,否则这个静态对象的实现会在不同的代码头文件引用的过程中一同被申请出来。
解决方案:
1.设计代码时选好单例的内存该放在哪,然后通过导入导出一解决(MSVC上导入导出使_declspec(dllimport)和_declspec(dllexport),类比extern变量一)。比如单例打算放在动态链接库A中,那就在生成动态库A的地方的这个static变量导出,其他动态链接库°或可执行文件使用的地方导入。
2.搞个统一的地方大家的单例共同注册到一起,用的时候拿出来。
tips:
Windows使用注意事项:
- 要求使用MD/MDd,而不是MT/MTd,反正至少保证要保证exe和所有dll使用同一间使用同一个crtheap堆,否则exe和不同dll间都有各自的crtheap堆,而new和delete需要在同一堆中配套执行。
- 慎用virtual,析构函数不能为virtual函数。因为当析构函数为virtual函数时,如果单例由dll1加载,而dll2在dll1卸载之后如果还在使用,那析构时调virtual的析构函数会去查虚函数表,而虚函数表是由dll1创建的,会引发崩溃。其他虚函数也涉及虚函数表,因此若要使用虚函数,那么除非能保证dll的卸载顺序,否则不要使用热卸载。事实上,全局单例的管理交由单例框架来实现后,析构函数是否使用virtual都不会产生泄漏,因为单例框架构造和析构时使用的都是具体的全局单例类,而不会是它们的基类。
- 单例全为懒加载,直到GetReference的时候才真正实例化单例对象,需要注意全局单例没有保证多线程间安全,因此在单例实例化/动态库首次获取单例时都是线程不安全的。若在SingletonManager.cpp的Count/Obtain/Release函数中使用std::mutex加锁能够实现单例获取的安全,但实例化过程(创建过程)仍是线程不安全的。
下一期来聊聊关于跨DLL引用DLL的时候导致的所有权问题
相关文章:

C++跨DLL内存所有权问题探幽(一)DLL提供的全局单例模式
最近在开发的时候,特别是遇到关于跨DLL申请对象、指针、内存等问题的时候遇到了这么一个问题。 问题 跨DLL能不能调用到DLL中提供的单例? 问题比较简单,就是我现在有一个进程A,有DLL B DLL C,这两个DLL都依赖DLL D的…...
短时间不点击云服务器,自动化断开连接,怎么设置长时间
在 Linux 系统中,如果你希望在一段时间内没有操作后保持远程连接不断开,可以通过修改 SSH 服务器的配置来实现。具体的步骤如下: 打开 SSH 服务器的配置文件: sudo vi /etc/ssh/sshd_config 找到以下两个参数并进行修改ÿ…...

typhonjs-escomplex 代码可读性 可维护度探索
目前市面上的前端代码质量评分中的代码可维护度是大都是基于 typhonjs-escomplex 这个库扫描而来,但是这个库的官方文档并没有介绍相关指标数据的计算规则,不知道规则如何提升指标数据呢?所以本文对 typhonjs-escomplex 源码进行探索…...
支持向量机基本原理,Libsvm工具箱详细介绍,基于支持向量机SVM的人脸朝向识别
目录 支持向量机SVM的详细原理 SVM的定义 SVM理论 Libsvm工具箱详解 简介 参数说明 易错及常见问题 完整代码和数据下载链接: 基于支持向量机SVM人脸朝向识别(代码完整,数据齐全)资源-CSDN文库 https://download.csdn.net/download/abc991835105/88527821 SVM应用实例, 基…...
密码破解工具的编写
预计更新 网络扫描工具的编写漏洞扫描工具的编写Web渗透测试工具的编写密码破解工具的编写漏洞利用工具的编写拒绝服务攻击工具的编写密码保护工具的编写情报收集工具的编写 密码破解工具是一种常见的安全工具,它可以通过不断尝试不同的密码组合来破解加密的数据或…...

BES2700H开发不完全手册
BES2700H开发不完全手册 是否需要申请加入数字音频系统研究开发交流答疑群(课题组)?可加我微信hezkz17, 本群提供音频技术答疑服务,群赠送语音信号处理降噪算法,ANC AEC ENC EQ BF BES蓝牙耳机音频资料 1 成功编译 2 代码 3 开放文档...

OpenGL的学习之路-3
前面1、2介绍的都是glut编程 下面就进行opengl正是部分啦。 1.绘制点 #include <iostream> #include <GL/gl.h> #include <GL/glu.h> #include <GL/glut.h>void myMainWinDraw();int main(int argc,char** argv) {glutInit(&argc,argv);glutIni…...

Vue 小黑记事本组件版
渲染功能: 1.提供数据: 提供在公共的父组件 App.vue 2.通过父传子,将数据传递给TodoMain 3.利用 v-for渲染 添加功能: 1.收集表单数据 v-model 2.监听事件(回车点击都要添加) 3.子传父,讲…...
javascript如何清空数组?
可以使用以下方法清空JavaScript数组: 直接赋值为空数组 arr []; let arr [1, 2, 3, 4]; arr []; // 现在arr是空数组使用 splice() 方法删除所有元素 let arr [1, 2, 3, 4]; arr.splice(0, arr.length); // 现在arr是空数组使用 length 属性将数组截断 let ar…...
MySQL MHA高可用切换
MySQL MHA 1.什么是 MHA MHA(MasterHigh Availability)是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。 MHA 的出现就是解决MySQL 单点的问题。 MySQL故障切换过程中,MHA能做到0-30秒内自动完成故障切换操作。 MHA能在…...

【Python】【应用】Python应用之一行命令搭建http、ftp服务器
🐚作者简介:花神庙码农(专注于Linux、WLAN、TCP/IP、Python等技术方向)🐳博客主页:花神庙码农 ,地址:https://blog.csdn.net/qxhgd🌐系列专栏:Python应用&…...

C++模拟实现——红黑树
一、介绍 红黑树也是对一般的搜索二叉树不能保证平衡的一个改进,和AVL树采用的思路不同,但同样需要旋转,其本质也是一颗平衡搜索二叉树,其节点有颜色的区分,并且被一些规则束缚,在这些规则下,能…...

java基础-数据类型
1、变量 变量就是申请内存来存储值。也就是说,当创建变量的时候,需要在内存中申请空间。 内存管理系统根据变量的类型为变量分配存储空间,分配的空间只能用来储存该类型数据。 因此,通过定义不同类型的变量,可以在内…...
设计数据库的时候会考虑哪些因素,怎样去建表?
在设计数据库时,通常会考虑以下因素: 数据的结构和关系:首先需要分析业务需求,了解需要存储的数据类型、数据之间的关系以及数据的组织结构。 数据的完整性和一致性:确保数据库中的数据完整性和一致性,例如…...

AI 绘画 | Stable Diffusion精确控制ControlNet扩展插件
ControlNet ControlNet是一个用于控制AI图像生成的插件,通过使用Conditional Generative Adversarial Networks(条件生成对抗网络)的技术来生成图像。它允许用户对生成的图像进行更精细的控制,从而在许多应用场景中非常有用,例如计算机视觉、艺术设计、虚拟现实等。 对于…...

青少年编程学习 等级考试 信奥赛NOI/蓝桥杯/NOC/GESP等比赛资料合集
一、博主愚见 在当今信息技术高速发展的时代,编程已经成为了一种必备的技能。随着社会对于科技人才的需求不断增加,青少年编程学习正逐渐成为一种趋势。为了更好地帮助青少年学习编程,提升他们的技能和素质,博主结合自身多年从事青…...

Linux 函数库
函数库: 我们的C程序中,并没有定义“printf”的函数实现,且在预编译中包含的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实“printf”函数的呢? 最后的答案是:系统把这些函数实现都被做到名为 libc.so.6 的库文件中去…...

Java 入门基础题
目录 1.输出一个整数的每一位 2.判定素数 3.求最大值方法的重载 4.输出闰年 5.打印 X 图形 6.数字9 出现的次数 7.计算分数的值 8. 模拟登陆 9.使用函数求最大值 10.斐波那契数列 星光不负赶路人,加油铁子们!!! 1…...

块设备的工作模式
块设备的mknod 还是会创建在 /dev 路径下面,这一点和字符设备一样。/dev 路径下面是 devtmpfs 文件系统。这是块设备遇到的第一个文件系统。我们会为这个块设备文件,分配一个特殊的 inode,这一点和字符设备也是一样的。只不过字符设备走 S_IS…...

Spring核心
Spring Framework Spring的两个核心IOC控制反转IOC容器依赖注入DIIOC容器实现注解管理BeanBean对象定义Bean对象获取 AOP面向切面编程 添加依赖入门案例注解通过Spring创建Java bean对象 xml管理Bean案例main下创建bean.XMl文件 DI依赖注入案例创建Spring配置文件 bean-di.xml …...

RocketMQ延迟消息机制
两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数,对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后…...
ssc377d修改flash分区大小
1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...

(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...
Python爬虫实战:研究feedparser库相关技术
1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
基于Uniapp开发HarmonyOS 5.0旅游应用技术实践
一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架,支持"一次开发,多端部署",可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务,为旅游应用带来…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...
【HTML-16】深入理解HTML中的块元素与行内元素
HTML元素根据其显示特性可以分为两大类:块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...
MySQL中【正则表达式】用法
MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现(两者等价),用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例: 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...