already defined in *.obj“符號已定義”問題原理及解決方案

VC6如果想在stdafx.h中定義全局變量,由於該頭文件會被include多次,所以,經常會出現以下經典的錯誤:already defined in StdAfx.obj。

解決方法:把該變量的定義int g_flag放到stdafx.cpp中,然後在使用的地方extern一下。假如你在CAADlg.cpp中使用了該變量g_flag,那麼就在 CAADlg.cpp的首部,構造函數的定義之外,添加上 extern int g_flag;

許多Visual C++的使用者都碰到過LNK2005:symbol already defined和LNK1169:one or more multiply defined symbols found這樣的鏈接錯誤,而且通常是在使用第三方庫時遇到的。對於這個問題,有的朋友可能不知其然,而有的朋友可能知其然卻不知其所以然,那麼本文就試圖爲大家徹底解開關於它的種種疑惑。

    大家都知道,從C/C++源程序到可執行文件要經歷兩個階段:(1)編譯器將源文件編譯成彙編代碼,然後由彙編器(assembler)翻譯成機器指令 (再加上其它相關信息)後輸出到一個個目標文件(object file,VC的編譯器編譯出的目標文件默認的後綴名是.obj)中;(2)鏈接器(linker)將一個個的目標文件(或許還會有若干程序庫)鏈接在一起生成一個完整的可執行文件。

    編譯器編譯源文件時會把源文件的全局符號(global symbol)分成強(strong)和弱(weak)兩類傳給彙編器,而隨後彙編器則將強弱信息編碼並保存在目標文件的符號表中。那麼何謂強弱呢?編譯器認爲函數與初始化了的全局變量都是強符號,而未初始化的全局變量則成了弱符號。比如有這麼個源文件:

extern int errorno;

int buf[2] = {1,2};

int *p;

int main()

{

   return 0;

}

其中main、buf是強符號,p是弱符號,而errorno則非強非弱,因爲它只是個外部變量的使用聲明。

    有了強弱符號的概念,鏈接器(Unix平臺)就會按如下規則(參考[1],p549~p550)處理與選擇被多次定義的全局符號:

規則1: 不允許強符號被多次定義(即不同的目標文件中不能有同名的強符號);

規則2: 如果一個符號在某個目標文件中是強符號,在其它文件中都是弱符號,那麼選擇強符號;

規則3: 如果一個符號在所有目標文件中都是弱符號,那麼選擇其中任意一個;

    雖然上述3條針對的是Unix平臺的鏈接器,但據作者試驗,至少VC6.0的linker也遵守這些規則。由此可知多個目標文件不能重複定義同名的函數與初始化了的全局變量,否則必然導致LNK2005和LNK1169兩種鏈接錯誤。可是,有的時候我們並沒有在自己的程序中發現這樣的重定義現象,卻也遇到了此種鏈接錯誤,這又是何解?嗯,問題稍微有點兒複雜,容我慢慢道來。

    衆所周知,ANSI C/C++ 定義了相當多的標準函數,而它們又分佈在許多不同的目標文件中,如果直接以目標文件的形式提供給程序員使用的話,就需要他們確切地知道哪個函數存在於哪個目標文件中,並且在鏈接時顯式地指定目標文件名才能成功地生成可執行文件,顯然這是一個巨大的負擔。所以C語言提供了一種將多個目標文件打包成一個文件的機制,這就是靜態程序庫(static library)。開發者在鏈接時只需指定程序庫的文件名,鏈接器就會自動到程序庫中尋找那些應用程序確實用到的目標模塊,並把(且只把)它們從庫中拷貝出來參與構建可執行文件。幾乎所有的C/C++開發系統都會把標準函數打包成標準庫提供給開發者使用(有不這麼做的嗎?)。

    程序庫爲開發者帶來了方便,但同時也是某些混亂的根源。我們來看看鏈接器(Unix平臺)是如何解析(resolve)對程序庫的引用的(參考[1],p556)。

   

    在符號解析(symbol resolution)階段,鏈接器按照所有目標文件和庫文件出現在命令行中的順序從左至右依次掃描它們,在此期間它要維護若干個集合:(1)集合E是將被合併到一起組成可執行文件的所有目標文件集合;(2)集合D是所有之前已被加入E的目標文件定義的符號集合;(3)集合U是未解析符號 (unresolved symbols,即那些被E中目標文件引用過但在D中還不存在的符號)的集合。一開始,E、D、U都是空的。

(1): 對命令行中的每一個輸入文件f,鏈接器確定它是目標文件還是庫文件,如果它是目標文件,就把f加入到E,並把f中未解析的符號和已定義的符號分別加入到U、D集合中,然後處理下一個輸入文件。

(2): 如果f是一個庫文件,鏈接器會嘗試把U中的所有未解析符號與f中各目標模塊定義的符號進行匹配。如果某個目標模塊m定義了一個U中的未解析符號,那麼就把 m加入到E中,並把m中未解析的符號和已定義的符號分別加入到U、D集合中。不斷地對f中的所有目標模塊重複這個過程直至到達一個不動點(fixed point),此時U和D不再變化。而那些未加入到E中的f裏的目標模塊就被簡單地丟棄,鏈接器繼續處理下一輸入文件。

(3): 當掃描完所有輸入文件時如果U非空或者有同名的符號被多次加入D,鏈接器報告錯誤信息並退出。否則,它把E中的所有目標文件合併在一起生成可執行文件。

    上述規則針對的是Unix平臺鏈接器,而VC(至少VC6.0)linker則有相當的不同: 它首先依次處理命令行中出現的所有目標文件,然後依照順序不停地掃描所有的庫文件,直至U爲空或者某遍(從頭到尾依次把所有的庫文件掃描完稱爲一遍)掃描過程中U、D無任何變化時結束掃描,此刻再根據U是否爲空以及是否有同名符號重複加入D來決定是出錯退出還是生成可執行文件。很明顯Unix鏈接器對輸入文件在命令行中出現的順序十分敏感,而VC的算法則可最大限度地減少文件順序對鏈接的影響。作者不清楚Unix下新的開發工具是否已經改進了相應的做法,歡迎有實踐經驗的朋友補充這方面的信息(補充於2005年10月10日: 經試驗,使用gcc 3.2.3的MinGW 3.1.0的鏈接器表現與參考[1]描述的一致)。

    VC帶的編譯器是cl.exe,它有這麼幾個與標準程序庫有關的選項: /ML、/MLd、/MT、/MTd、/MD、/MDd。這些選項告訴編譯器應用程序想使用什麼版本的C標準程序庫。/ML(缺省選項)對應單線程靜態版的標準程序庫(libc.lib);/MT對應多線程靜態版標準庫(libcmt.lib),此時編譯器會自動定義_MT宏;/MD對應多線程DLL版 (導入庫msvcrt.lib,DLL是msvcrt.dll),編譯器自動定義_MT和_DLL兩個宏。後面加d的選項都會讓編譯器自動多定義一個 _DEBUG宏,表示要使用對應標準庫的調試版,因此/MLd對應調試版單線程靜態標準庫(libcd.lib),/MTd對應調試版多線程靜態標準庫 (libcmtd.lib),/MDd對應調試版多線程DLL標準庫(導入庫msvcrtd.lib,DLL是msvcrtd.dll)。雖然我們的確在編譯時明白無誤地告訴了編譯器應用程序希望使用什麼版本的標準庫,可是當編譯器幹完了活,輪到鏈接器開工時它又如何得知一個個目標文件到底在思念誰?爲了傳遞相思,我們的編譯器就幹了點祕密的勾當。在cl編譯出的目標文件中會有一個專門的區域(關心這個區域到底在文件中什麼地方的朋友可以參考COFF和 PE文件格式)存放一些指導鏈接器如何工作的信息,其中有一項就叫缺省庫(default library),它指定了若干個庫文件名,當鏈接器掃描該目標文件時將按照它們在目標模塊中出現的順序處理這些庫名: 如果該庫在當前輸入文件列表中還不存在,那麼便把它加入到輸入文件列表末尾,否則略過。說到這裏,我們先來做個小實驗。寫個頂頂簡單的程序,然後保存爲 main.c :

/* main.c */

int main() { return 0; }

用下面這個命令編譯main.c(什麼?你從不用命令行來編譯程序?這個......) :

cl /c main.c

/c是告訴cl只編譯源文件,不用鏈接。因爲/ML是缺省選項,所以上述命令也相當於: cl /c /ML main.c 。如果沒什麼問題的話(要出了問題纔是活見鬼!當然除非你的環境變量沒有設置好,這時你應該去VC的bin目錄下找到vcvars32.bat文件然後運行它。),當前目錄下會出現一個main.obj文件,這就是我們可愛的目標文件。隨便用一個文本編輯器打開它(是的,文本編輯器,大膽地去做別害怕),搜索"defaultlib"字符串,通常你就會看到這樣的東西: "-defaultlib:LIBC -defaultlib:OLDNAMES"。啊哈,沒錯,這就

是保存在目標文件中的缺省庫信息。我們的目標文件顯然指定了兩個缺省庫,一個是單線程靜態版標準庫libc.lib(這與/ML選項相符);一個是oldnames.lib(它是爲了兼容微軟以前的C/C++開發系統,基本不用了,爲了簡化討論可以忽略它)。另外,如果在源程序中用了

/* xxxx代表實際的庫文件名 */

#pragma comment(lib,"xxxx")

編譯指示命令(compiler directive)指定要鏈接的庫,那麼這個信息也會被保存到目標文件的缺省庫信息項中,且位於缺省標準庫之前。如果有多個這樣的命令,那麼對應庫名在目標文件中出現的順序與它們在源程序中出現的順序完全一致(且都在缺省標準庫之前)。

    VC的鏈接器是link.exe,因爲main.obj保存了缺省庫信息,所以可以用

link main.obj libc.lib

或者

link main.obj

來生成可執行文件main.exe,這兩個命令是等價的。但是如果你用

link main.obj libcd.lib

的話,鏈接器會給出一個警告: "warning LNK4098: defaultlib "LIBC" conflicts with use of other libs; use /NODEFAULTLIB:library",因爲你顯式指定的標準庫版本與目標文件的缺省值不一致。通常來說,應該保證鏈接器合併的所有目標文件指定的缺省標準庫版本一致,否則編譯器一定會給出上面的警告,而LNK2005和LNK1169鏈接錯誤則有時會出現有時不會。那麼這個有時到底是什麼時候?呵呵,彆着急,下面的一切正是爲喜歡追根究底的你準備的。

    建一個源文件,就叫mylib.c,內容如下:

/* mylib.c */

#include <stdio.h>

void foo(void)

{

   printf("%s","I am from mylib!/n");

}

cl /c /MLd mylib.c

命令編譯,注意/MLd選項是指定libcd.lib爲默認標準庫。lib.exe是VC自帶的用於將目標文件打包成程序庫的命令,所以我們可以用

lib /OUT:my.lib mylib.obj

將mylib.obj打包成庫,輸出的庫文件名是my.lib。接下來把main.c改成:

/* main.c */

void foo(void);

int main()

{

   foo();

   return 0;

}

cl /c main.c

編譯,然後用

link main.obj my.lib

進行鏈接。這個命令能夠成功地生成main.exe而不會產生LNK2005和 LNK1169鏈接錯誤,你僅僅是得到了一條警告信息:"warning LNK4098: defaultlib "LIBCD" conflicts with use of other libs; use /NODEFAULTLIB:library"。我們根據前文所述的掃描規則來分析一下鏈接器此時做了些啥(加一個/VERBOSE選項就可以看到詳盡的鏈接過程,但要注意,幾乎所有的C編譯器都會在符號前加一個下劃線後再輸出,所以在目標文件和鏈接輸出信息中看到的符號名都比在源程序中見到的多出一個 '_',此點不可不察。)。

    一開始E、U、D都是空集。鏈接器首先掃描main.obj,把它的默認標準庫libc.lib加入到輸入文件列表末尾,它自己加入E集合,同時未解析的 foo加入U,main加入D。接着掃描my.lib,因爲這是個庫,所以會拿當前U中的所有符號(當然現在就一個foo)與my.lib中的所有目標模塊(當然也只有一個mylib.obj)依次匹配,看是否有模塊定義了U中的符號。結果mylib.obj確實定義了foo,於是它加入到E,foo從U 轉移到D,未解析的printf加入到U,指定的默認標準庫libcd.lib也加到輸入文件列表末尾(在libc.lib之後)。不斷地在my.lib 庫的各模塊上進行迭代以匹配U中的符號,直到U、D都不再變化。很明顯,現在就已經到達了這麼一個不動點,所以接着掃描下一個輸入文件,就是 libc.lib。鏈接器發現libc.lib裏的printf.obj裏定義有printf,於是printf從U移到D,printf.obj加入到 E,它定義的所有符號加入到D,它裏頭的未解析符號加入到U。如果鏈接時沒有指定/ENTRY(程序入口點選項),那麼鏈接器默認的入口點就是函數 mainCRTStartup(GUI程序的默認入口點則是WinMainCRTStartup),它在crt0.obj中被定義,所以crt0.obj 及它直接或間接引用的模塊(比如malloc.obj、free.obj等)都被加入到E中,這些目標模塊指定的默認庫(只crt0init.obj指定了kernel32.lib)加到輸入文件列表末尾,同時更新U和D。不斷匹配libc.lib中各模塊直至到達不動點,然後處理libcd.lib,但是它裏面的所有目標模塊都沒有定義U中的任何一個符號,所以鏈接器略過它進入到最後一個輸入文件kernel32.lib。事實上,U中已有和將要加入的未解析符號都可以在其中找到定義,那麼當處理完kernel32.lib時,U必然爲空,於是鏈接器合併E中的所有模塊生成可執行文件。

    上文描述了雖然各目標模塊指定了不同版本的缺省標準庫但仍然鏈接成功的例子,接下來你將目睹因爲這種不嚴謹而導致的悲慘失敗。

    修改mylib.c成這個樣子:

#include <crtdbg.h>

void foo(void)

{

   // just a test , don't care memory leak

   _malloc_dbg( 1, _NORMAL_BLOCK, __FILE__, __LINE__ );

}

其中_malloc_dbg不是ANSI C的標準庫函數,它是VC標準庫提供的malloc的調試版,與相關函數配套能幫助開發者抓各種內存錯誤。使用它一定要定義_DEBUG宏,否則預處理器會把它自動轉爲malloc。繼續用

cl /c /MLd mylib.c

lib /OUT:my.lib mylib.obj

編譯打包。當再次用

link main.obj my.lib

進行鏈接時,我們看到了什麼?天哪,一堆的LNK2005加上個貴爲"fatal error"的LNK1169墊底,當然還少不了那個LNK4098。鏈接器是不是瘋了?不,你冤枉可憐的鏈接器了,我拍胸脯保證它可是一直在盡心盡責地照章辦事。

    一開始E、U、D爲空,鏈接器掃描main.obj,把libc.lib加到輸入文件列表末尾,把main.obj加進E,把foo加進U,把main加進D。接着掃描my.lib,於是mylib.obj加入E,libcd.lib加到輸入文件列表末尾,foo從U轉移到D,_malloc_dbg加進 U。然後掃描libc.lib,這時會發現libc.lib裏任何一個目標模塊都沒有定義_malloc_dbg(它只在調試版的標準庫中存在),所以不會有任何一個模塊因爲_malloc_dbg而加入E。但因爲libc.lib中的crt0.obj定義了默認入口點函數mainCRTStartup,所以crt0.obj及它直接或間接引用的模塊(比如malloc.obj、free.obj等)都被加入到E中,這些目標模塊指定的默認庫(只 crt0init.obj指定了kernel32.lib)加到輸入文件列表末尾,同時更新U和D。不斷匹配libc.lib中各模塊直至到達不動點後再處理libcd.lib,發現dbgheap.obj定義了_malloc_dbg,於是dbgheap.obj加入到E,它的未解析符號加入U,它定義的所有其它符號加入D,這時災難便來了。之前malloc等符號已經在D中(隨着libc.lib裏的malloc.obj加入E而加入的),而 dbgheap.obj及因它而引入的其它模塊又定義了包括malloc在內的許多同名符號,導致了重定義衝突。所以鏈接器在處理完所有輸入文件(是的,即使中途有重定義衝突它也會處理所有的文件以便生成一個完整的衝突列表)後只好報告: 這活兒沒法兒幹。

    現在我們該知道,鏈接器完全沒有責任,責任在我們自己的身上。是我們粗心地把缺省標準庫版本不一致的目標文件(main.obj)與程序庫 (my.lib)鏈接起來,引發了大災難。解決辦法很簡單,要麼用/MLd選項來重編譯main.c;要麼用/ML選項重編譯mylib.c;再或者乾脆在鏈接時用/NODEFAULTLIB:XXX選項忽略默認庫XXX,但這種方法非常不保險(想想爲什麼?),所以不推薦。

    在上述例子中,我們擁有庫my.lib的源代碼(mylib.c),所以可以用不同的選項重新編譯這些源代碼並再次打包。可如果使用的是第三方的庫,它並沒有提供源代碼,那麼我們就只有改變自己程序的編譯選項來適應這些庫了。但是如何知道庫中目標模塊指定的默認庫呢?其實VC提供的一個小工具便可以完成任務,這就是dumpbin.exe。運行下面這個命令

dumpbin /DIRECTIVES my.lib

然後在輸出中找那些"Linker Directives"引導的信息,你一定會發現每一處這樣的信息都會包含若干個類似"-defaultlib:XXXX"這樣的字符串,其中XXXX便代表目標模塊指定的缺省庫名(注意,如果在編譯時指定了/Zl選項,那麼目標模塊中將不會有defaultlib信息)。

    知道了第三方庫指定的默認標準庫,再用合適的選項編譯我們的應用程序,就可以避免LNK2005和LNK1169鏈接錯誤。喜歡IDE的朋友,你一樣可以到 "Project屬性" -> "C/C++" -> "代碼生成(code generation)" -> "運行時庫(run-time library)" 項下設置應用程序的默認標準庫版本,這與命令行選項的效果是一樣的。

參考資料:

[1] 《Computer Systems: A Programmer's Perspective》

    著:  Randal E. Bryant, David R. O'Hallaron

    電子工業出版社,2004

.net中的編譯問題,出現諸如:

(MSVCR80D.dll)  :  error  LNK2005:  __CrtDbgReport  already  defined  in  libcmtd.lib(dbgrpt.obj) 

msvcrtd.lib(MSVCR80D.dll)  :  error  LNK2005:  _memmove  already  defined  in  libcmtd.lib(memmove.obj) 

的解決辦法:

編程中經常能遇到LNK2005錯誤——重複定義錯誤,其實LNK2005錯誤並不是一個很難解決的錯誤。弄清楚它形成的原因,就可以輕鬆解決它了。 

 

造成LNK2005錯誤主要有以下幾種情況: 

1.重複定義全局變量。可能存在兩種情況: 

A、對於一些初學編程的程序員,有時候會以爲需要使用全局變量的地方就可以使用定義申明一下。其實這是錯誤的,全局變量是針對整個工程的。正確的應該是在一個CPP文件中定義如下:int  g_Test;那麼在使用的CPP文件中就應該使用:extern  int  g_Test即可,如果還是使用int  g_Test,那麼就會產生LNK2005錯誤,一般錯誤錯誤信息類似:AAA.obj  error  LNK2005  int  book  c?book@@3HA  already  defined  in  BBB.obj。切記的就是不能給變量賦值否則還是會有LNK2005錯誤。 

             這裏需要的是“聲明”,不是“定義”!根據C++標準的規定,一個變量是聲明,必須同時滿足兩個條件,否則就是定義: 

(1)聲明必須使用extern關鍵字;(2)不能給變量賦初值 

所以,下面的是聲明: 

extern  int  a; 

下面的是定義 

int  a;  int  a  =  0;  extern  int  a  =0; 

B、對於那麼編程不是那麼嚴謹的程序員,總是在需要使用變量的文件中隨意定義一個全局變量,並且對於變量名也不予考慮,這也往往容易造成變量名重複,而造成LNK2005錯誤。 

 

2.頭文件的包含重複。往往需要包含的頭文件中含有變量、函數、類的定義,在其它使用的地方又不得不多次包含之,如果頭文件中沒有相關的宏等防止重複鏈接的措施,那麼就會產生LNK2005錯誤。解決辦法是在需要包含的頭文件中做類似的處理:#ifndef  MY_H_FILE      //如果沒有定義這個宏 

#define  MY_H_FILE      //定義這個宏 

…….      //頭文件主體內容 

……. 

#endif 

上面是使用宏來做的,也可以使用預編譯來做,在頭文件中加入: 

#pragma  once 

//頭文件主體 

3.使用第三方的庫造成的。這種情況主要是C運行期函數庫和MFC的庫衝突造成的。具體的辦法就是將那個提示出錯的庫放到另外一個庫的前面。另外選擇不同的C函數庫,可能會引起這個錯誤。微軟和C有兩種C運行期函數庫,一種是普通的函數庫:LIBC.LIB,不支持多線程。另外一種是支持多線程的:msvcrt.lib。如果一個工程裏,這兩種函數庫混合使用,可能會引起這個錯誤,一般情況下它需要MFC的庫先於C運行期函數庫被鏈接,因此建議使用支持多線程的msvcrt.lib。所以在使用第三方的庫之前首先要知道它鏈接的是什麼庫,否則就可能造成LNK2005錯誤。如果不得不使用第三方的庫,可以嘗試按下面所說的方法修改,但不能保證一定能解決問題,前兩種方法是微軟提供的: 

A、選擇VC菜單 Project->Settings->Link->Catagory選擇Input,再在Ignore  libraries  的Edit欄中填入你需要忽略的庫,如:Nafxcwd.lib;Libcmtd.lib。然後在Object/library  Modules的Edit欄中填入正確的庫的順序,這裏需要你能確定什麼是正確的順序,呵呵,God  bless  you! 

B、選擇VC菜單Project->Settings->Link頁,然後在Project  Options的Edit欄中輸入/verbose:lib,這樣就可以在編譯鏈接程序過程中在輸出窗口看到鏈接的順序了。 

C、選擇VC菜單Project->Settings->C/C++頁,Catagory選擇Code  Generation後再在User  Runtime  libraray中選擇MultiThread  DLL等其他庫,逐一嘗試。 

關於編譯器的相關處理過程,參考: 

http://www.donews.net/xzwenlan/archive/2004/12/23/211668.aspx 

 

這就是我所遇到過的LNK2005錯誤的幾種情況,肯定還有其他的情況也可能造成這種錯誤,所以我不希望你在看完這篇文章以後,再遇到LNK2005錯誤時候,不動腦筋的想對號入座的排除錯誤。編程的過程就是一個思考的過程,所以還是多多開動你的頭腦,那樣收穫會更多!

附錄:

編譯器處理相關

一.預處理器-編譯器-彙編器-鏈接器

預處理器會處理相關的預處理指令,一般是以"#"開頭的指令。如:#include "xx.h" #define等。

編譯器把對應的*.cpp翻譯成*.s文件(彙編語言)。

彙編器則處理*.s生成對應的*.o文件(obj目標文件)

最後鏈接器把所有的*.o文件鏈接成一個可執行文件(?.exe)

1.部件:

首先要知道部件(可以暫且狹義地理解爲一個類)一般分爲頭文件(我喜歡稱爲接口,如:*.h)及實現文件(如:*.cpp)。

一般頭文件會是放一些用來作聲明的東東作爲接口而存在的。

而實現文件主要是實現的具體代碼。

2.編譯單個文件:

記住IDE在bulid文件時只編譯實現文件(如*.cpp)來產生obj,在vc下你可以對某個?.cpp按下ctrl+f7單獨編譯它

生成對應一個?.obj文件。在編譯?.cpp時IDE會在?.cpp中按順序處理用#include包括進來的頭文件

(如果該頭文件中又#include有文件,同樣會按順序跟進處理各個頭文件,如此遞歸。。)

3.內部鏈接與外部鏈接:

內、外鏈接是比較基礎的東東,但是也是新手最容易錯的地方,所以這裏有必要祥細討論一下。

內部鏈接產生的符號只在本地?.obj中可見,而外部鏈接的符號是所有*.obj之間可見的。

如:用inline的是內部鏈接,在文件頭中直接聲明的變量、不帶inline的全局函數都是外部鏈接。

在文件頭中類的內部聲明的函數(不帶函數體)是外部鏈接,而帶函數體一般會是內部鏈接(因爲IDE會盡量把它作爲內聯函數)

認識內部鏈接與外部鏈接有什麼作用呢?下面用vc6舉個例子:

// 文件main.cpp內容:

void main(){}

// 文件t1.cpp內容:

#include "a.h"

void Test1(){ Foo(); }

// 文件t2.cpp內容:

#include "a.h"

void Test2(){ Foo(); }

// 文件a.h內容:

void Foo( ){ }

好,用vc生成一個空的console程序(File - new - projects - win32 console application),並關掉預編譯選項開關

(project - setting - Cagegoryrecompiled Headers - Not using precompiled headers)

現在你打開t1.cpp按ctrl+f7編譯生成t1.obj通過

打開t2.cpp按ctrl+f7編譯生成t2.obj通過

而當你鏈接時會發現:

Linking...

t2.obj : error LNK2005: "void __cdecl Foo(void)" (?Foo@@YAXXZ) already defined in t1.obj

這是因爲:

1. 編譯t1.cpp在處理到#include "a.h"中的Foo時看到的Foo函數原型定義是外部鏈接的,所以在t1.obj中記錄Foo符號是外部的。

2. 編譯t2.cpp在處理到#include "a.h"中的Foo時看到的Foo函數原型定義是外部鏈接的,所以在t2.obj中記錄Foo符號是外部的。

3. 最後在鏈接 t1.obj 及 t2.obj 時, vc發現有兩處地方(t1.obj和t2.obj中)定義了相同的外部符號(注意:是定義,外部符號可以

多處聲明但不可多處定義,因爲外部符號是全局可見的,假設這時有t3.cpp聲明用到了這個符號就不知道應該調用t1.obj

中的還是t2.obj中的了),所以會報錯。

解決的辦法有幾種:

a.將a.h中的定義改寫爲聲明,而用另一個文件a.cpp來存放函數體。(提示:把上述程序改來試試)

(函數體放在其它任何一個cpp中如t1.cpp也可以,不過良好的習慣是用對應cpp文件來存放)。

這時包括a.h的文件除了a.obj中有函數體代碼外,

其它包括a.h的cpp生成的obj文件都只有對應的符號而沒有函數體,如t1.obj、t2.obj就只有符號,當最後鏈接時IDE會把

a.obj的Foo()函數體鏈接進exe文件中,並把t1.obj、t2.obj中的Foo符號轉換成對應在函數體exe文件中的地址。

另外:當變量放在a.h中會變成全局變量的定義,如何讓它變爲聲明呢?

例如: 我們在a.h中加入:class CFoo{};CFoo* obj;

這時按f7進行build時出現:

Linking...

t2.obj : error LNK2005: "class CFoo * obj" (?obj@@3PAVCFoo@@A) already defined in t1.obj

一個好辦法就是在a.cpp中定義此變量( CFoo* obj,然後拷貝此定義到a.h文件中並在前面加上extern(extern CFoo* obj

如此就可通過了。當然extern也可以在任何調用此變量的位置之前聲明,不過強烈建議不要這麼作,因爲到處作用extern,會

導致接口不統一。良好的習慣是接口一般就放到對應的頭文件。

b. 將a.h中的定義修改成內部鏈接,即加上inline關鍵字,這時每個t1.obj和t2.obj都存放有一份Foo函數體,但它們不是外部

符號,所以不會被別的obj文件引用到,故不存在衝突。(提示:把上述程序改來試試)

另外我作了個實驗來驗證”vc是把是否是外部符號的標誌記錄在obj文件中的“(有點繞口)。可以看看,如下:

(1)文件內容:

// 文件main.cpp內容:

void main(){}

// 文件t1.cpp內容:

#include "a.h"

void Test1(){ Foo(); }

// 文件t2.cpp內容:

#include "a.h"

void Test2(){ Foo(); }

// 文件a.h內容:

inline void Foo( ){ }

(2) 選t1.cpp按ctrl+f7單獨編譯,並把編譯後的t1.obj修改成t1.obj_inline

(3) 選t2.cpp按ctrl+f7單獨編譯,並把編譯後的t2.obj修改成t2.obj_inline

(4) 把除了t1.obj_inline及t2.obj_inline外的其它編譯生成的文件刪除。

(5) 修改a.h內容爲:void Foo( ){ },使之變爲非內聯函數作測試

(6) rebuild all所有文件。這時提示:

Linking...

t2.obj : error LNK2005: "void __cdecl Foo(void)" (?Foo@@YAXXZ) already defined in t1.obj

Debug/cle.exe : fatal error LNK1169: one or more multiply defined symbols found

(7) 好,看看工程目錄下的debug目錄中會看到新生成的obj文件。

下面我們來手工鏈接看看,

打開菜單中的project - setting - Link,拷貝Project options下的所有內容,如下:

kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /incremental:yes /pdb:"Debug/cle.pdb" /debug /machine:I386 /out:"Debug/cle.exe" /pdbtype:sept

把它修改成:

Link.exe kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /incremental:yes /pdb:"Debug/cle.pdb" /debug /machine:I386 /out:"Debug/cle.exe" /pdbtype:sept Debug/t1.obj Debug/t2.obj Debug/main.obj

pause

注意前面多了Link.exe,後面多了Debug/t1.obj Debug/t2.obj Debug/main.obj以及

最後一個pause批處理命令,然後把它另存到工程目錄(此目錄下會看到debug目錄)下起名爲link.bat

運行它,就會看到:

t2.obj : error LNK2005: "void __cdecl Foo(void)" (?Foo@@YAXXZ) already defined i

n t1.obj

Debug/cle.exe : fatal error LNK1169: one or more multiply defined symbols found

很好,我們鏈接原來的obj文件得到的效果跟在vc中用rebuild all出來的效果一樣。那麼現在如果

我們把備份出來的t1.obj_inline覆蓋t1.obj而t2.obj_inline覆蓋t2.obj再手動鏈接應該會是

不會出錯的,因爲原t1.obj_inline及t2.obj_inline中存放的是內部鏈接符號。好運行Link.bat,果然

不出所料,鏈接成功了,看看debug目錄下多出了一個exe文件。這就說明了內或外符號在obj有標誌標識!

(提示:上述爲什麼不用vc的f7build鏈接呢,因爲文件時間改變了,build會重新生成新的obj,

所以我們用手動鏈接保證obj不變)[注bj信息可用dumpbin.exe查看]

4.#include規則:

有很多人不知道#include 文件該放在何處?

1). 增強部件自身的完整性:

爲了保證部件完整,部件的cpp實現文件(如test.cpp)中第一個#include的應當是它自身對應的頭文件(如test.h)。

(除非你用預編譯頭文件, 預編譯頭必須放在第一個)。這樣就保證了該部件頭文件(test.h)所必須依賴的其它接口(如a.h等)要放到它對應的文件頭中(test.h),而不是在cpp中(test.cpp)把所依賴的其它頭文件(a.h等)移到其自身對應的頭文件(test.h等)之前(因爲這樣強迫其它包括此部件的頭文件 (test.h)的文件(b.cpp)也必須再寫一遍include(即b.cpp若要#include "test.h"也必須#include "a.h")”。另外我們一般會盡量減少文件頭之間的依賴關係,看下面:

2). 減少部件之間的依賴性:

在1的基礎上儘量把#include到的文件放在cpp中包括。

這就要求我們一般不要在頭文件中直接引用其它變量的實現,而是把此引用搬到實現文件中。

例如:

// 文件foo.h:

class CFoo{

void Foo(){}

};

// 文件test.h:

#include "foo.h"

class CTest{

CFoo* m_pFoo;

public:

CTest() : m_pFoo(NULL){}

void Test(){ if(m_pFoo){ m_pFoo->Foo();}}

...........

};

// 文件test.cpp:

#include "test.h"

.....

如上文件test.h中我們其實可以#include "foo.h"移到test.cpp文件中。因爲CFoo* m_pFoo我們只想在部件CTest中用到,

而將來想用到CTest部件而包括test.h的其它部件沒有必要見到foo.h接口,所以我們用前向聲明修改原文件如下:

// 文件foo.h:

class CFoo{

public:

void Foo(){}

};

// 文件test.h:

class CFoo;

class CTest{

CFoo* m_pFoo;

public:

CTest();

void Test();

//........

};

// 文件test.cpp:

#include "test.h" // 這裏第一個放該部件自身對應的接口頭文件

#include "foo.h" // 該部件用到了foo.h

CTest::CTest() : m_pFoo(0){

m_pFoo = new CFoo;

}

void CTest::Test(){

if(m_pFoo){

m_pFoo->Foo();

}

}

//.....

// 再加上main.cpp來測試:

#include "test.h" // 這裏我們就不用見到#include "foo.h"了

CTest test;

void main(){

test.Test();

}

3). 雙重包含衛哨:

在文件頭中包括其它頭文件時(如:#include "xx.h")建議也加上包含衛哨:

// test.h文件內容:

#ifndef __XX1_H_

#include "xx1.h"

#endif

#ifndef __XX2_H_

#include "xx2.h"

#endif

......

雖然我們已經在xx.h文件中開頭已經加過,但是因爲編譯器在打開#include文件也

是需要時間的,如果在外部加上包含衛哨,對於很大的工程可以節省更多的編譯時間。

發佈了0 篇原創文章 · 獲贊 4 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章