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提供的全局单例模式的更多相关文章
- [转]System.DllNotFoundException: 无法加载 DLL“*.dll”: 内存位置访问无效。 (异常来自 HRESULT:0x800703E6)
我在使用地税发票控件进行开票的测试的时候,在xp上测试时正常的,在别人的win7系统测试也是正常,但我在我本机确不正常.我本机装的是msdn版本win7系统,这个系统比较原装. 错误信息如下: -- ...
- DLL内存分配与共享
一旦DLL的文件映像被映射到调用进程的地址空间中,DLL的函数就可以供进程中运行的所有线程使用.实际上,DLL几乎将失去它作为DLL的全部特征.对于进程中的线程来说,DLL的代码和数据看上去就像恰巧是 ...
- Delphi之DLL知识学习1---什么是DLL
DLL(动态链接库)是程序模块,它包括代码.数据或资源,能够被其他的Windows 应用程序共享.DLL的主要特点之一是应用程序可以在运行时调入代码执行,而不是在编译时链接代码,因此,多个应用程序可以 ...
- DLL入门浅析(2)——如何使用DLL
转载自:http://www.cppblog.com/suiaiguo/archive/2009/07/20/90621.html 上文我简单的介绍了如何建立一个简单DLL,下面再我简单的介绍一下如何 ...
- DLL入门浅析(1)——如何建立DLL
转载自:http://www.cppblog.com/suiaiguo/archive/2009/07/20/90619.html 初学DLL,结合教程,总结一下自己的所得,希望对DLL初学者们有所帮 ...
- [转载]解析WINDOWS中的DLL文件---经典DLL解读
[转载]解析WINDOWS中的DLL文件---经典DLL解读 在Windows世界中,有无数块活动的大陆,它们都有一个共同的名字——动态链接库.现在就走进这些神奇的活动大陆,找出它们隐藏已久的秘密吧! ...
- DLL注入新姿势:反射式DLL注入研究
在分析koadic渗透利器时,发现它有一个注入模块,其DLL注入实现方式和一般的注入方式不一样.搜索了一下发现是由HarmanySecurity的Stephen Fewer提出的ReflectiveD ...
- .lib .dll 区别介绍、使用(dll的两种引入方式)
.lib .dll文件都是程序可直接引用的文件,前者就是所谓的库文件,后者是动态链接库(Dynamic Link Library)也是一个库文件.而.pdb则可以理解为符号表文件.DLL(Dynami ...
- 关于Dll、Com组件、托管dll和非托管dll
转自:https://blog.csdn.net/black_bad1993/article/details/53906252 Com组件 1.线程模型是干嘛用的?解决"多个线程" ...
- java.lang.UnsatisfiedLinkError: C:\apache-tomcat-8.0.21\bin\tcnative-1.dll: Can't load IA 32-bit .dll on a AMD 64-bit platform
Tomcat启动报错: 25-Mar-2016 10:40:43.478 SEVERE [main] org.apache.catalina.startup.Catalina.stopServer C ...
随机推荐
- DI入门案例
1.基于IoC管理bean 2.Service中使用new形式创建的Dao对象是否保留?(不保留) 3.Service中需要的Dao对象如何进入到Service中?(提供方法) 4.Service与D ...
- DFS遍历图(链式邻接表实现)
1 #include<iostream> 2 #include<string.h> 3 #include<stdlib.h> 4 #include<stdio ...
- 华企盾DSC邮件服务器测试连接提示Bad login or password(账号密码错误)
解决方法:出现该提示说明账号和密码有一个填错了,注意:这里的密码不是邮箱本身的密码,是授权码,具体可以在邮箱设置中查看,而且必须开启smtp服务才能正常使用.
- 如何从零开始实现TDOA技术的 UWB 精确定位系统(3)
这是一个系列文章<如何从零开始实现TDOA技术的 UWB 精确定位系统>第3部分. 重要提示(劝退说明): Q:做这个定位系统需要基础么?A:文章不是写给小白看的,需要有电子技术和软件 ...
- UE5: 探究Actor Tick的注册与执行
1. 前情提要 因工作需要,有在编辑器模式下执行Actor的Tick函数的需求.经过查阅资料,了解到重载Actor::ShouldTickIfViewportOnly函数可以实现在编辑器视口下也可以执 ...
- Spring Boot 整合 Log4j2 日志并压测性能
1/ Log4j2的性能测试 从图中不难看出,在线程数为 2~16 之间,混合使用同步和异步的logger来打印日志,性能是最好的. 2/ 目标 混合 sync/async 彩色日志 分类输出到不同文 ...
- Java 查找并替换PDF中的指定文本
本文介绍通过Java程序批量替换PDF中的指定文本内容. 1. 程序环境准备如下: 程序使用环境如图,需要注意的是,本文使用了免费版的PDF jar工具:另外JDK版本建议使用高版本更佳. jar ...
- 实践案例丨云连接CC实现跨区域多VPC与线下IDC Server互联
摘要:用实践案例带你掌握云连接CC如何实现跨区域多VPC与线下IDC Server互联. [背景] 当前在华为云华南.华东.香港region均部署了业务,同时在华南region通过云专线与线下IDC打 ...
- 如何更好的分析潜在人脉?聊聊华为云图引擎GES的Cypher子查询
摘要:本文以华为云图引擎 GES 为例,来介绍如何使用图查询语言 Cypher 表达一些需要做数据局部遍历的场景. 本文分享自华为云社区<使用 Cypher 子查询进行图探索 -- 以华为云图引 ...
- 规模化敏捷框架何从入手?这篇文章把SAFe讲透了!
摘要:敏捷软件开发理念已渐渐被业界普遍接受,越来越多的公司和团队不得不面对一个新的问题,就是规模化敏捷的引入和实现.目前市场上规模化框架主要有SAFe,Less,Scrum of Scrums, Sp ...