不妨先看一個例子:

//A.h #pragma once #include "B.h" class A { public: A(void); ~A(void); }; //B.h #pragma once int a; class B { public: B(void); ~B(void); }; //main.cpp #include <iostream> #include "A.h" #include "B.h" using namespace std; int main() { }
這時候會在鏈接的時候報錯
1>B.obj : error LNK2005: "int a" (?a@@3HA) 已經在 A.obj 中定義
1>main.obj : error LNK2005: "int a" (?a@@3HA) 已經在 A.obj 中定義
錯誤基本原因:
因為A.h在#include”b.h”后會產生一個int a的定義語句。程序在編譯的時候只會對單個文件進行語法等要素的編譯生成obj文件,在鏈接的時候將這些obj文件整合起來,這時候由於a.obj和b.obj都有int a的定義,就出現了上述所謂的重定義。
深入探究原因:
因為:1.定義和聲明(定義就是說程序一定知道會為其分配多少內存,比如int a就會知道分配4bytes的內存,聲明是不會知道分配多少內存的。定義分為3種:int a 對象定義;void func(){…}函數定義;Class A{…}類型定義;聲明分為2種:void func();函數聲明, Class A;類型聲明)。最重要的點:在一個文件中同一個聲明可以多次聲明,但是定義只能定義一次。2.在編譯的時候,include”B.h”會將B.h中的所有東西拷貝到a.h中,這時候的a.h就應該是這樣的:

//A.h #pragma once int a; class B { public: B(void); ~B(void); }; class A { public: A(void); ~A(void); };
我突然又想到個問題:為什么只有int a類型重定義,不會有class B類型重定義呢?
不妨再來看個例子。

//A.h #pragma once #include "B.h" //int tempA; //void Func2(); //class Cattle //{ //public: // Cattle(); // ~Cattle(); //}; class Apple { public: Apple(void); ~Apple(void); BadBoy bad; Cattle catt; void Func1(); }; //B.h #pragma once int tempB; void func(){} class Cattle { public: Cattle(); ~Cattle(); }; class BadBoy { public: BadBoy(void); ~BadBoy(void); }; #include <iostream> #include "A.h" #include "B.h" using namespace std; //main.cpp int main() { }
錯誤輸出:
1>B.obj : error LNK2005: "void __cdecl func(void)" (?func@@YAXXZ) 已經在 A.obj 中定義
1>B.obj : error LNK2005: "int tempB" (?tempB@@3HA) 已經在 A.obj 中定義
1>main.obj : error LNK2005: "void __cdecl func(void)" (?func@@YAXXZ) 已經在 A.obj 中定義
1>main.obj : error LNK2005: "int tempB" (?tempB@@3HA) 已經在 A.obj 中定義
我打開a.obj找到這樣一段:
$T__________`_______________________________________?tempB@@3HA_?func@@YAXXZ___RTC_Shutdown.rtc$TMZ___RTC_Shutdown___RTC_InitBase.rtc$IMZ___RTC_InitBase_??0Apple@@QAE@XZ_??1BadBoy@@QAE@XZ___unwindfunclet$??0Apple@@QAE@XZ$0_??0Cattle@@QAE@XZ_??0BadBoy@@QAE@XZ____security_cookie___ehhandler$??0Apple@@QAE@XZ____CxxFrameHandler3_@__security_check_cookie@4___ehfuncinfo$??0Apple@@QAE@XZ___unwindtable$??0Apple@@QAE@XZ___RTC_CheckEsp_??1Apple@@QAE@XZ___unwindfunclet$??1Apple@@QAE@XZ$0_??1Cattle@@QAE@XZ___ehhandler$??1Apple@@QAE@XZ___ehfuncinfo$??1Apple@@QAE@XZ___unwindtable$??1Apple@@QAE@XZ_
這
在b.obj找到這樣一段:
$T__________`___________________________________?___?tempB@@3HA_?func@@YAXXZ___RTC_Shutdown.rtc$TMZ___RTC_Shutdown___RTC_InitBase.rtc$IMZ___RTC_InitBase_??0Cattle@@QAE@XZ_??1Cattle@@QAE@XZ_??0BadBoy@@QAE@XZ_??1BadBoy@@QAE@XZ_
通過這樣一段,我個人所認為的是對a.obj和b.obj進行鏈接時,其實就是字符串比較,遇到$T之后的如果a.obj和b.obj同時出現相同的?***@@***的字符串時候就會進行產生重定義的錯誤。在遇到$ IMZ___RTC_InitBase之后就不會進行字符串比較了。這只是我個人猜測,但是VS絕對有某種方式將類型定義隱藏起來,從而不會出現重定義的錯誤。
這2段信息都是表示這個obj中的出現的定義和函數引用信息。實際就是h文件向下轉換的更底層的代碼語言。
我在后面測試的BUG中發現了紅色標注的內容。
>A.obj : error LNK2019: 無法解析的外部符號 "public: __thiscall BadBoy::~BadBoy(void)" (??1BadBoy@@QAE@XZ),該符號在函數 __unwindfunclet$??0Apple@@QAE@XZ$0 中被引用
1>A.obj : error LNK2019: 無法解析的外部符號 "public: __thiscall Cattle::Cattle(void)" (??0Cattle@@QAE@XZ),該符號在函數 "public: __thiscall Apple::Apple(void)" (??0Apple@@QAE@XZ) 中被引用
1>A.obj : error LNK2019: 無法解析的外部符號 "public: __thiscall BadBoy::BadBoy(void)" (??0BadBoy@@QAE@XZ),該符號在函數 "public: __thiscall Apple::Apple(void)" (??0Apple@@QAE@XZ) 中被引用
1>A.obj : error LNK2019: 無法解析的外部符號 "public: __thiscall Cattle::~Cattle(void)" (??1Cattle@@QAE@XZ),該符號在函數 "public: __thiscall Apple::~Apple(void)" (??1Apple@@QAE@XZ) 中被引用
可以推斷:@表示冒號,??0Apple@@QAE@XZ == XZ:QAE::Apple() == public:thiscall::apple()
這幾個錯誤的原因都是A.obj文件沒有在其他obj文件中找到這些函數的實現體。
另外,我在debug的過程中有個重要發現:在A.obj找不到Apple類中的屬性bad和catt。這說明了obj文件不保存屬性的名稱,只保存了該屬性的序列長度(也就是多少比特數),同時如果只聲明Func1(),在CPP文件中不實現,也找不到方法Func();這為后期的Func1()編譯可以通過(因為已經聲明),鏈接不能通過埋下了禍害(找不到實現體)。
結論:
- 全局變量(對象)定義和全局函數定義一定不能出現在h文件中。類定義、枚舉定義、結構體定義都可以。
- 類里面聲明的函數方法,如果只聲明,且沒有被調用過,那么編譯和鏈接均可以通過。但是,如果只聲明,且被調用到,那么就會出現編譯通過、鏈接不通過的錯誤。個人推薦,每個聲明的函數方法都應該要實現。
- 編譯的時候就是語法檢測和聲明檢測(出現未聲明的標識符會報錯),鏈接的時候就是定義檢測(出現重定義和函數調用時候沒有該函數的定義會報錯)。
- 無法解析的外部符號,大多數情況都是由於只聲明了函數方法,沒有函數方法實現造成的。
番外話:1、如果你的A.h的聲明,在C.cpp中實現,那么生成的是C.obj文件。
2、如果你只有聲明的h文件,沒有實現的cpp文件,那么不會生成obj文件,且不會有任何報錯。