随想Visual C++ “导出/导入类的严重危害”
VC中有一个关键字__declspec(dllexport), 其目的很简单,就是导出符号,供其它可执行模块使用,主要用于动态链接库(DLL), 然而也可用于EXE模块
与此相对应,__declspec(dllimport), 将指明由动态链接库导入符号,除了导入全局变量/类的静态成员变量,很多时候可以忽略, 这个时候寻找符号的优先级将有所不同
当这两个关键字配合类使用,将会产生比较尴尬的问题了
DLL:
struct __declspec(dllexport) A
{
void TestInline()
{
printf(“from dll\n”);
}
};
EXE:
struct __declspec(dllimport) A
{
void TestInline()
{
printf(“from exe\n”);
}
};
int main()
{
A obj;
obj.TestInline(); //输出 “from dll”!
return 0;
}
危害1:
A中的内联函数全部失效
这当然可以从符号链接表中发现链接符号的踪迹来表明内联函数失效了,不过有一个更简单的方法,我们可以在EXE的代码中把”TestInline”内联函数的定义代码加以简单修改,让其输出其它的字符串测试,这个时候运行会发现,输出的字符串仍然是”from dll”(注意编译为”Release”发行版, DEBUG版会将内联函数作为普通函数处理), 这就说明了,内联函数被完全忽略了。如果这个时候,还能想到”TestInline”的定义代码还有什么作用的话,那就是白白消耗编译的时间,编译期间编译器辛辛苦苦生成的代码,链接器会毫不留情地忽略掉。
很显然,忽略掉内联函数的代码,会有一个直接的副作用,那就是使应用程序的运行期变缓慢,特别是内联函数被大量在循环内调用,而这个内联当成普通函数处理了
C++运行库中有大量的内联成员函数,除了模板类以外(当然,非完全特化的模板类不可能支持导出的),基本上都是以导出类的形式导出,这样也就意味着,动态链接C++运行库的程序,这类内联函数全部失效,编译时间可一点没少。
危害2:
私有成员函数,以及原本不想导出的函数(没有多少重用价值,导出后还要承担维护版本兼容性的责任),也被”不知不觉”
地导出了,对于私有(private)成员函数, 导出到外面有什么用呢?占据了符号表的一个位置,可是谁又能调用到呢?
如果要想调用,看来要强制把private变为public了,但是VC的符号形成机制包含了public/private/protected,
也就是通过了编译也无法通过链接,唯一可能的办法就是,修改DLL头文件,在类里面增加”friend”, 在EXE端,
改变共享库DLL头文件的办法,很显然不是个正规方案
另外,导出了这些“无法调用”的函数,也影响了链接器的优化。对于这些函数,如果DLL模块内部也没有调用到,
本可以完全把这些代码优化掉,但是由于导出,链接器将无能为力, 这也直接增加了DLL文件的体积
由此想到的:
MFC/ATL动态库似乎了解到这个问题,所以,它们宁可一个一个成员函数进行符号导出,也没有进行整个类的导出,
为什么它们不把这个方式推荐给VC动态运行库呢?
VC传统静态链接运行库的方式虽然有某种弊端,但是却可以完全避开上述提到的问题,而且脱离了”版本型”DLL的依赖(MSVCR71.dll, MSVCP71.dll, MSVCR90.dll, MSVCP90.dll…), 对于这类DLL的依赖,将给以后程序升级带来隐含并难以解决的问题,大家可以考虑如下情形
版本1:A.exe依赖于B.dll, 并且它们同时依赖于msvcp71.dll
版本2: 由于A采用了新的VC版本进行编译,A.exe现在依赖于msvcp90.dll了,B.dll没有升级,仍然依赖msvcp71.dll
这个时候,进程中的内存结构产生了微妙的变化,举例来说,原来msvcp71.dll中的全局变量(例如cout)同时被A.exe和B.dll使用。现在呢,A.exe使用的是msvcp90.dll中的cout, B.dll使用的还是msvcp71.dll中的cout, 这种耦合的变化,将会导致程序执行逻辑的变化(如果你运气好,也许会没有问题的),例如,如果你在A中设置了cout的格式,然后调用B的接口,最后B的接口通过cout输出,版本2与版本1将会拿到不同的执行效果(源代码可一点没变哦,仅仅是换了个编译器编译)。大型程序逻辑将是非常复杂的,看来还是不要升级编译器进行编译的好,因为这将导致dll依赖关系的变化,这真是个无奈的选择
混合编译选项 ”/clr” 看来要考虑允许静态链接运行库了(现在的VC版本只能动态链接运行库的),因为运行库DLL存在版本和效率缺陷
大家有没有发现LINUX操作系统下的so文件一般比windows下的要大很多,其中一个原因是这个模型相当于默认全部导出,即使是全局函数也是一样,哪怕你临时写一个没意义的函数,忘记去掉了,也一并导出,虽然你并没有给”外界”头文件,告诉这个接口如何被调用,但是这个接口已经事实存在了。虽然这样会简化一些工作,但是个人觉得,是非常不负责任的,特别是现代的应用大都是多任务模块化的,这样做潜在地多吃掉不少内存和硬盘,而且对于版本兼容性,将带来麻烦。我对LINUX了解不多,不知道是否有支持指定导出函数的方法呢?
COM(Component Object Model), 也许有很多其它弊端,但却解决了上述问题,而且可以拿掉DLL文件名中描述版本的丑陋数字
个人认为,发展C++的二进制接口标准ABI(Application Binary Inte易做图ce)是非常重要的,COM应该算一个ABI标准吧。总是静态地依赖源代码作为可重用模块,真的弊端很大,多任务时占用的磁盘和内存也会增加。托管编程模型毫无疑问解决了这个问题,当然也带来了其它方面的一些问题。C++在编程模型上可以说比托管编程模型要简单很多,但缺少一个二进制标准。解决这个问题将比在语言层面增加几个关键字要重要很多
补充:软件开发 , Vc ,