使用Visual Studio連接LIB庫文件注意事項

在使用Visual Studio在Windows下開發應用程序時,可能面臨須要引用第三方庫來支撐自身代碼的狀況。第三方庫一般如下面兩種方式提供:函數

一、靜態LIB庫:這種提供形式一般包含LIB庫文件、頭文件及相關文檔說明。工具

二、動態DLL庫:這種提供形式一般也包含LIB庫文件(有些廠商不提供LIB庫文件),頭文件,DLL文件以及相關文檔說明。spa

不管以上那種形式的庫,在使用時都會面臨連接這個步驟(LoadLibrary->GetProcAddress方式載入DLL庫不在本文討論範圍內, 下同),而連接步驟又因爲將要生成的目標工程的不一樣類型變得愈加複雜。爲何這麼說呢,咱們繼續往下看。code

一般在連接一個第三方庫的LIB文件時,咱們使用下面兩種方法:遊戲

一、#pragma comment(lib, "XXX.LIB") 雜注方式。開發

二、項目->屬性->連接器->輸入->附加依賴項方式。文檔

乍一看這兩種方式咱們都用過,並且在使用時並無感受到兩種方式有何不一樣。但實際上仔細分析仍是有些地方值得商榷的。兩種不一樣的連接方法在生成不一樣類型的目標工程時表現出的行爲區分明顯。爲了展現方便,咱們作了以下幾個實驗:io

目標工程類型 使用#pragma雜注連接 使用附加依賴項連接
可執行文件(EXE)
動態連接庫(DLL)
靜態庫(LIB)
可執行文件(EXE),並連接上一步驟生成的靜態庫。

爲此我創建了一個解決方案,包含以下工程:Exe、Dll、Lib、Exe2,分別對應上面四個實驗所的目標項目。這個解決方案中還包含另一個LIB庫工程:Dep,上面的四個實驗工程的生成直接或間接依賴這個靜態庫。爲了節約空間以及方便閱讀,我關閉了全部工程的預編譯頭功能,刪除了stdafx.h/cpp文件,將代碼組織到單一文件中。整個解決方案以下圖所示:編譯

而每一個工程的源代碼都很是簡單,僅用於證實咱們的實驗結論。首先咱們看一下Dep這個靜態庫中包含的內容以下:table

//depmain.cpp
//它很是簡單,僅僅定義了一個函數。其餘工程連接這個靜態庫後即可以調用這個函數。

#include <stdio.h>

void depExportFunction()
{
	printf("This is Dep, and I export a function.");
}

下面咱們開始第一個目標工程Exe。Exe這個工程中也沒有包含任何複雜的代碼,咱們分別經過#pragma雜注及項目依賴項兩種方式連接Dep項目,觀察是否可以連接成功。

//exemain.cpp

#include <stdio.h>

//因爲是Debug配置, 解決方案輸出目錄在這個位置。
#pragma comment(lib, "../Debug/dep.lib")

//聲明Dep工程中的函數。
void depExportFunction();

int main()
{
    printf("This is Exe, and I want to call a function in Dep.\n");
    depExportFunction();
    return 0;
}

上面的代碼展現了使用#pragma雜注方式連接到Dep的方法。咱們也可使用項目依賴項方法連接Dep工程。

咱們用兩種連接方式都可以正確連接,並獲得可執行文件,運行獲得的結果也與咱們的意願相符。

若是你稍微動手實踐一下,你就會發現,生成Dll工程的連接實驗的結果應該與生成Exe工程的實驗結果相同。這好像證實了#pragma雜注與附加依賴項在行爲上沒有任何差異,但是真的是這樣嘛?咱們繼續進行後面的實驗(爲了不篇幅冗長囉嗦,我省略了Dll工程的實驗過程,由於我很懶,並且結果與Exe相同)。

咱們把重點放在生成Lib工程上來。

首先咱們觀察下面的代碼:

//libmain.cpp

#include <stdio.h>

#pragma comment(lib, "../Debug/dep.lib")

void depExportFunction();

void libExportFunction()
{
	printf("This is Lib, and I export another function, and I want to call the function in Dep.\n");
	depExportFunction();
}

這段代碼能夠正常生成名稱爲Lib.lib的靜態庫文件。對於這種文件,咱們可使用微軟提供的Lib.exe工具來查看其中包含了哪些obj文件,連接了哪些其餘的LIB庫文件。咱們能夠看到咱們剛剛編譯好的Lib.lib庫文件包含了下面的內容:

咱們好像發現了一些問題,新生成的Lib.lib文件中怎麼沒有包含Dep.lib中的內容?好吧,咱們暫時先把這個問題放在這,先看一下使用項目依賴項生成的庫文件是什麼樣子,而後在來對比一下。下面使用項目依賴項屬性從新生成了一次:

哦,原來真的是有區別,使用#pragma雜注生成的Lib居然沒有包含Dep的任何內容!這是什麼鬼?好吧,咱們看一下MSDN上怎麼解釋這個現象:

簡單說就是#pragma雜注僅僅是告知連接器在連接時別忘了去指定的路徑尋找另外一個靜態庫,不然就缺失某些二進制obj文件了,至於找到的庫中到底有啥,連接器本身去分析。而項目依賴項屬性則會將所依賴的靜態庫文件中的全部obj文件連接到即將生成新庫文件中。

那麼好吧,咱們玩一個遊戲試試,如今咱們把#pragma雜注所連接的Dep.lib文件路徑和項目依賴項屬性中的Dep.lib文件路徑所有刪除,但保留對Dep中函數的生命和調用,猜猜會發生什麼事情?你能夠本身嘗試編譯一下,是否是很驚訝,居然編譯經過了!趕忙來看一下生成的Lib.lib文件中都包含了些什麼?

不出所料,只有libmain.obj。雖然咱們在libmain.cpp中調用了Dep.lib中的函數depExportFunction(),但這裏絲毫沒有出現有關Dep庫的影子。

試想一下,若是Exe2工程依賴了Lib工程,那麼在你發行Lib庫文件給Exe2時,因爲Lib又依賴了Dep,但Dep卻沒有被連接到Lib中來,相信你會被一大堆無頭腦的LNK2019(沒法解析的外部符號)錯誤淹沒。或者,即便你使用了#pragma雜注連接了Dep到Lib,在發行Lib給Exe2工程時,也會由於上面的緣由獲得一條莫名其妙的錯誤LNK1104(沒法打開文件XXX.lib)。

實際上在生成一個靜態庫庫時,連接器對於所依賴的其餘庫中的內容並不敏感(生成過成不報錯),若經過項目依賴項配置了一個有效的依賴庫路徑,則最後生成時可以將依賴庫中的所有內容囊括到生成的庫中,若使用#pragma雜注指示依賴庫路徑,則會在生成的靜態庫中也包含這條雜注(但不包含依賴庫中的obj文件),最終在連接這個生成的庫到其餘EXE/DLL中時纔會去搜索雜注路徑中標識的依賴庫文件(本例中Lib.lib不包含Dep.lib,使用Lib.lib時還須要連接Dep.lib或depmain.obj)。固然,若寫錯了路徑,在生成目標庫時也不會出錯,只是生成的靜態庫中缺失了依賴庫的obj文件,須要在使用生成庫的EXE/DLL工程中單獨連接依賴庫或依賴庫內的obj文件集合(例如Exe2連接Lib.exe,還須要單獨連接Dep.lib或depmain.obj)。

接下來咱們繼續調戲鏈接器。咱們將Lib工程中的cpp文件libmain.cpp更名爲depmain.cpp,並使用項目依賴項方式從新生成Lib.lib庫,看下咱們的新庫中包含了什麼:

咦?怎麼只有一個depmain.obj?難道兩個同名的depmain.obj被合併成一個了?咱們在Exe2工程中連接一下這個新生成的Lib.lib,並調用其中的libExportFunction()函數(該函數又調用Dep庫中的depExportFunction()函數),Exe2工程的代碼以下.

//exe2main.cpp

#include <stdio.h>

//生成EXE/DLL時使用何種方法結果相同.
#pragma comment(lib, "../Debug/Lib.lib")

//聲明Lib工程中的函數.
void libExportFunction();

int main()
{
	printf("This is Exe2, and I want to call the function in Lib.\n");

	libExportFunction();
	return 0;
}

咱們的到了下面的錯誤輸出:

鏈接器居然替換了相同名稱的obj文件而不是合併(後面沒有找到Dep中函數的符號也證實了僅連接了目標工程中的obj文件,依賴庫中的同名obj文件被忽略)!

試想一下你本身的代碼,若是你寫的靜態庫被別人連接,而你恰巧在stdafx.cpp中書寫了大量的代碼(我知道這不科學,但有人這麼作),又恰巧連接你的靜態庫的人也要生成一個靜態庫,他也開啓了預編譯頭,使用stdafx.cpp,那麼抱歉,他生成的stdafx.obj將會被連接到目標庫中,而你提供的依賴庫中的stdafx.obj將會被忽略,多麼悲催的事情。

前面的實驗好像隱約印證了庫文件中所包含的obj文件和文件名有某種聯繫,那麼咱們還能夠換個姿式繼續調戲,咱們在Exe2工程中創建一個子目錄Exe2Sub,在該目錄中新建一個與原來Exe2工程中同名的cpp文件(exe2main.cpp),文件中只須要定義一個空函數便可,而後將這個子目錄添加到Exe2工程中。

工程中兩個exe2main.cpp同名,但不在同一個目錄下,咱們註釋掉Exe2工程對於Lib庫的連接雜注,接着編譯一下這個工程,觀察結果:

MSBUILD提示你發生了一個警告,由於不管源文件組織的方式如何,最終obj文件都會放到一個目錄裏,這個目錄中發現了兩個同名的obj,那麼後生成的obj會覆蓋早先生成的obj,因此MSBUILD要告知你這可能引起錯誤。緊接着錯誤就來了,很不幸,咱們的包含空函數的exe2main.obj忽略了包含main函數的exe2main.obj,鏈接器找不到主函數了,連接器蒙圈了!

是否是很刺激,僅僅是連接這個環節還會有這麼多好玩的地方?實際上在軟件開發過程當中,咱們極可能會遇到其中的一到兩個問題,殊不知道緣由,仔細分析這篇文章相信你可以從中得到一些收益。對於上面討論的各類問題,我在個人筆記中有簡略的總結,但願能夠幫助到還在被連接器這麼個程序猿們:

一、#pragma雜注會寫入一條連接指令到目標LIB中,該連接指令指明目標LIB依賴源LIB,但並不會將源LIB中的OBJ連接到目標LIB中,待目標LIB被連接至EXE/DLL時,若源LIB不存在,則連接失敗,找不到#pragma雜注所指明的源LIB。

二、附加依賴項會將源LIB中的OBJ文件連接至目標LIB,但在連接過程當中若是目標LIB中的OBJ文件與源LIB中的OBJ文件同名,則目標LIB中的OBJ替換源LIB中的同名OBJ文件,在此種狀況下,目標LIB被連接到EXE/DLL時,須要單獨連接被覆蓋的源LIB中的OBJ文件,不然會出現連接錯誤,找不到對應的符號。

三、當目標LIB庫應用了源LIB中的符號時,若忘記書寫#pragma雜注,或忘記添加項目依賴項,生成目標LIB的過程不會被中斷,也不會報錯,源LIB中的OBJ不會被連接到目標LIB中,在使用目標LIB生成EXE/DLL時,必須同時將源LIB也帶上,不然出現連接錯誤,找不到對應的符號。

四、當源LIB被兩次以上間接連接到目標LIB或EXE/DLL時,鏈接器會忽略相同符號中的一個,但不肯定是哪個,須要手動明確指定忽略的LIB,不然可能致使新版LIB被舊版LIB代替的問題。

相關文章
相關標籤/搜索