如何防止標頭檔案被重複包含或引用?
一、#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
前處理器在執行這四句的時候,先開啟Test1.h然後發現裡面的巨集TEST1_H沒有被定義,所以會包含這個檔案,第二句的時候,同樣還是會開啟Test2.h的發現巨集已定義,就不包含該檔案按了。第三句時,發現之前沒有包含Test2,h則會把該檔案包含進來,執行第四句的時候,發現該檔案已經被包含了,所以不用開啟就直接跳過了
二、條件編譯
#include"a.h"
#include"b.h"
看上去沒什麼問題。如果a.h和b.h都包含了一個頭檔案x.h。那麼x.h在此也同樣被包含了兩次,只不過它的形式不是那麼明顯而已。
多重包含在絕大多數情況下出現在大型程式中,它往往需要使用很多標頭檔案,因此要發現重複包含並不容易。要解決這個問題,我們可以使用條件編譯。如果所有的標頭檔案都像下面這樣編寫:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H
...//(
#endif
那麼多重包含的危險就被消除了。當頭檔案第一次被包含時,它被正常處理,符號HEADERNAME_H被定義為1。如果標頭檔案被再次包含,通過條件編譯,它的內容被忽略。符號HEADERNAME_H按照被包含標頭檔案的檔名進行取名,以避免由於其他標頭檔案使用相同的符號而引起的衝突。
但是,你必須記住前處理器仍將整個標頭檔案讀入,即使這個標頭檔案所有內容將被忽略。由於這種處理將託慢編譯速度,所以如果可能,應該避免出現多重包含。
問題:test-1.0使用#ifndef只是防止了標頭檔案被重複包含(其實本例中只有一個頭件,不會存在重複包含的問題),但是無法防止變數被重複定義。如以下程式碼:
//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);
}
錯誤分析:
由於工程中的每個.c檔案都是獨立的解釋的,即使標頭檔案有
#ifndef _TEST_H_ #define _TEST_H_ .... #enfif
在其他檔案中只要包含了test.h就會獨立的解釋,然後每個.c檔案生成獨立的標示符。在編譯器連結時,就會將工程中所有的符號整合在一起,由於檔案中有重名變數,於是就出現了重複定義的錯誤。
解決方法:
在.c檔案中定義變數,然後再建一個頭檔案(.h檔案),在所有的變數宣告前加上extern,注意這裡不要對變數進行的初始化。然後在其他需要使用全域性變數的.c檔案中包含.h檔案。編譯器會為.c生成目標檔案,然後連結時,如果該.c檔案使用了全域性變數,連結器就會連結到定義變數的.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);
}
問題擴充套件: 變數的宣告有兩種情況:
(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的前置說明,下面的程式將不同通過編譯,編譯器將會給出類似“缺少型別說明符”這樣的出錯提示。
// 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,不通過。再編譯B.cpp,還是不通過。編譯器去編譯A.h,發現包含了B.h,就去編譯B.h。編譯B.h的時候發現包含了A.h,但是A.h已經編譯過了(其實沒有編譯完成,可能編譯器做了記錄,A.h已經被編譯了,這樣可以避免陷入死迴圈。編譯出錯總比死迴圈強點),就沒有再次編譯A.h就繼續編譯。後面發現用到了A的定義,這下好了,A的定義並沒有編譯完成,所以找不到A的定義,就編譯出錯了。
這時使用前置宣告就可以解決問題:
// 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;
}
類的前置宣告是有許多的好處的。
我們使用前置宣告的一個好處是,從上面看到,當我們在類A使用類B的前置宣告時,我們修改類B時,只需要重新編譯類B,而不需要重新編譯a.h的(當然,在真正使用類B時,必須包含b.h)。
另外一個好處是減小類A的大小,上面的程式碼沒有體現,那麼我們來看下:
//a.h
class B;
class A
{
....
private:
B *b;
....
};
//b.h
class B
{
....
private:
int a;
int b;
int c;
};
我們看上面的程式碼,類B的大小是12(在32位機子上)。
如果我們在類A中包含的是B的物件,那麼類A的大小就是12(假設沒有其它成員變數和虛擬函式)。如果包含的是類B的指標*b變數,那麼類A的大小就是4,所以這樣是可以減少類A的大小的,
特別是對於在STL的容器裡包含的是類的物件而不是指標的時候,這個就特別有用了。在前置宣告時,我們只能使用的就是類的指標和引用(因為引用也是居於指標的實現的)。
為什麼我們前置宣告時,只能使用型別的指標和引用呢?
看下下面這個類:
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;
};
上面定義的這個類A,其中_b變數和get_b()函式是新增加進這個類的。
改變:
第一個改變當然是增加了_b變數和get_b()成員函式;
第二個改變是這個類的大小改變了,原來是4,現在是8。
第三個改變是成員_a的偏移地址改變了,原來相對於類的偏移是0,現在是4了。
上面的改變都是我們顯式的、看得到的改變。還有一個隱藏的改變。
隱藏的改變是類A的預設建構函式和預設拷貝建構函式發生了改變。
由上面的改變可以看到,任何呼叫類A的成員變數或成員函式的行為都需要改變,因此,我們的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就不需要重新編譯。
像我們這樣前置宣告類A:
classA;
是一種不完整的宣告,只要類B中沒有執行需要了解類A的大小或者成員的操作,則這樣的不完整宣告允許宣告指向A的指標和引用。
而在前一個程式碼中的語句
Aa;
是需要了解A的大小的,不然是不可能知道如果給類B分配記憶體大小的,因此不完整的前置宣告就不行,必須要包含a.h來獲得類A的大小,同時也要重新編譯類B。
再回到前面的問題,使用前置宣告只允許的宣告是指標或引用的一個原因是隻要這個宣告沒有執行需要了解類A的大小或者成員的操作就可以了,所以宣告成指標或引用是沒有
執行需要了解類A的大小或者成員的操作的
前置宣告解決兩個類的互相依賴
// 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;
}
編譯之後發現錯誤:使用了未定義的型別B;
->something 的左邊必須指向類/結構/聯合/型別
原因:
1.(1)處使用了型別B的定義,因為呼叫了類B中的一個成員函式。前置宣告class B;僅僅聲明瞭有一個B這樣的型別,而並沒有給出相關的定義,類B的相關定義,是在類A後面出現的,因此出現了編譯錯誤;
2.程式碼一之所以能夠通過編譯,是因為其中僅僅用到B這個型別,並沒有用到類B的定義。
解決辦法是什麼?
將類的宣告和類的實現(即類的定義)分離。如下所示:
// 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;
}
結論:
前置宣告只能作為指標或引用,不能定義類的物件,自然也就不能呼叫物件中的方法了。
而且需要注意,如果將類A的成員變數B* b;改寫成B& b;的話,必須要將b在A類的建構函式中,採用初始化列表的方式初始化,否則也會出錯。