編譯器常見錯誤及Link2005錯誤原因

一.預處理器-編譯器-彙編器-鏈接器
預處理器會處理相關的預處理指令,一般是以”#”開頭的指令。如:#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文件也
是需要時間的,如果在外部加上包含衛哨,對於很大的工程可以節省更多的編譯時間。============================================================================================================‍ 編程中經常能遇到LNK2005錯誤——重複定義錯誤,其實LNK2005錯誤並不是一個很難解決的錯誤。弄清楚它形成的原因,就可以輕鬆解決它了。 

造成LNK2005錯誤主要有以下幾種情況: 
1.重複定義全局變量。可能存在兩種情況: 
A、對於一些初學編程的程序員,有時候會以爲需要使用全局變量的地方就可以使用定義申明一下。其實這是錯誤的,全局變量是針對整個工程的。正確的應該是在一個CPP文件中定義如下:int g_Test;那麼在使用的CPP文件中就應該使用:exter 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等其他庫,逐一嘗試。 
發佈了17 篇原創文章 · 獲贊 1 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章