如何防止頭文件被重復包含或引用?


一、條件編譯

#ifndef ***

#define ***

 

#endif

二、#pragma once 

只要在頭文件的最開始加入這條指令就能夠保證頭文件被編譯一次,這條指令實際上在VC6中就已經有了,但是考慮到兼容性並沒有太多的使用。

#pragmaonce是編譯相關,就是說這個編譯系統上能用,但在其他編譯系統不一定可以,也就是說移植性差,不過現在基本上已經是每個編譯器都有這個定義了。

#pragmaonce這種方式,是微軟編譯器獨有的,也是后來才有的,所以知道的人並不是很多,用的人也不是很多,因為他不支持跨平台。如果你想寫跨平台的代碼,最好使用上一種。這是一種由編譯器提供支持的方式,防止同一文件的二次編譯,這里的同一文件指的是物理文件。

第一種也是有弊端的:

假如你的某一個頭文件有多份拷貝,那么這些文件雖然在邏輯上都是一樣的,但是在物理上他們卻是不同的,所以當你把這些文件包含的時候,就會發現真的都包含進來了,然后就是編譯錯誤了。還有,當物理上的同一文件被嵌套包含的時候,使用第一種方法預處理會每一次打開該文件做判斷的,但是第二種方法則不會,所以在此#pragma once會更快些。下面舉例說明

   // Test1.h
    #ifndefine  TEST1_H
    #defineTEST1_H
    ...
    #endif
  
    // Test2.h
    #pragma once        
    ...
    
    // Test.cpp
    #include "Test1.h"     // line 1
    #include "Test1.h"     // line 2
    #include "Test2.h"     // line 3
    #include "Test2.h"     // line 4 這里的Test2.h是同一物理文件

預處理器在執行這四句的時候,先打開Test1.h然后發現里面的宏TEST1_H沒有被定義,所以會包含這個文件,第二句的時候,同樣還是會打開Test1.h的發現宏已定義,就不包含該文件按了。

  第三句時,發現之前沒有包含Test2,h則會把該文件包含進來,執行第四句的時候,發現該文件已經被包含了,所以不用打開就直接跳過了


 

條件編譯

#include"a.h"

#include"b.h"
看上去沒什么問題。如果a.h和b.h都包含了一個頭文件x.h。那么x.h在此也同樣被包含了兩次,只不過它的形式不是那么明顯而已。
多重包含在絕大多數情況下出現在大型程序中,它往往需要使用很多頭文件,因此要發現重復包含並不容易。要解決這個問題,我們可以使用條件編譯。如果所有的頭文件都像下面這樣編寫:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H

...//(頭文件內容)

#endif

#ifndef _ASC_H
#define _ASC_H

...//(頭文件內容)

#endif
那么多重包含的危險就被消除了。當頭文件第一次被包含時,它被正常處理,符號HEADERNAME_H被定義為1。如果頭文件被再次包含,通過條件編譯,它的內容被忽略。符號HEADERNAME_H按照被包含頭文件的文件名進行取名,以避免由於其他頭文件使用相同的符號而引起的沖突。

但是,你必須記住預處理器仍將整個頭文件讀入,即使這個頭文件所有內容將被忽略。由於這種處理將托慢編譯速度,所以如果可能,應該避免出現多重包含。


 

  問題:test--1.0使用#ifndef只是防止了頭文件被重復包含(其實本例中只有一個頭件,不會存在重復包含的問題),但是無法防止變量被重復定義。如以下代碼:

 1 //vs 2012 : test.c
 2 
 3 #include <stdio.h>
 4 #include "test.h"
 5 
 6 extern i;
 7 extern void test1();
 8 extern void test2();
 9 
10 int main()
11 {
12    test1();
13    printf("ok/n");
14    test2();
15    printf("%d/n",i);
16    return 0;
17 }

 

 1 //vs 2012 : test.h
 2 
 3 
 4 #ifndef _TEST_H_
 5 #define _TEST_H_
 6 
 7 char add1[] = "www.shellbox.cn/n";
 8 char add2[] = "www.scriptbox.cn/n";
 9 int i = 10;
10 void test1();
11 void test2();
12 
13 #endif

 

 1 //vs 2012 : test1.c
 2 
 3 --
 4 #include <stdio.h>
 5 #include "test.h"
 6 
 7 extern char add1[];
 8 
 9 void test1()
10 {
11    printf(add1);
12 }

 

 1 //vs 2012 : test2.c
 2 
 3 #include<stdio.h>
 4 #include "test.h"
 5 
 6 extern char add2[];
 7 extern i;
 8 
 9 void test2()
10 {
11    printf(add2);
12    for (; i > 0; i--) 
13        printf("%d-", i);
14 }

 

 錯誤分析:
由於工程中的每個.c文件都是獨立的解釋的,即使頭文件有
#ifndef _TEST_H_

#define _TEST_H_

....   //其中內部變量是以定義的形式而不是外部聲明的形式存放在這的

#enfif 
在其他文件中只要包含了test.h就會獨立的解釋,然后每個.c文件生成獨立的變量標示符。

在編譯器鏈接時,就會將工程中所有的符號整合在一起,由於文件中有重名變量標識符,編譯器連接時不知道連接哪一個,於是就出現了重復定義的錯誤。
解決方法:
  在.c文件中定義變量,然后再建一個頭文件(.h文件),在所有的變量聲明前加上extern,注意這里不要對變量進行的初始化,加上外部聲明在對.h文件展開時全是聲明變量而不是定義,在編譯器連接時只會去找定義的那個變量,外部聲明的同名變量不會對其產生影響。

  然后在其他需要使用全局變量的.c文件中包含.h文件。編譯器會為.c生成目標文件,然后鏈接時,如果該.c文件使用了全局變量,鏈接器就會鏈接到定義變量的.c文件 。

 1 //vs 2012 : test.h
 2 
 3 //-------------------------------
 4 
 5 #ifndef _TEST_H_
 6 
 7 #define _TEST_H_
 8 
 9 
10 extern int i;
11 
12 extern char add1[];
13 
14 extern char add2[];
15 
16 
17 void test1();
18 
19 void test2();
20  
21 
22 #endif

 

 1 //vs 2012 : test.c
 2 
 3 //-------------------------------
 4 
 5 #include <stdio.h>
 6 
 7 #include "test.h"
 8 
 9 
10 int i = 10;
11 
12 char add1[] = "www.shellbox.cn/n";
13 
14 char add2[] = "www.scriptbox.cn/n";
15 
16 extern void test1();
17 
18 extern void test2();
19 
20  
21 int main()
22 
23 {
24 
25    test1();
26 
27   printf("ok/n");
28 
29    test2();
30 
31   printf("%d/n",i);
32 
33    return 0;
34 
35 }

 

 

 1 //vs 2012 : test1.c
 2 
 3 //-------------------------------
 4 
 5 #include <stdio.h>
 6 
 7 #include "test.h"
 8 
 9 
10 extern char add1[];
11 
12 
13 void test1()
14 
15 {
16 
17    printf(add1);
18 
19 }

 

 1 //vs 2012 : test2.c
 2 
 3 //-------------------------------
 4 
 5 #include <stdio.h>
 6 
 7 #include "test.h"
 8 
 9  
10 extern char add2[];
11 
12 extern int i;

 

 1 void test2()
 2 
 3 {
 4 
 5    printf(add2);
 6 
 7    for (; i > 0;i--)
 8 
 9        printf("%d-",i);
10 
11 }

問題擴展: 變量的聲明有兩種情況:

    (1) 一種是需要建立存儲空間的(定義、聲明)。例如:int a在聲明的時候就已經建立了存儲空間。 
    (2) 另一種是不需要建立存儲空間的(外部聲明)。例如:extern int a其中變量a是在別的文件中定義的。
    前者是"定義性聲明(defining declaration)"或者稱為"定義(definition)",而后者是"引用性聲明(referncingdeclaration)"。從廣義的角度來講聲明中包含着定義,但是並非所有的聲明都是定義,例如:

  int a它既是聲明,同時又是定義。

  然而對於extern a來講它只是聲明不是定義。一般的情況下我們常常這樣敘述,把建立空間的聲明稱之為"定義",而把不需要建立存儲空間稱之為"聲明"。很明顯我們在這里指的聲明是范圍比較窄的,也就是說非定義性質的聲明。

例如:在主函數中 
int main()
{
    extern int A; //這是個聲明而不是定義,聲明A是一個已經定義了的外部變量
                 //注意:聲明外部變量時可以把變量類型去掉如:extern A;
    dosth();      //執行函數
}

int A;            //是定義,定義了A為整型的外部變量(全局變量) 
    外部變量(全局變量)的"定義"與外部變量的"聲明"是不相同的,外部變量的定義只能有一次,它的位置是在所有函數之外,而同一個文件中的外部變量聲明可以是多次的,它可以在函數之內(哪個函數要用就在那個函數中聲明)也可以在函數之外(在外部變量的定義點之前)。系統會根據外部變量的定義(而不是根據外部變量的聲明)分配存儲空間的對於外部變量來講,初始化只能是在"定義"中進行,而不是在"聲明"中。所謂的"聲明",其作用,是聲明該變量是一個已定義過的外部變量,僅僅是在為了引用該變量而作的"聲明"而已。extern只作聲明,不作定義。 
    用static來聲明一個變量的作用有二:
    (1) 對於局部變量用static聲明,則是為該變量分配的空間在整個程序的執行期內都始終存在
    (2) 外部變量用static來聲明,則該變量的作用只限於本文件模塊

(此部分參考自:如何防止頭文件被重復包含、嵌套包含

三、前置聲明:

在編寫C++程序的時候,偶爾需要用到前置聲明(Forward declaration)。下面的程序中,帶注釋的那行就是類B的前置說明。這是必須的,因為類A中用到了類B,

而類B的聲明出現在類A的后面。如果沒有類B的前置說明,下面的程序將不同通過編譯,編譯器將會給出類似“缺少類型說明符”這樣的出錯提示。

 1 // A.h  
 2 
 3 #include "B.h"  
 4 
 5 class A  
 6 
 7 {  
 8 
 9     B b;  
10 
11 public:  
12 
13     A(void);  
14 
15     virtual ~A(void);  
16 
17 };  

 

 1 //A.cpp  
 2 
 3 #include "A.h"  
 4 
 5 A::A(void)  
 6 
 7 {  
 8 
 9 }  
10 
11   
12 A::~A(void)  
13 
14 {  
15 
16 }  

 

 1 // B.h  
 2 
 3 #include "A.h"  
 4 
 5 class B  
 6 
 7 {  
 8 
 9     A a;  
10 
11 public:  
12 
13     B(void);  
14 
15     ~B(void);  
16 
17 };  

 

 1 // B.cpp  
 2 
 3 #include "B.h"  
 4 
 5 B::B(void)  
 6 
 7 {  
 8 
 9 }  
10 
11 
12 B::~B(void)  
13 
14 {  
15 
16 }

  編譯一下A.cpp,不通過。再編譯B.cpp,還是不通過。編譯器去編譯A.h,發現包含了B.h,就去編譯B.h。編譯B.h的時候發現包含了A.h,但是A.h已經編譯過了(其實沒有編譯完成,可能編譯器做了記錄,A.h已經被編譯了,這樣可以避免陷入死循環。編譯出錯總比死循環強點),就沒有再次編譯A.h就繼續編譯。后面發現用到了A的定義,這下好了,A的定義並沒有編譯完成,所以找不到A的定義,就編譯出錯了。

 

這時使用前置聲明就可以解決問題:

 1 // A.h  
 2 
 3 #include "B.h" 
 4 
 5 class B; //前置聲明
 6 
 7 class A  
 8 
 9 {
10     private:  
11         B  b;  
12 
13     public:  
14         A(void);  
15         virtual ~A(void);  
16 
17 };  

 

 1 //A.cpp 
 2 
 3 #include "A.h"  
 4 
 5 A::A(void)  
 6 
 7 {  
 8 
 9 }  
10 
11 
12 A::~A(void)  
13 
14 {  
15 
16 }  

 

 1 // B.h  
 2 
 3 #include "A.h"  
 4 
 5 class B  
 6 
 7 {
 8     private:   
 9     A a;  
10 
11     public:  
12     B(void);  
13     ~B(void);  
14 
15 };  

 

 1 // B.cpp 
 2 
 3 #include "B.h"  
 4 
 5 B::B(void)  
 6 {  
 7 
 8 }  
 9  
10 
11 B::~B(void)  
12 {  
13 
14 }

 

 1 test.cpp 
 2 
 3 int main()
 4 {
 5     B* b = new B();
 6 
 7     A* a = new A();
 8 
 9     delete a;
10 
11     delete b;
12 
13     return 0;
14 }

類的前置聲明是有許多的好處的。

我們使用前置聲明的一個好處是,從上面看到,當我們在類A使用類B的前置聲明時,我們修改類B時,只需要重新編譯類B,而不需要重新編譯a.h的(當然,在真正使用類B時,必須包含b.h)。

另外一個好處是減小類A的大小,上面的代碼沒有體現,那么我們來看下:

 1 //a.h  
 2 
 3 class B;  
 4 
 5 class A  
 6 
 7 {  
 8     ....  
 9 
10 private:  
11 
12     B *b;  
13 ....  
14 
15 };  

 

 1 //b.h  
 2 
 3 class B  
 4 
 5 {  
 6 ....  
 7 
 8 private:  
 9 
10     int a;  
11 
12     int b;  
13 
14     int c;  
15 
16 }; 

我們看上面的代碼,類B的大小是12(在32位機子上)。

如果我們在類A中包含的是B的對象,那么類A的大小就是12(假設沒有其它成員變量和虛函數)。如果包含的是類B的指針*b變量,那么類A的大小就是4,所以這樣是可以減少類A的大小的,

特別是對於在STL的容器里包含的是類的對象而不是指針的時候,這個就特別有用了。在前置聲明時,我們只能使用的就是類的指針和引用(因為引用也是居於指針的實現的)。

為什么我們前置聲明時,只能使用類型的指針和引用呢?

看下下面這個類:

 1 class A  
 2 
 3 {  
 4 
 5 public:  
 6 
 7     A(int a):_a(a),_b(_a){} // _b is new add  
 8 
 9       
10 
11     int get_a() const {return _a;}  
12 
13     int get_b() const {return _b;} // new add  
14 
15 private:  
16 
17     int _b; // new add  
18 
19     int _a;  
20 
21 };  

上面定義的這個類A,其中_b變量和get_b()函數是新增加進這個類的。

改變:

第一個改變當然是增加了_b變量和get_b()成員函數;

第二個改變是這個類的大小改變了,原來是4,現在是8。

第三個改變是成員_a的偏移地址改變了,原來相對於類的偏移是0,現在是4了。

上面的改變都是我們顯式的、看得到的改變。還有一個隱藏的改變。

隱藏的改變是類A的默認構造函數和默認拷貝構造函數發生了改變。

由上面的改變可以看到,任何調用類A的成員變量或成員函數的行為都需要改變,因此,我們的a.h需要重新編譯。

如果我們的b.h是這樣的:

 1 //b.h  
 2 
 3 #include "a.h"  
 4 
 5 class B  
 6 
 7 {  
 8 ...  
 9 
10 private:  
11 
12     A a;  
13 
14 };  

那么我們的b.h也需要重新編譯。

如果是這樣的:

 1 //b.h  
 2 
 3 class A;  
 4 
 5 class B  
 6 
 7 {  
 8 
 9 ... 
10 
11 private:  
12 
13    A *a;  
14 
15 };  

那么我們的b.h就不需要重新編譯。

像我們這樣前置聲明類A:

classA;

是一種不完整的聲明,只要類B中沒有執行需要了解類A的大小或者成員的操作,則這樣的不完整聲明允許聲明指向A的指針和引用。

而在前一個代碼中的語句

Aa;

是需要了解A的大小的,不然是不可能知道如果給類B分配內存大小的,因此不完整的前置聲明就不行,必須要包含a.h來獲得類A的大小,同時也要重新編譯類B。

再回到前面的問題,使用前置聲明只允許的聲明是指針或引用的一個原因是只要這個聲明沒有執行需要了解類A的大小或者成員的操作就可以了,所以聲明成指針或引用是沒有

執行需要了解類A的大小或者成員的操作的

 

前置聲明解決兩個類的互相依賴

 1 // A.h 
 2 
 3 class B; 
 4 
 5 class A 
 6 
 7 { 
 8     B* b; 
 9 public: 
10 
11     A(B* b):b(b)
12     {}  
13 
14     void something()
15     {
16         b->something();
17     }
18 
19 };  

   

 1  //A.cpp 
 2 
 3     #include "B.h" 
 4 
 5     #include "A.h" 
 6 
 7     A::A(B * b) 
 8 
 9     { 
10         b= new B; 
11     } 
12 
13 
14     A::~A(void) 
15     { 
16          delete b;
17     } 

 

 1 // B.h 
 2 
 3  class A; 
 4 
 5  class B 
 6 { 
 7 
 8      A a; 
 9 
10     public: 
11 
12      B(void); 
13     void something()
14     {
15         cout<<"something happend ..."<<endl; 
16     }
17    
18      ~B(void); 
19 };     

 

 1  // B.cpp 
 2 
 3 #include "A.h" 
 4 #include "B.h" 
 5 
 6 B::B(void) 
 7 { 
 8      a= New A; 
 9 } 
10 
11 
12 B::~B(void) 
13 { 
14 
15 }

 

 1 test.cpp
 2 
 3 int main()
 4 {
 5     B * n = new B();
 6     A *a = new A(b);
 7     delete a;
 8     delete b;
 9     return 0;
10 
11 }

編譯之后發現錯誤:使用了未定義的類型B;

     ->something 的左邊必須指向類/結構/聯合/類型 

原因:

1.       (1)處使用了類型B的定義,因為調用了類B中的一個成員函數。前置聲明class B;僅僅聲明了有一個B這樣的類型,而並沒有給出相關的定義,類B的相關定義,是在類A后面出現的,因此出現了編譯錯誤;

2.       代碼一之所以能夠通過編譯,是因為其中僅僅用到B這個類型,並沒有用到類B的定義。

解決辦法是什么?

將類的聲明和類的實現(即類的定義)分離。如下所示:

 1 // A.h 
 2 
 3 class B; 
 4 
 5 class A 
 6 
 7 { 
 8 
 9     B* b; 
10 
11 public: 
12 
13     A(B* b):b(b)
14     {}  
15 
16      void something();
17 
18 ~A(void)
19 
20 }; 

      

 1  // B.h 
 2  class A; 
 3 
 4 class B 
 5 { 
 6 
 7      A a; 
 8 
 9     public: 
10 
11         B(void); 
12 
13     void something();
14 
15        ~B(void); 
16 
17 }; 

 

 1     //A.cpp 
 2 
 3     #include "B.h" 
 4 
 5 #include "A.h" 
 6 
 7 A::A(B * b) 
 8 
 9 { 
10 
11      b= new B; 
12 
13 }     
14 
15 
16 void something()
17 {
18 
19 b->something();
20 
21 }   
22 
23  A::~A(void) 
24 {  } 

 

   

 1 // B.cpp 
 2 
 3 #include "A.h" 
 4 
 5 #include "B.h" 
 6 
 7 B::B(void) 
 8 
 9 { 
10 
11     a= New A; 
12 
13 } 
14 
15 void B::something()
16 
17 {
18 
19 cout<<"something happend ..."<<endl; 
20 
21 }
22 
23 
24 B::~B(void) 
25  {   }

 

 1 test.cpp
 2  
 3 
 4 int main()
 5 
 6 {
 7     B * n = new B();
 8 
 9     A *a = new A(b);
10 
11     delete a;
12 
13     delete b;
14 
15     return 0;
16 }

結論:

前置聲明只能作為指針或引用,不能定義類的對象,自然也就不能調用對象中的方法了。

而且需要注意,如果將類A的成員變量B* b;改寫成B& b;的話,必須要將b在A類的構造函數中,采用初始化列表的方式初始化,否則也會出錯。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM