1. 程式人生 > >C++ 中標頭檔案重複定義的問題

C++ 中標頭檔案重複定義的問題

一、同一標頭檔案中類巢狀的疑問

假設我們有兩個類A和B,分別定義在各自的標頭檔案A.h和B.h中,但是在A中要用到B,B中也要用到A,像下面的寫法是錯誤的:
class B;

class A
{
public:
B b;
};

class B
{
public:
A a;
};
因為在A物件中要開闢一塊屬於B的空間,而B中又有A的空間,是一個邏輯錯誤,無法實現的。在這裡我們只需要把其中的一個A類中的B型別成員改成指標形式 就可以避免這個無限延伸的怪圈了。為什麼要更改A而不是B?因為就算你在B中做了類似的動作,也仍然會編譯錯誤,表面上這僅僅上一個先後順序的問題。
為什麼會這樣呢?因為C++編譯器自上而下編譯原始檔的時候,對每一個數據的定義,總是需要知道定義的資料的型別的大小。在預先宣告語句class B;之後,編譯器已經知道B是一個類,但是其中的資料卻是未知的,因此B型別的大小也不知道。這樣就造成了編譯失敗,VC++6.0下會得到如下編譯錯 誤:
error C2079: ‘b’ uses undefined class ‘B’
將A中的b更改為B指標型別之後,由於在特定的平臺上,指標所佔的空間是一定的(在Win32平臺上是4位元組),這樣可以通過編譯。

二、不同標頭檔案中的類的巢狀在實際程式設計中,不同的類一般是放在不同的相互獨立的標頭檔案中的,這樣兩個類在相互引用時又會有不一樣的問題。重複編譯是問題出現的根本原因。為了保證頭文 件僅被編譯一次,在C++中常用的辦法是使用條件編譯命令。

Example:
animal.h
class animal
{
……
};

animal.cpp 
 #include "animal.h"         
 #include <iostream.h>       
  ......


 fish.h 
 #include "animal.h"          
 class fish
 { 
  ......    
 }; 

 fish.cpp 
 #include "fish.h" 
 #include <iostream.h> 
 ......

 main.cpp 
 #include "animal.h" 
 #include "fish.h" 
 void main() 
 { 
    ......
 }

編譯檔案,會出現class type redefinition的錯誤

為什麼會出現類重複定義的錯誤呢?請讀者仔細檢視EX10.cpp檔案,在這個檔案中包含了animal.h和fish.h這兩個標頭檔案。當編譯器編譯EX10.cpp檔案時,因為在檔案中包含了animal.h標頭檔案,編譯器展開這個標頭檔案,知道animal這個類定義了,接著展開fish.h 標頭檔案,而在fish.h標頭檔案中也包含了animal.h,再次展開animal.h,於是animal這個類就重複定義了。

要解決標頭檔案重複包含的問題,可以使用條件預處理指令。
修改後的標頭檔案如下:
animal.h
#ifndef ANIMAL_H_H
#define ANIMAL_H_H
class animal
{
……
};
#endif

 fish.h 
 #include "animal.h" 
 #ifndef FISH_H_H 
 #define FISH_H_H 
 class fish
 { 
     ......     
 }; 
 #endif

 我們再看EX10.cpp的編譯過程。當編譯器展開animal.h標頭檔案時,條件預處理指令判斷ANIMAL_H_H沒有定義,於是就定 義它,然後繼續執行,定義了animal這個類;接著展開fish.h標頭檔案,而在fish.h標頭檔案中也包含了animal.h,再次展開 animal.h,這個時候條件預處理指令發現ANIMAL_H_H已經定義,於是跳轉到#endif,執行結束。

但是不要以為使用了這種機制就全部搞定了,比如在以下的程式碼中:

//檔案A.h中的程式碼
#pragma once

#include “B.h”

class A
{
public:
B* b;
};

//檔案B.h中的程式碼
#pragma once

#include “A.h”

class B
{
public:
A* a;
};

這裡兩者都使用了指標成員,因此巢狀本身不會有什麼問題,在主函式前面使用#include “A.h”之後,主要編譯錯誤如下:
error C2501: ‘A’ : missing storage-class or type specifiers
仍然是型別不能找到的錯誤。其實這裡仍然需要前置宣告。分別新增前置宣告之後,可以成功編譯了。程式碼形式如下:

//檔案A.h中的程式碼
#pragma once

#include “B.h”

class B;

class A
{
public:
B* b;
};

//檔案B.h中的程式碼
#pragma once

#include “A.h”

class A;

class B
{
public:
A* a;
};

這樣至少可以說明,標頭檔案包含代替不了前置宣告。有的時候只能依靠前置宣告來解決問題。我們還要思考一下,有了前置宣告的時候標頭檔案包含還是必要的 嗎?我們嘗試去掉A.h和B.h中的#include行,發現沒有出現新的錯誤。那麼究竟什麼時候需要前置宣告,什麼時候需要標頭檔案包含呢?

三、兩點原則

標頭檔案包含其實是一件很煩瑣的工作,不但我們看著累,編譯器編譯的時候也很累,再加上標頭檔案中常常出現的巨集定義。感覺各種巨集定義的展開是非常耗時間的,遠不如自定義函式來得速度。我僅就不同標頭檔案、原始檔間的句則結構問題提出兩點原則,僅供參考:

第一個原則應該是,如果可以不包含標頭檔案,那就不要包含了。這時候前置宣告可以解決問題。如果使用的僅僅是一個類的指標,沒有使用這個類的具體物件(非指標),也沒有訪問到類的具體成員,那麼前置宣告就可以了。因為指標這一資料型別的大小是特定的,編譯器可以獲知。

第二個原則應該是,儘量在CPP檔案中包含標頭檔案,而非在標頭檔案中。假設類A的一個成員是是一個指向類B的指標,在類A的標頭檔案中使用了類B的前置宣告並編譯成功,那麼在A的實現中我們需要訪問B的具體成員,因此需要包含標頭檔案,那麼我們應該在類A的實現部分(CPP檔案)包含類B的標頭檔案而非宣告部分 (H檔案)。