VC中有一個關(guān)鍵字__declspec(dllexport), 其目的很簡單,就是導(dǎo)出符號,供其它可執(zhí)行模塊使用,主要用于動態(tài)鏈接庫(DLL), 然而也可用于EXE模塊
與此相對應(yīng),__declspec(dllimport), 將指明由動態(tài)鏈接庫導(dǎo)入符號,除了導(dǎo)入全局變量/類的靜態(tài)成員變量,很多時候可以忽略, 這個時候?qū)ふ曳柕膬?yōu)先級將有所不同
當(dāng)這兩個關(guān)鍵字配合類使用,將會產(chǎn)生比較尷尬的問題了
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中的內(nèi)聯(lián)函數(shù)全部失效
這當(dāng)然可以從符號鏈接表中發(fā)現(xiàn)鏈接符號的蹤跡來表明內(nèi)聯(lián)函數(shù)失效了,不過有一個更簡單的方法,我們可以在EXE的代碼中把”TestInline”內(nèi)聯(lián)函數(shù)的定義代碼加以簡單修改,讓其輸出其它的字符串測試,這個時候運(yùn)行會發(fā)現(xiàn),輸出的字符串仍然是”from dll”(注意編譯為”Release”發(fā)行版, DEBUG版會將內(nèi)聯(lián)函數(shù)作為普通函數(shù)處理), 這就說明了,內(nèi)聯(lián)函數(shù)被完全忽略了。如果這個時候,還能想到”TestInline”的定義代碼還有什么作用的話,那就是白白消耗編譯的時間,編譯期間編譯器辛辛苦苦生成的代碼,鏈接器會毫不留情地忽略掉。
很顯然,忽略掉內(nèi)聯(lián)函數(shù)的代碼,會有一個直接的副作用,那就是使應(yīng)用程序的運(yùn)行期變緩慢,特別是內(nèi)聯(lián)函數(shù)被大量在循環(huán)內(nèi)調(diào)用,而這個內(nèi)聯(lián)當(dāng)成普通函數(shù)處理了
C++運(yùn)行庫中有大量的內(nèi)聯(lián)成員函數(shù),除了模板類以外(當(dāng)然,非完全特化的模板類不可能支持導(dǎo)出的),基本上都是以導(dǎo)出類的形式導(dǎo)出,這樣也就意味著,動態(tài)鏈接C++運(yùn)行庫的程序,這類內(nèi)聯(lián)函數(shù)全部失效,編譯時間可一點(diǎn)沒少。
危害2:
私有成員函數(shù),以及原本不想導(dǎo)出的函數(shù)(沒有多少重用價值,導(dǎo)出后還要承擔(dān)維護(hù)版本兼容性的責(zé)任),也被”不知不覺”
地導(dǎo)出了,對于私有(private)成員函數(shù), 導(dǎo)出到外面有什么用呢?占據(jù)了符號表的一個位置,可是誰又能調(diào)用到呢?
如果要想調(diào)用,看來要強(qiáng)制把private變?yōu)閜ublic了,但是VC的符號形成機(jī)制包含了public/private/protected,
也就是通過了編譯也無法通過鏈接,唯一可能的辦法就是,修改DLL頭文件,在類里面增加”friend”, 在EXE端,
改變共享庫DLL頭文件的辦法,很顯然不是個正規(guī)方案
另外,導(dǎo)出了這些“無法調(diào)用”的函數(shù),也影響了鏈接器的優(yōu)化。對于這些函數(shù),如果DLL模塊內(nèi)部也沒有調(diào)用到,
本可以完全把這些代碼優(yōu)化掉,但是由于導(dǎo)出,鏈接器將無能為力, 這也直接增加了DLL文件的體積
由此想到的:
MFC/ATL動態(tài)庫似乎了解到這個問題,所以,它們寧可一個一個成員函數(shù)進(jìn)行符號導(dǎo)出,也沒有進(jìn)行整個類的導(dǎo)出,
為什么它們不把這個方式推薦給VC動態(tài)運(yùn)行庫呢?
VC傳統(tǒng)靜態(tài)鏈接運(yùn)行庫的方式雖然有某種弊端,但是卻可以完全避開上述提到的問題,而且脫離了”版本型”DLL的依賴(MSVCR71.dll, MSVCP71.dll, MSVCR90.dll, MSVCP90.dll…), 對于這類DLL的依賴,將給以后程序升級帶來隱含并難以解決的問題,大家可以考慮如下情形
版本1:A.exe依賴于B.dll, 并且它們同時依賴于msvcp71.dll
版本2: 由于A采用了新的VC版本進(jìn)行編譯,A.exe現(xiàn)在依賴于msvcp90.dll了,B.dll沒有升級,仍然依賴msvcp71.dll
這個時候,進(jìn)程中的內(nèi)存結(jié)構(gòu)產(chǎn)生了微妙的變化,舉例來說,原來msvcp71.dll中的全局變量(例如cout)同時被A.exe和B.dll使用。現(xiàn)在呢,A.exe使用的是msvcp90.dll中的cout, B.dll使用的還是msvcp71.dll中的cout, 這種耦合的變化,將會導(dǎo)致程序執(zhí)行邏輯的變化(如果你運(yùn)氣好,也許會沒有問題的),例如,如果你在A中設(shè)置了cout的格式,然后調(diào)用B的接口,最后B的接口通過cout輸出,版本2與版本1將會拿到不同的執(zhí)行效果(源代碼可一點(diǎn)沒變哦,僅僅是換了個編譯器編譯)。大型程序邏輯將是非常復(fù)雜的,看來還是不要升級編譯器進(jìn)行編譯的好,因?yàn)檫@將導(dǎo)致dll依賴關(guān)系的變化,這真是個無奈的選擇
混合編譯選項(xiàng) ”/clr” 看來要考慮允許靜態(tài)鏈接運(yùn)行庫了(現(xiàn)在的VC版本只能動態(tài)鏈接運(yùn)行庫的),因?yàn)檫\(yùn)行庫DLL存在版本和效率缺陷
大家有沒有發(fā)現(xiàn)LINUX操作系統(tǒng)下的so文件一般比windows下的要大很多,其中一個原因是這個模型相當(dāng)于默認(rèn)全部導(dǎo)出,即使是全局函數(shù)也是一樣,哪怕你臨時寫一個沒意義的函數(shù),忘記去掉了,也一并導(dǎo)出,雖然你并沒有給”外界”頭文件,告訴這個接口如何被調(diào)用,但是這個接口已經(jīng)事實(shí)存在了。雖然這樣會簡化一些工作,但是個人覺得,是非常不負(fù)責(zé)任的,特別是現(xiàn)代的應(yīng)用大都是多任務(wù)模塊化的,這樣做潛在地多吃掉不少內(nèi)存和硬盤,而且對于版本兼容性,將帶來麻煩。我對LINUX了解不多,不知道是否有支持指定導(dǎo)出函數(shù)的方法呢?
COM(Component Object Model), 也許有很多其它弊端,但卻解決了上述問題,而且可以拿掉DLL文件名中描述版本的丑陋數(shù)字
個人認(rèn)為,發(fā)展C++的二進(jìn)制接口標(biāo)準(zhǔn)ABI(Application Binary Interface)是非常重要的,COM應(yīng)該算一個ABI標(biāo)準(zhǔn)吧。總是靜態(tài)地依賴源代碼作為可重用模塊,真的弊端很大,多任務(wù)時占用的磁盤和內(nèi)存也會增加。托管編程模型毫無疑問解決了這個問題,當(dāng)然也帶來了其它方面的一些問題。C++在編程模型上可以說比托管編程模型要簡單很多,但缺少一個二進(jìn)制標(biāo)準(zhǔn)。解決這個問題將比在語言層面增加幾個關(guān)鍵字要重要很多