有一次在项目中用到C++的string (以前一直是用C的,对于C++的一些特性不是很了解),记录下遇到的一些问题。
整个项目有一个DLL和一个exe程序,DLL的类成员里面使用的一些string(主要是用它的find 、+的功能),在编译的时候有warning C4251的警告,F5运行程序没有什么问题,但是直接打开exe的时候就崩溃了,怀疑和这个C4251有关,在网上查看了一下发现string类并不是一个DLL的导出类,程序在执行的时候,可能会调用不动的DLL库,有些说的添加template class __declspec( dllexport ) std::***没有用,警告依然在,程序直接运行,也还是会崩溃。
最后找到了一个说的比较深入,也比较有用的方法。
这是因为STL里的string类并非是一个DLL的输出类,这会导致类的成员变量name无法正确输出。
在你将CTest定义为一个导出类后,CTest中的所有成员函数都将编译连接为DLL的输出函数。包括一些隐含的构造/析构函数以及operator=等。如果一个类的一些PUBLIC成员变量不是输出类,那么DLL的使用者对这些公共成员的方法的访问可能会导致访问不同的VC运行库。
分析下面的代码:
CTest test;
// 调用CTest的构造函数,由于CTest是输出类,构造函数也是DLL的输出函数,构造函数调用的string的构造函数,string调用VC运行库分配内存,这个时候string类使用的运行库是与CTest实现所在的DLL进行连接的那个VC运行库
test.intValue = 3; //没有问题
// 没有问题,因为对整数的赋值与运行库无关
test.name = "name"; //编译、连接都通过,运行时出现内存访问异常:
//test.name中的数据指针是非法的堆指针
// 由于string不是DLL的输出类,所以string的operator=不是一个DLL内部的函数,
对string的operator=成员的访问会导致编译器调用当前的模块(EXE)使用的VC运行库中的函数,这便产生了问题。
。
// 同样在析构的时候也可能产生这样的问题
解决的办法是
在编译两个模块时选择相同类型的DLL版本的VC运行库,相同类型的静态的VC运行库不解决问题,因为在这种情况下,不同的模块分别包含VC静态运行库的静态数据和代码。
其实问题很简单,为了便于说明问题,假定CTest实现所在的DLL是TEST.DLL, CTest的使用者是CLIENT.EXE。
在构造CTest的时候,CTest的构造函数调用string的构造函数,这个时候string使用的是与TEST.DLL连接的运行库中的内存分配函数。
当CLIENT.EXE代码中对string赋值的时候,会导致string重新分配内存,这个时候string使用的是与CLIENT.EXE连接的运行库中的内存分配函数。在这个过程中,string会将在构造时分配的内存释放掉,这些内存是由与TEST.DLL连接的运行库的内存分配函数分配的,但是却由与CLIENT.EXE连接的运行库的内存分配函数释放,如果TEST.DLL和Client.EXE使用的是同样类型的运行库DLL(都使用Debug Multi-thread DLL, 或者Release Multi-Thread DLL), 那么是不会有问题的,因为这个时候VC的运行库中的malloc, free使用同一的CRT内部数据。
如果使用不同的CRT库,那么实际上在CLIENT.EXE运行的时候,进程的地址空间里实际上存在两份不同的CRT的代码和数据(分别被TEST.DLL和CLIENT.EXE使用), 当一个CRT的free函数企图释放由另一个CRT通过malloc分配的函数的时候,就会出现问题,这个时候CrtIsValidHeapPointer函数就会认为这不是一个有效的堆指针。
那么如果使用同样的静态库呢?这个问题同样存在,因为如果使用静态库,在CLIENT.EXE运行的时候,进程的地址空间里还是会存在两份CRT的代码和数据,尽管他们的代码和数据结构很多都一样。
类似的问题有通过一个CRT的fopen获得一个FILE指针,然后由另一个CRT的fclose关闭等等。
否则,在释放内存的时候,那么CrtIsValidHeapPointer就会告诉你这快内存不是由这个CRT分配的。