如何防止头文件被重复包含或引用?
一、#pragma once?( 比較常用)
只要在頭文件的最開(kāi)始加入這條指令就能夠保證頭文件被編譯一次,這條指令實(shí)際上在VC6中就已經(jīng)有了,但是考慮到兼容性并沒(méi)有太多的使用。
#pragmaonce是編譯相關(guān),就是說(shuō)這個(gè)編譯系統(tǒng)上能用,但在其他編譯系統(tǒng)不一定可以,也就是說(shuō)移植性差,不過(guò)現(xiàn)在基本上已經(jīng)是每個(gè)編譯器都有這個(gè)定義了。
#pragmaonce這種方式,是微軟編譯器獨(dú)有的,也是后來(lái)才有的,所以知道的人并不是很多,用的人也不是很多,因?yàn)樗恢С挚缙脚_(tái)。如果你想寫(xiě)跨平臺(tái)的代碼,最好使用上一種。這是一種由編譯器提供支持的方式,防止同一文件的二次編譯,這里的同一文件指的是物理文件。
他也是有弊端的:
假如你的某一個(gè)頭文件有多份拷貝,那么這些文件雖然在邏輯上都是一樣的,但是在物理上他們卻是不同的,所以當(dāng)你把這些文件包含的時(shí)候,就會(huì)發(fā)現(xiàn)真的都包含進(jìn)來(lái)了,然后就是編譯錯(cuò)誤了。還有,當(dāng)物理上的同一文件被嵌套包含的時(shí)候,使用第一種方法預(yù)處理會(huì)每一次打開(kāi)該文件做判斷的,但是第二種方法則不會(huì),所以在此#pragma once會(huì)更快些。下面舉例說(shuō)明
?? // 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是同一物理文件
預(yù)處理器在執(zhí)行這四句的時(shí)候,先打開(kāi)Test1.h然后發(fā)現(xiàn)里面的宏TEST1_H沒(méi)有被定義,所以會(huì)包含這個(gè)文件,第二句的時(shí)候,同樣還是會(huì)打開(kāi)Test2.h的發(fā)現(xiàn)宏已定義,就不包含該文件按了。第三句時(shí),發(fā)現(xiàn)之前沒(méi)有包含Test2,h則會(huì)把該文件包含進(jìn)來(lái),執(zhí)行第四句的時(shí)候,發(fā)現(xiàn)該文件已經(jīng)被包含了,所以不用打開(kāi)就直接跳過(guò)了
?
二、條件編譯
?
#include"a.h"
#include"b.h"
 
 看上去沒(méi)什么問(wèn)題。如果a.h和b.h都包含了一個(gè)頭文件x.h。那么x.h在此也同樣被包含了兩次,只不過(guò)它的形式不是那么明顯而已。
 
 多重包含在絕大多數(shù)情況下出現(xiàn)在大型程序中,它往往需要使用很多頭文件,因此要發(fā)現(xiàn)重復(fù)包含并不容易。要解決這個(gè)問(wèn)題,我們可以使用條件編譯。如果所有的頭文件都像下面這樣編寫(xiě):
 #ifndef _HEADERNAME_H
 #define _HEADERNAME_H
 
 ...//(頭文件內(nèi)容)
 
 #endif
 
 那么多重包含的危險(xiǎn)就被消除了。當(dāng)頭文件第一次被包含時(shí),它被正常處理,符號(hào)HEADERNAME_H被定義為1。如果頭文件被再次包含,通過(guò)條件編譯,它的內(nèi)容被忽略。符號(hào)HEADERNAME_H按照被包含頭文件的文件名進(jìn)行取名,以避免由于其他頭文件使用相同的符號(hào)而引起的沖突。
但是,你必須記住預(yù)處理器仍將整個(gè)頭文件讀入,即使這個(gè)頭文件所有內(nèi)容將被忽略。由于這種處理將托慢編譯速度,所以如果可能,應(yīng)該避免出現(xiàn)多重包含。
?
?
問(wèn)題:test-1.0使用#ifndef只是防止了頭文件被重復(fù)包含(其實(shí)本例中只有一個(gè)頭件,不會(huì)存在重復(fù)包含的問(wèn)題),但是無(wú)法防止變量被重復(fù)定義。如以下代碼:
?
//vs 2012 : test.c
 #include <stdio.h>
 #include "test.h"
 
 extern i;
 extern void test1();
 extern void test2();
 
 int main()
 {
 ?? test1();
 ?? printf("ok/n");
 ?? test2();
 ?? printf("%d/n",i);
 ?? return 0;
 }
 
 
 
 
//vs 2012 : test.h
 #ifndef _TEST_H_
 #define _TEST_H_
 
 char add1[] = "www.shellbox.cn/n";
 char add2[] = "www.scriptbox.cn/n";
 int i = 10;
 void test1();
 void test2();
 
 #endif
 
 
 
 
 
//vs 2012 : test1.c
--
 #include <stdio.h>
 #include "test.h"
 
 extern char add1[];
 
 void test1()
 {
 ?? printf(add1);
 }
 
 
 
 
 
//vs 2012 : test2.c
#include<stdio.h>
 #include "test.h"
 
 extern char add2[];
 extern i;
 
 void test2()
 {
 ?? printf(add2);
 ?? for (; i > 0; i--)?
 ?? ??? printf("%d-", i);
 }
?
?錯(cuò)誤分析:
 由于工程中的每個(gè).c文件都是獨(dú)立的解釋的,即使頭文件有
 #ifndef _TEST_H_ #define _TEST_H_ .... #enfif
 在其他文件中只要包含了test.h就會(huì)獨(dú)立的解釋,然后每個(gè).c文件生成獨(dú)立的標(biāo)示符。在編譯器鏈接時(shí),就會(huì)將工程中所有的符號(hào)整合在一起,由于文件中有重名變量,于是就出現(xiàn)了重復(fù)定義的錯(cuò)誤。
 
 解決方法:
 在.c文件中定義變量,然后再建一個(gè)頭文件(.h文件),在所有的變量聲明前加上extern,注意這里不要對(duì)變量進(jìn)行的初始化。然后在其他需要使用全局變量的.c文件中包含.h文件。編譯器會(huì)為.c生成目標(biāo)文件,然后鏈接時(shí),如果該.c文件使用了全局變量,鏈接器就會(huì)鏈接到定義變量的.c文件?。
//vs 2012 : test.h
//-------------------------------
#ifndef _TEST_H_
#define _TEST_H_
?
extern int i;
extern char add1[];
extern char add2[];
?
void test1();
void test2();
?
#endif
?
?
//vs 2012 : test.c
//-------------------------------
#include <stdio.h>
#include "test.h"
?
?
int i = 10;
char add1[] = "www.shellbox.cn/n";
char add2[] = "www.scriptbox.cn/n";
extern void test1();
extern void test2();
?
int main()
{
?? test1();
??printf("ok/n");
?? test2();
??printf("%d/n",i);
?? return 0;
}
?
//vs 2012 : test1.c
//-------------------------------
#include <stdio.h>
#include "test.h"
?
extern char add1[];
?
void test1()
{
?? printf(add1);
}
?
?
//vs 2012 : test2.c
//-------------------------------
#include <stdio.h>
#include "test.h"
?
extern char add2[];
extern int i;
?
void test2()
{
?? printf(add2);
?? for (; i > 0;i--)
?????? printf("%d-",i);
}
問(wèn)題擴(kuò)展:?變量的聲明有兩種情況:
???(1) 一種是需要建立存儲(chǔ)空間的(定義、聲明)。例如:int a在聲明的時(shí)候就已經(jīng)建立了存儲(chǔ)空間。?
 ??? (2) 另一種是不需要建立存儲(chǔ)空間的(聲明)。例如:extern int a其中變量a是在別的文件中定義的。
 ??? 前者是"定義性聲明(defining declaration)"或者稱(chēng)為"定義(definition)",而后者是"引用性聲明(referncingdeclaration)"。從廣義的角度來(lái)講聲明中包含著定義,但是并非所有的聲明都是定義,例如:int a它既是聲明,同時(shí)又是定義。然而對(duì)于extern a來(lái)講它只是聲明不是定義。一般的情況下我們常常這樣敘述,把建立空間的聲明稱(chēng)之為"定義",而把不需要建立存儲(chǔ)空間稱(chēng)之為"聲明"。很明顯我們?cè)谶@里指的聲明是范圍比較窄的,也就是說(shuō)非定義性質(zhì)的聲明。
 
 例如:在主函數(shù)中?
 int main()
 {
 ??? extern int A; //這是個(gè)聲明而不是定義,聲明A是一個(gè)已經(jīng)定義了的外部變量
 ?????????????????//注意:聲明外部變量時(shí)可以把變量類(lèi)型去掉如:extern A;
 ??? dosth();????? //執(zhí)行函數(shù)
 }
 
 int A;??????????? //是定義,定義了A為整型的外部變量(全局變量)?
 
 
 ????外部變量(全局變量)的"定義"與外部變量的"聲明"是不相同的,外部變量的定義只能有一次,它的位置是在所有函數(shù)之外,而同一個(gè)文件中的外部變量聲明可以是多次的,它可以在函數(shù)之內(nèi)(哪個(gè)函數(shù)要用就在那個(gè)函數(shù)中聲明)也可以在函數(shù)之外(在外部變量的定義點(diǎn)之前)。系統(tǒng)會(huì)根據(jù)外部變量的定義(而不是根據(jù)外部變量的聲明)分配存儲(chǔ)空間的。對(duì)于外部變量來(lái)講,初始化只能是在"定義"中進(jìn)行,而不是在"聲明"中。所謂的"聲明",其作用,是聲明該變量是一個(gè)已在后面定義過(guò)的外部變量,僅僅是在為了"提前"引用該變量而作的"聲明"而已。extern只作聲明,不作定義。?
 
 ??? 用static來(lái)聲明一個(gè)變量的作用有二:
 ??? (1) 對(duì)于局部變量用static聲明,則是為該變量分配的空間在整個(gè)程序的執(zhí)行期內(nèi)都始終存在
 ??? (2) 外部變量用static來(lái)聲明,則該變量的作用只限于本文件模塊
 (此部分參考自:如何防止頭文件被重復(fù)包含、嵌套包含)
?
三、前置聲明:
?
在編寫(xiě)C++程序的時(shí)候,偶爾需要用到前置聲明(Forward declaration)。下面的程序中,帶注釋的那行就是類(lèi)B的前置說(shuō)明。這是必須的,因?yàn)轭?lèi)A中用到了類(lèi)B,
而類(lèi)B的聲明出現(xiàn)在類(lèi)A的后面。如果沒(méi)有類(lèi)B的前置說(shuō)明,下面的程序?qū)⒉煌ㄟ^(guò)編譯,編譯器將會(huì)給出類(lèi)似“缺少類(lèi)型說(shuō)明符”這樣的出錯(cuò)提示。
?
//?A.h??
#include?"B.h"??
class?A??
{??
????B?b;??
public:??
????A(void);??
????virtual?~A(void);??
};??
??
//A.cpp??
#include?"A.h"??
A::A(void)??
{??
}??
??
??
A::~A(void)??
{??
}??
??
//?B.h??
#include?"A.h"??
class?B??
{??
????A?a;??
public:??
????B(void);??
????~B(void);??
};??
??
//?B.cpp??
#include?"B.h"??
B::B(void)??
{??
}??
??
??
B::~B(void)??
{??
}
 
 
編譯一下A.cpp,不通過(guò)。再編譯B.cpp,還是不通過(guò)。編譯器去編譯A.h,發(fā)現(xiàn)包含了B.h,就去編譯B.h。編譯B.h的時(shí)候發(fā)現(xiàn)包含了A.h,但是A.h已經(jīng)編譯過(guò)了(其實(shí)沒(méi)有編譯完成,可能編譯器做了記錄,A.h已經(jīng)被編譯了,這樣可以避免陷入死循環(huán)。編譯出錯(cuò)總比死循環(huán)強(qiáng)點(diǎn)),就沒(méi)有再次編譯A.h就繼續(xù)編譯。后面發(fā)現(xiàn)用到了A的定義,這下好了,A的定義并沒(méi)有編譯完成,所以找不到A的定義,就編譯出錯(cuò)了。
?
這時(shí)使用前置聲明就可以解決問(wèn)題:
//?A.h??
#include?"B.h"?
class B; //前置聲明
class?A??
{
 
 
private:??
????B ?b;??
public:??
????A(void);??
????virtual?~A(void);??
};??
??
//A.cpp?
#include?"A.h"??
A::A(void)??
{??
}??
??
??
A::~A(void)??
{??
}??
??
//?B.h??
#include?"A.h"??
class?B??
{
private:???
????A?a;??
public:??
????B(void);??
????~B(void);??
};??
??
//?B.cpp?
#include?"B.h"??
B::B(void)??
{??
}??
??
??
B::~B(void)??
{??
}
 
test.cpp
 
int main()
{
B* b = new B();
A* a = new A();
delete a;
delete b;
return 0;
}
?
類(lèi)的前置聲明是有許多的好處的。
我們使用前置聲明的一個(gè)好處是,從上面看到,當(dāng)我們?cè)陬?lèi)A使用類(lèi)B的前置聲明時(shí),我們修改類(lèi)B時(shí),只需要重新編譯類(lèi)B,而不需要重新編譯a.h的(當(dāng)然,在真正使用類(lèi)B時(shí),必須包含b.h)。
另外一個(gè)好處是減小類(lèi)A的大小,上面的代碼沒(méi)有體現(xiàn),那么我們來(lái)看下:
?
//a.h??
class?B;??
class?A??
{??
????....??
private:??
????B?*b;??
....??
};??
//b.h??
class?B??
{??
....??
private:??
????int?a;??
????int?b;??
????int?c;??
};??
?
我們看上面的代碼,類(lèi)B的大小是12(在32位機(jī)子上)。
如果我們?cè)陬?lèi)A中包含的是B的對(duì)象,那么類(lèi)A的大小就是12(假設(shè)沒(méi)有其它成員變量和虛函數(shù))。如果包含的是類(lèi)B的指針*b變量,那么類(lèi)A的大小就是4,所以這樣是可以減少類(lèi)A的大小的,
特別是對(duì)于在STL的容器里包含的是類(lèi)的對(duì)象而不是指針的時(shí)候,這個(gè)就特別有用了。在前置聲明時(shí),我們只能使用的就是類(lèi)的指針和引用(因?yàn)橐靡彩蔷佑谥羔樀膶?shí)現(xiàn)的)。
?
為什么我們前置聲明時(shí),只能使用類(lèi)型的指針和引用呢?
看下下面這個(gè)類(lèi):
?
class?A??
{??
public:??
????A(int?a):_a(a),_b(_a){}?//?_b?is?new?add??
??????
????int?get_a()?const?{return?_a;}??
????int?get_b()?const?{return?_b;}?//?new?add??
private:??
????int?_b;?//?new?add??
????int?_a;??
};??
?
上面定義的這個(gè)類(lèi)A,其中_b變量和get_b()函數(shù)是新增加進(jìn)這個(gè)類(lèi)的。
改變:
第一個(gè)改變當(dāng)然是增加了_b變量和get_b()成員函數(shù);
第二個(gè)改變是這個(gè)類(lèi)的大小改變了,原來(lái)是4,現(xiàn)在是8。
第三個(gè)改變是成員_a的偏移地址改變了,原來(lái)相對(duì)于類(lèi)的偏移是0,現(xiàn)在是4了。
上面的改變都是我們顯式的、看得到的改變。還有一個(gè)隱藏的改變。
隱藏的改變是類(lèi)A的默認(rèn)構(gòu)造函數(shù)和默認(rèn)拷貝構(gòu)造函數(shù)發(fā)生了改變。
由上面的改變可以看到,任何調(diào)用類(lèi)A的成員變量或成員函數(shù)的行為都需要改變,因此,我們的a.h需要重新編譯。
如果我們的b.h是這樣的:
?
//b.h??
#include?"a.h"??
class?B??
{??
...??
private:??
????A?a;??
};??
?
那么我們的b.h也需要重新編譯。
如果是這樣的:
?
//b.h??
class?A;??
class?B??
{??
...?
private:??
???A?*a;??
};??
?
那么我們的b.h就不需要重新編譯。
像我們這樣前置聲明類(lèi)A:
classA;
是一種不完整的聲明,只要類(lèi)B中沒(méi)有執(zhí)行需要了解類(lèi)A的大小或者成員的操作,則這樣的不完整聲明允許聲明指向A的指針和引用。
而在前一個(gè)代碼中的語(yǔ)句
Aa;
是需要了解A的大小的,不然是不可能知道如果給類(lèi)B分配內(nèi)存大小的,因此不完整的前置聲明就不行,必須要包含a.h來(lái)獲得類(lèi)A的大小,同時(shí)也要重新編譯類(lèi)B。
再回到前面的問(wèn)題,使用前置聲明只允許的聲明是指針或引用的一個(gè)原因是只要這個(gè)聲明沒(méi)有執(zhí)行需要了解類(lèi)A的大小或者成員的操作就可以了,所以聲明成指針或引用是沒(méi)有
執(zhí)行需要了解類(lèi)A的大小或者成員的操作的
?
前置聲明解決兩個(gè)類(lèi)的互相依賴(lài)
??? // A.h?
class B;?
class A?
{?
??? B* b;?
public:?
??? A(B* b):b(b)
{} ?
? ? ? ? void something()
{
b->something();
}
??? };?
??? ?
??? //A.cpp?
??? #include "B.h"?
??? #include "A.h"?
??? A::A(B * b)?
??? {?
??? ??? b= new B;?
??? }?
??? ?
??? ?
??? A::~A(void)?
??? {?
? ? ?delete b;
??? }?
??? ?
??? // B.h?
??? class A;?
??? class B?
??? {?
??? ??? A a;?
??? public:?
??? ??? B(void);?
void something()
{
cout<<"something happend ..."<<endl;?
}
??? ???~B(void);?
??? };?
??? ?
??? // B.cpp?
??? #include "A.h"?
??? #include "B.h"?
??? B::B(void)?
??? {?
??? ??? a= New A;?
??? }?
??? ?
??? ?
??? B::~B(void)?
??? {?
??? }
 
test.cpp
 
int main()
{
B * n = new B();
A *a = new A(b);
 
delete a;
delete b;
return 0;
}
 
編譯之后發(fā)現(xiàn)錯(cuò)誤:使用了未定義的類(lèi)型B;
? ? ?->something 的左邊必須指向類(lèi)/結(jié)構(gòu)/聯(lián)合/類(lèi)型
原因:
1.???????(1)處使用了類(lèi)型B的定義,因?yàn)檎{(diào)用了類(lèi)B中的一個(gè)成員函數(shù)。前置聲明class B;僅僅聲明了有一個(gè)B這樣的類(lèi)型,而并沒(méi)有給出相關(guān)的定義,類(lèi)B的相關(guān)定義,是在類(lèi)A后面出現(xiàn)的,因此出現(xiàn)了編譯錯(cuò)誤;
2.???????代碼一之所以能夠通過(guò)編譯,是因?yàn)槠渲袃H僅用到B這個(gè)類(lèi)型,并沒(méi)有用到類(lèi)B的定義。
?
解決辦法是什么?
將類(lèi)的聲明和類(lèi)的實(shí)現(xiàn)(即類(lèi)的定義)分離。如下所示:
? ??// A.h?
class?B;?
class?A?
{?
????B* b;?
public:?
??? A(B* b):b(b)
{} ?
? ? ? ??void something();
~A(void)
??? };?
? ? ? ? ?
????// B.h?
????class?A;?
????class?B?
??? {?
??? ????A?a;?
????public:?
??? ??? B(void);?
void something();
??? ???~B(void);?
??? };?
 
? ??//A.cpp?
????#include?"B.h"?
????#include?"A.h"?
????A::A(B * b)?
??? {?
??? ??? b=?new?B;?
??? } ? ??
? ? ? ??void something()
{
b->something();
}? ?
????A::~A(void)?
??? { ?}?
??? ?
????// B.cpp?
????#include?"A.h"?
????#include?"B.h"?
????B::B(void)?
??? {?
??? ??? a= New A;?
??? }?
??? ?
void B::something()
{
cout<<"something happend ..."<<endl;?
}
??? ?????B::~B(void)?
??? { ? }
 
test.cpp
 
int main()
{
B * n = new B();
A *a = new A(b);
 
delete a;
delete b;
return 0;
}
結(jié)論:
前置聲明只能作為指針或引用,不能定義類(lèi)的對(duì)象,自然也就不能調(diào)用對(duì)象中的方法了。
?
而且需要注意,如果將類(lèi)A的成員變量B* b;改寫(xiě)成B& b;的話(huà),必須要將b在A類(lèi)的構(gòu)造函數(shù)中,采用初始化列表的方式初始化,否則也會(huì)出錯(cuò)。
總結(jié)
以上是生活随笔為你收集整理的如何防止头文件被重复包含或引用?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
 
                            
                        - 上一篇: 【Python ASCII码转换】——制
- 下一篇: Matlab猜数字游戏GUI界面设计
