C++標頭檔案重複包含問題分析及解決方案
一、標頭檔案重複包含問題分析
1) 問題重現
舉例說明。假設在某個C++ 標頭檔案 或 原始檔 中,包含了A.h和B.h兩個標頭檔案:
#include "A.h"
#include "B.h"
事實上,在標頭檔案B.h中也包含了標頭檔案A的引用,即:
#include"A.h"
這樣在編譯這個檔案時,因為檔案包含了 A.h 這個標頭檔案,編譯器展開這個標頭檔案,知道了 A 這個類的定義了,接著展開B.h標頭檔案,而在B.h標頭檔案中也包含了A.h,在此展開A.h,於是類A就重複定義了。
以上就是標頭檔案重複包含問題的重現過程。
2) 解決方案
2.1採用條件編譯
具體的實現如下:
預編譯語句:
#ifndef AFX_A_H__E4EC8E17_XXXX_4C73_B589_XXXXC__INCLUDED_
#define AFX_A_H__E4EC8E17_XXXX_4C73_B589_XXXXC__INCLUDED_
class A
{
public:
A();
~A();
};
#endif //AFX_A_H__E4EC8E17_XXXX_4C73_B589_XXXXC__INCLUDED_
說明:條件編譯後這一串字串主要是為了保證唯一,自己可以任意定義,但最好可以包含標頭檔案或類名的資訊,這樣方便閱讀程式碼。
再次編譯,當編譯器再次展開A.h標頭檔案時,條件預處理指令判斷AFX_A_H__E4EC8E17_XXXX_4C73_B589_XXXXC__INCLUDED_沒有定義,於是就定義它,然後繼續執行,定義了A這個類;接著展開B.h標頭檔案,而事實上在B.h標頭檔案中也包含了A.h,再次展開A.h,這個時候條件預處理指令發現AFX_A_H__E4EC8E17_XXXX_4C73_B589_XXXXC__INCLUDED_已經定義,於是跳轉到#endif,執行結束。這樣,在此次的編譯過程中,A這個類只定義了1次。
2.2 新增雜注#pragma once
#pragma once是一個比較常用的C/C++雜注,只要在標頭檔案的最開始加入這條雜注,就能夠保證標頭檔案只被編譯一次。
1. #pragma once概述
#pragma once是編譯器相關的,就是說即使這個編譯系統上有效,但在其他編譯系統也不一定可以,不過現在基本上已經是每個編譯器都有這個雜注了。
#ifndef,#define,#endif是C/C++語言中的巨集定義,通過巨集定義避免檔案多次編譯。所以在所有支援C++語言的編譯器上都是有效的,如果寫的程式要跨平臺,最好使用這種方式。
2. 具體寫法
方式一:
#ifndef _SOMEFILE_H_
#define _SOMEFILE_H_
.......... // 一些宣告語句
#endif
方式二:
#pragma once
... ... // 一些宣告語句
3. 兩者比較
l #ifndef的方式依賴於巨集名字不能衝突,這不光可以保證同一個檔案不會被包含多次,也能保證內容完全相同的兩個檔案不會被不小心同時包含。當然,缺點就是如果不同標頭檔案的巨集名不小心“撞車”,可能就會導致標頭檔案明明存在,編譯器卻硬說找不到宣告的狀況。
l #pragma once則由編譯器提供保證:同一個檔案不會被編譯多次。注意這裡所說的“同一個檔案”是指物理上的一個檔案,而不是指內容相同的兩個檔案。帶來的好處是,你不必再費勁想個巨集名了,當然也就不會出現巨集名碰撞引發的奇怪問題。對應的缺點就是如果某個標頭檔案有多份拷貝,本方法不能保證他們不被重複包含。當然,相比巨集名碰撞引發的“找不到宣告”的問題,重複包含更容易被發現並修正。
l 方式一由語言支援所以移植性好,方式二 可以避免名字衝突
l #pragma once方式產生於#ifndef之後,因此很多人可能甚至沒有聽說過。目前看來#ifndef更受到推崇。因為#ifndef受語言天生的支援,不受編譯器的任何限制;而#pragma once方式卻不受一些較老版本的編譯器支援,換言之,它的相容性不夠好。也許,再過幾年等舊的編譯器死絕了,這就不是什麼問題了。
我還看到一種用法是把兩者放在一起的:
#pragma once
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
... ... // 一些宣告語句
#endif
看起來似乎是想兼有兩者的優點。不過只要使用了#ifndef就會有巨集名衝突的危險,所以混用兩種方法似乎不能帶來更多的好處,倒是會讓一些不熟悉的人感到困惑。
二、標頭檔案重複包含的影響
重複包含標頭檔案有以下問題:
1. 使預處理的速度變慢了,要處理很多本來不需要處理的標頭檔案。
2. 可能前處理器就陷入死迴圈了(其實編譯器都會規定一個包含層數的上限)。例如C.h包含D.h,D.h又包含C.h的情況,如果不採用防止標頭檔案的重複定義,那麼前處理器就會進入死迴圈了。
3. 標頭檔案裡有些程式碼不允許重複出現。而重複定義標頭檔案會重複出現一些程式碼。(雖然變數和函式允許多次宣告(只要不是多次定義就行),但標頭檔案裡有些程式碼是不允許多次出現的)。例如:使用typedef型別定義和結構體Tag定義等,在一個程式檔案中只允許出現一次。
三、關於條件編譯
語法格式:
#ifdef 標誌符
程式段1
#else
程式段2
#endif
含義:當定義了標誌符則對程式段1進行編譯,而沒有定義標誌符時則編譯程式段2。
採用條件編譯的原因:其實這跟事物具有多樣性一樣。我們需要對不同的狀況採取不同的對策。例如程式的執行平臺具有多樣性,我們在window平臺下編寫的程式,可能使用某一個庫或者與硬體相關的屬性或方法,現在要將我們的程式移植到別的計算機系統上執行的時候,假定為linux系統,那麼程式上依賴的庫或者有些和硬體相關聯的屬性和方法不得不更改,那麼我們只能在編寫程式的時候提高程式的健壯性,此時就需要條件編譯語句為我們實現這樣的功能。
四、附錄——不同編譯器或開發環境對應的條件編譯指令
· 編譯器
GCC#ifdef __GNUC__
§ #if __GNUC__ >= 3 // GCC3.0以上
Visual C++#ifdef _MSC_VER(非VC編譯器很多地方也有定義)
§ #if _MSC_VER >=1000 // VC++4.0以上
§ #if _MSC_VER >=1100 // VC++5.0以上
§ #if _MSC_VER >=1200 // VC++6.0以上
§ #if _MSC_VER >=1300 // VC2003以上
§ #if _MSC_VER >=1400 // VC2005以上
Borland C++
#ifdef __BORLANDC__
UNIX
UNIX
#ifdef __unix
or
#ifdef __unix__
Linux
#ifdef __linux
or
#ifdef __linux__
FreeBSD
#ifdef __FreeBSD__
NetBSD
#ifdef __NetBSD__
Windows
32bit#ifdef _WIN32(或者WIN32)
64bit#ifdef _WIN64
GUI App#ifdef _WINDOWS
CUI App#ifdef _CONSOLE
Windows的Ver … WINVER※ PC機Windows(95/98/Me/NT/2000/XP/Vista)和Windows CE都定義了
§ #if (WINVER >= 0x030a) // Windows 3.1以上
§ #if (WINVER >= 0x0400) // Windows 95/NT4.0以上
§ #if (WINVER >= 0x0410) // Windows 98以上
§ #if (WINVER >= 0x0500) // Windows Me/2000以上
§ #if (WINVER >= 0x0501) // Windows XP以上
§ #if (WINVER >= 0x0600) // Windows Vista以上
o Windows 95/98/Me的Ver … _WIN32_WINDOWS
§ MFC App、PC機上(Windows CE沒有定義)#ifdef _WIN32_WINDOWS
§ #if (_WIN32_WINDOWS >= 0x0400) // Windows 95以上
§ #if (_WIN32_WINDOWS >= 0x0410) // Windows 98以上
§ #if (_WIN32_WINDOWS >= 0x0500) // Windows Me以上
Windows NT的Ver … _WIN32_WINNT
§ #if (_WIN32_WINNT >= 0x0500) // Windows 2000以上
§ #if (_WIN32_WINNT >= 0x0501) // Windows XP以上
§ #if (_WIN32_WINNT >= 0x0600) // Windows Vista以上
Windows CE(PocketPC)#ifdef _WIN32_WCE
Windows CE … WINCEOSVER
Windows CEWCE_IF
Internet Explorer的Ver … _WIN32_IE
Cygwin
Cygwin#ifdef __CYGWIN__
32bit版Cygwin(現在好像還沒有64bit版)#ifdef __CYGWIN32__
MinGW(-mno-cygwin指定)
#ifdef __MINGW32__
完