平台:Windows7,Visual C++ 2010
1. 引言
實驗室的一個項目,用到OpenGL進行實時繪制,還用到一些其他的庫,一個困擾我很久的問題就是編譯時遇到的各種符號未定義,符號重定義之類的鏈接錯誤,其一般形式如下:
xxx.obj : error LNK2019: 無法解析的外部符號__xx_xxx@xx,該符號在函數 _xxx 中被引用
MSVCRTD.lib(ti_inst.obj) : error LNK2005: "private: class type_info & __thiscall type_info::operator=(class type_info const &)" (??4type_info@@AAEAAV0@ABV0@@Z) 已經在 LIBCMTD.lib(typinfo.obj) 中定義
簡單的說,這種問題一般是缺少庫(library,或庫的版本不對)或多個庫引用的CRT(C run-time library,C語言運行庫)不一致造成的。本文對這一問題做簡要探討,並用glew、freeglut庫的配置作為例子。
2. 靜態鏈接庫、動態鏈接庫、CRT、STL
我們要到一個函數,要么是需要該函數的源代碼,要么是知道該函數的聲明並有該函數的實現,這里的“實現”又分為靜態鏈接庫、動態鏈接庫。在windows平台上,靜態鏈接庫對應以.lib為后綴的庫文件,動態鏈接庫對應.dll為后綴的動態鏈接庫文件。關於靜態鏈接庫、動態鏈接庫請參考wikipedia相應條目:
http://en.wikipedia.org/wiki/Static_library
http://en.wikipedia.org/wiki/Dynamic-link_library
我們用VC++寫的程序默認編譯為可執行文件(.exe),如果想發布自己的庫,可以在VS的“項目屬性 >> 配置屬性 >> 常規 >> 配置類型”修改。這樣如果以后想用這些函數就不需要引入對應.cpp文件,而只需包含帶有該函數聲明的頭文件,並引用庫文件即可——對於靜態鏈接庫,可以用“#pragma comment (lib, "xxx.lib")”指令,或在VS的“項目屬性 >> 配置屬性 >> 鏈接器 >> 輸入 >> 附加依賴”中添加;對於動態鏈接庫,可以用“__declspec(dllimport)”聲明要用的函數,如果為.dll文件實現了導入庫(對應的.lib文件,里面實現了函數導入,使用同靜態鏈接庫),則動態庫的使用同靜態庫,只是程序執行時需要.dll文件。msdn上有靜態庫和動態庫的使用教程:
http://msdn.microsoft.com/en-us/library/ms235627.aspx
http://msdn.microsoft.com/en-us/library/ms235636.aspx
簡單總結,可執行文件(.exe)和庫文件(.lib、.dll)都含有源代碼編譯出來的可執行二進制代碼。靜態鏈接和動態鏈接的區別在於:靜態鏈接編譯出的可執行代碼體積較大,動態鏈接編譯出的可執行代碼執行時依賴對應的.dll文件。
CRT(C語言運行庫)實現了C語言相關初始化代碼以及實現了C函數庫,C++可以看做C語言的超集,所以C++並沒有“CPRT(C++運行庫)”,C++也使用CRT,標准C++除CRT外還實現了STL(standard C++ library,C++標准庫,注意STL是Standard Template Library的縮寫,因為C++標准庫主要是用模板實現的)。既然函數的“實現”至少有靜態和動態之分,那CRT或STL也有不止一個版本,后文針對VC2010平台討論這些版本。
總結,CRT是C語言函數庫及初始化代碼的實現,STL是C++標准庫的實現,所謂“實現”就是由源代碼編譯出來的.lib、.dll文件等。
3. VS的編譯選項
在VC2010上,CRT和STL至少分為靜態和動態,靜態和動態中又各自有Debug和Release版本(早期VC還有單線程和多線程之分,目前VC++中只提供多線程版本),這樣CRT和STL都有至少四個版本。現在來解釋引言中的符號未定義、符號重定義鏈接錯誤的可能情景,程序A中調用了函數f,函數f是在程序B中編寫的,為了使用f,將程序B編譯為庫(而非.exe)——靜態庫:B.lib\動態庫:B.lib、B.dll,程序A為了使用f,包含頭文件B.h(其中有函數f的聲明)並引用B.lib:
1 #include"B.h" 2 #pragma comment (lib, "B.lib")
如果沒有上面的第二句代碼,則出現了符號未定義的鏈接錯誤:
main.obj : error LNK2019: 無法解析的外部符號 _f@0,該符號在函數 _main 中被引用
上面錯誤信息中的“_f@0”具體取決於函數調用約定的命名方式(_cdecl、_stdcall等)。
如果編譯程序B時使用了動態版本的CRT而編譯A時使用的是靜態版本CRT(即A、B使用了不同版本的CRT),則出現了符號重定義之類的鏈接錯誤(不絕對)。
當然如果用動態鏈接版本的B,程序A運行時可執行文件搜索路徑中必須包含B.dll,否則報告“丟失xxx.dll”之類的錯誤。
設置程序到底使用哪個版本的CRT可在VS的“項目屬性 >> 配置屬性 >> C/C++ >> 代碼生成 >> 運行庫”中設置,現在將幾種設置對應的庫文件,編譯器的宏定義列在下表:
| Option |
Preprocessor directives |
C run-time library (without iostream or standard C++ library) |
Standard C++ Library |
| /MT |
_MT |
libcmt.lib |
LIBCPMT.LIB |
| /MD |
_MT, _DLL |
msvcrt.lib (import library for MSVCR100.DLL) |
MSVCPRT.LIB (import library for MSVCP100.dll) |
| /MTd |
_DEBUG, _MT |
libcmtd.lib |
LIBCPMTD.LIB |
| /MDd |
_DEBUG, _MT, _DLL |
msvcrtd.lib (import library for MSVCR100D.DLL) |
MSVCPRTD.LIB (import library for MSVCP100D.DLL) |
其中,MT為是multi-thread的縮寫,上面說了,所有這些庫都是多線程的,大寫D代表DLL,小寫d代表debug,如/MDd下引用動態鏈接調試版本的庫,並且編譯器定義宏_DEBUG, _MT, _DLL(程序中可以用#ifdef指令來判斷庫版本),引用的CRT實現文件為MSVCPRTD.LIB,該文件只是導入庫並沒有具體的執行二進制代碼,程序運行時動態鏈接MSVCP100D.DLL文件,STL實現文件同理。
文件名“MSVC[R,P]100[D]”中的“100”對應VC2010,VC2003、VC2005、VC2008、VC2010、VC2012分別為71、80、90、100、110,有些時候我們運行一個程序提示“丟失msvcrxxx.dll”,可以通過安裝對應VS來解決,如果不想安裝VS,也可通過安裝“Microsoft Visual C++ 20xx [SP1] Redistributable Package”來解決。
可參考msdn的C run-time libraries條目:
http://msdn.microsoft.com/en-us/library/vstudio/abx4dbyh(v=vs.100).aspx
4. 編譯glew
可到以下地址下載最新glew:
解壓后打開...\glew-1.10.0\build\vc10\glew.sln文件,可以看到有“glew_shared”和“glew_static”兩個項目,從右鍵屬性中可以看到它們分別生成動態和靜態的庫:


還可以看到debug和release配置下分別使用相應debug和release版本CRT:


博文寫到這里,發現一個問題,“glew_static”應該使用靜態版本的CRT,但從上圖看到,release下是靜態鏈接(/MT),但debug下怎么不是“/MTd”呢?(后面會進一步分析)
在使用glew是需要包含相應頭文件,並鏈接相應庫文件,將上面生成的四個版本的庫文件拷貝出來:

其中文件名中的s代表static,即靜態鏈接,d代表debug,即調試版本,不帶s的是動態鏈接版本,不帶d的是release版本,文件名可以從glew工程的配置“項目屬性 >> 常規 >> 目標文件名”中看到:

然后將...\glew-1.10.0\include\GL\下頭文件拷貝出來:

將頭文件所在路徑添加到到VC2010項目包含目錄中,有兩種方法:“項目屬性 >> 配置屬性 >> VC++目錄 >> 包含目錄”或“項目屬性 >> 配置屬性 >> C/C++ >> 常規 >> 附加包含目錄”,將庫文件所在路徑添加到到VC2010項目庫目錄中,也有兩種方法:“項目屬性 >> 配置屬性 >> VC++目錄 >> 庫目錄”或“項目屬性 >> 配置屬性 >> 鏈接器 >> 常規 >> 附加庫目錄”。
通過判斷CRT版本來引用不同庫(這樣避免CRT版本不一致):
1 #ifdef _DLL // dynamic link 2 #ifdef _DEBUG 3 #pragma comment (lib, "glew32d.lib") 4 #pragma comment (lib, "freeglutd.lib") 5 #else 6 #pragma comment (lib, "glew32.lib") 7 #pragma comment (lib, "freeglut.lib") 8 #endif 9 #else // static link 10 #ifdef _DEBUG 11 #pragma comment (lib, "glew32sd.lib") 12 #pragma comment (lib, "freeglutsd.lib") 13 #else 14 #pragma comment (lib, "glew32s.lib") 15 #pragma comment (lib, "freegluts.lib") 16 #endif 17 #define GLEW_STATIC 18 #define FREEGLUT_STATIC 19 #endif 20 #include "GL/glew.h" 21 #include "GL/freeglut.h"
上述代碼利用編譯器在不同配置(/MT、/MD、/MTd、/MDd)下內置的不同宏來判斷使用的CRT版本,並引用對應版本glew和freeglut庫版本。
這樣配置后編譯自己的程序不會再出現引言中的鏈接錯誤了,但有很多如下警告:
glew32s.lib(glew.obj) : warning LNK4099: 未找到 PDB“vc100.pdb”(使用“glew32s.lib(glew.obj)”或在“C:\Users\hll\Desktop\fluid 2014.01\Release\vc100.pdb”中尋找);正在鏈接對象,如同沒有調試信息一樣
將glew工程配置成不生成調試信息,或把調試信息直接生成到.obj文件中(而非.pdb文件)即可,“項目屬性 >> 配置屬性 >> C/C++ >> 常規 >> 調試信息格式”,空表示不生成調試信息,C7把調試信息直接生成到.obj文件中,默認的Zi生成.pdb文件:

接着上面說到的“glew_static”的配置問題(往上找那段綠色的話),在自己工程配置為“/MTd”時引用glew32sd.lib庫程序報錯如下:
1>------ 已啟動生成: 項目: exampleGL, 配置: Debug_static Win32 ------
1>生成啟動時間為 2014/1/15 17:42:55。
1>InitializeBuildStatus:
1> 正在對“Debug_static\exampleGL.unsuccessfulbuild”執行 Touch 任務。
1>ClCompile:
1> 所有輸出均為最新。
1>ManifestResourceCompile:
1> 所有輸出均為最新。
1>MSVCRTD.lib(ti_inst.obj) : error LNK2005: "private: __thiscall type_info::type_info(class type_info const &)" (??0type_info@@AAE@ABV0@@Z) 已經在 LIBCMTD.lib(typinfo.obj) 中定義
1>MSVCRTD.lib(ti_inst.obj) : error LNK2005: "private: class type_info & __thiscall type_info::operator=(class type_info const &)" (??4type_info@@AAEAAV0@ABV0@@Z) 已經在 LIBCMTD.lib(typinfo.obj) 中定義
1>LINK : warning LNK4098: 默認庫“MSVCRTD”與其他庫的使用沖突;請使用 /NODEFAULTLIB:library
1>C:\Users\hll\Desktop\exampleGL\Debug_static\exampleGL.exe : fatal error LNK1169: 找到一個或多個多重定義的符號
1>
1>生成失敗。
1>
1>已用時間 00:00:00.38
========== 生成: 成功 0 個,失敗 1 個,最新 0 個,跳過 0 個 ==========
利用上面VC2010編譯配置表(往上找加粗的表),配置為“/MTd”使用的是庫libcmtd.lib,而msvcrtd.lib是“/MDd”配置下使用的庫,解決上述符號重定義錯誤的一個方法如下:
#pragma comment (linker, "/NODEFAULTLIB:MSVCRTD.lib")
但很明顯,這不是漂亮的解決方法,如果我們“擅自”將“glew_static”的上述配置“/MDd”改為“/MTd” (還是往上找那段綠色的話),這個問題也會消失,看來這可能是glew發布版(1.10.0)的一個bug(除了剛分析的“glew_static” debug的配置“/MDd”改為“/MTd”,還有一處,“glew_shared” release的配置“/MT”改為“/MD”),但這正好成就了我們對本文技術分析結果的完美應用~
5. 編譯freeglut
可到以下地址下載最新freeglut:
http://freeglut.sourceforge.net/
有了glew編譯經驗,以及自己的工程配置經驗之后,freeglut的編譯這里就簡單些說了。
解壓后打開...\freeglut-2.8.1\VisualStudio\2010\freeglut.sln文件,可以看到它的配置略有不同:

再隨便打開一個CRT配置可以看到:

freeglut並沒有像glew那樣在CRT配置上出現小bug(還是往上找那段綠色的話)。
好了,像glew一樣,用配置管理器的4個選項(debug、release、debug_static、release_static,分別對應4個CRT版本)分別編譯出4個版本的庫(6個文件,4個.lib,2個.dll),但freeglut並沒有像glew那樣將4個版本的文件分別命名用或不用s及d結尾,它的debug版和release版文件名相同,我只好自己改啦(這一改帶來很多問題):

改為:

其他類推,並將freeglut_std.h文件中如下代碼:
... # pragma comment (lib, "freeglut_static.lib") ... # pragma comment (lib, "freeglut.lib") ...
修改為:
... # ifdef _DEBUG # pragma comment (lib, "freeglutsd.lib") # else # pragma comment (lib, "freegluts.lib") # endif ... # ifdef _DEBUG # pragma comment (lib, "freeglutd.lib") # else # pragma comment (lib, "freeglut.lib") # endif ...
修改依據相同,還是根據CRT的4個版本引用4個版本的.lib文件。注意,我之前在freeglut項目中只做了“目標文件名”的修改,而未做.h文件的上述修改來編譯freeglut(只是將.h文件拷貝出來后才修改,這樣自己項目包含的是修改后的freeglut_std.h文件,而編譯freeglut用的是原版),這樣的結果是,生成出來的.lib文件內部仍在引用"freeglut_static.lib"(而不是"freegluts.lib"),用二進制打開生成的.lib文件如下:

而使用修改后的freeglut_std.h文件編譯freeglut結果如下:

使用未修改的freeglut_std.h文件生成"freegluts.lib" 后,自己工程包含修改后的freeglut_std.h,按說只引用"freegluts.lib",但鏈接器仍報告找不到"freeglut_static.lib"文件。
另外一個類似的問題是,當編譯動態鏈接debug版本的庫時,生成文件為freeglutd.dll和freeglutd.lib(名字規則:非靜態不帶s,debug帶d),頭文件中引用"freeglutd.lib"將freeglutd.dll拷貝到VC2010自動生成的debug文件夾下(和自己工程生成的.exe文件同一文件夾),運行程序結果報告“丟失freeglut.dll”(不帶我自己修改后的名字的d),編譯freeglut生成的.lib和.dll文件名為freeglutd,但.lib文件內部引用的.dll文件名為freeglut(不帶d),驗證如下:

經過一番研究, freeglut的配置下,freeglutd.lib文件是鏈接器根據一個.def文件生成的(glew的導入庫配置在“項目屬性 >> 配置屬性 >> 鏈接器 >> 高級 >> 導入庫”):

.def文件內容如下:

經查,第一行“LIBRARY freeglut”的含義正是“引用freeglut.dll”,將該句去掉,鏈接器生成的.lib文件引用的.dll文件自動和生成的.dll文件同名,問題解決:

另外值得一提的是當生成動態鏈接版本的.dll文件時,用到了一個資源文件,其內容如下(glew中的):

6. 搭建OpenGL工程
工程原則:將glew和freeglut庫放在工程文件夾下以避免對環境依賴、不能出現任何關於庫沖突等警告(錯誤當然更不可以)、根據CRT的4個版本定義4個配置(debug,release,debug_static,release_static)。
將上面的glew和freeglut的編譯總結在下面:
glew—
1.bug修復,“glew_static” debug的配置“/MDd”改為“/MTd”,“glew_shared” release的配置“/MT”改為“/MD”
2.不生成調試信息,“glew_static”和“glew_shared”所有配置下的“調試信息格式”改為空
3.對“glew_static” debug及release 和 “glew_shared” debug及release分別編譯,得到glew32sd.lib、glew32s.lib、glew32d.lib(glew32d.dll)、glew32.lib(glew32.dll)
freeglut—
1.生成目標文件名修改,“freeglut”的“目標文件名”項原來為$(ProjectName)和$(ProjectName)_static,4個配置debug、release、debug_static、release_static分別改為$(ProjectName)d、$(ProjectName)、$(ProjectName)sd、$(ProjectName)s
2.不生成調試信息,“freeglut”所有配置下的“調試信息格式”改為空
3.freeglut_std.h文件修改如上述
4.freeglutdll.def文件刪去第一行的“LIBRARY freeglut”
5.對“freeglut”的4個配置debug、release、debug_static、release_static分別編譯,得到freeglutsd.lib、freegluts.lib、freeglutd.lib(freeglutd.dll)、freeglut.lib(freeglut.dll)
如下構造文件夾tool:
tool
freeglut-2.8.1
bin
freeglut.dll, freeglutd.dll
inc
GL
freeglut.h, freeglut_ext.h, freeglut_std.h, glut.h
lib
freeglut.lib, freeglutd.lib, freegluts.lib, freeglutsd.lib
glew-1.10.0
bin
glew32.dll, glew32d.dll
inc
GL
glew.h, glxew.h, wglew.h
lib
glew32.lib, glew32d.lib, glew32s.lib, glew32sd.lib
如下構造VC2010工程:
新建VS C++控制台項目,將上面tool文件夾拷貝到解決方案文件夾下
打開配置管理器,添加Debug_static(從Debug復制)和Release_static(從Release復制)配置
將Debug、Debug_static、Release、Release_static的“運行庫”分別配置為:/MDd、/MTd、/MD、/MT
在VS“項目屬性 >> 配置屬性 >> VC++目錄 >> 包含目錄”所有配置下添加如下項
$(SolutionDir)tool\glew-1.10.0\inc
$(SolutionDir)tool\freeglut-2.8.1\inc
在VS“項目屬性 >> 配置屬性 >> VC++目錄 >> 庫目錄”所有配置下添加如下項
$(SolutionDir)tool\glew-1.10.0\lib
$(SolutionDir)tool\freeglut-2.8.1\lib
添加文件gl_inc.h如下:

添加main.cpp如下:

程序運行結果截圖:

考慮到方便本文的讀者做實驗,現將搭建的OpenGL工程exampleGL貢獻出來(庸俗的代碼水准讓大家見笑了):
鏈接: http://pan.baidu.com/s/1kTuPUQz 密碼: jiky
7. 總結
在VC++上,CRT和STL有4個版本,分別對應編譯選項:/MDd、/MTd、/MD、/MT;
根據編譯選項的不同,開源程序編譯出的庫也分為多個版本(一般較全面的是4個,沒有4個的可以手動添加配置),這些版本鏈接不同的CRT;
應根據自己程序的編譯選項(用編譯器預置宏來判斷)鏈接對應的開源庫,否則很有可能出現符號未定義、符號重定義的鏈接錯誤。
