第19章 預 處 理 器
預處理器是一種處理源檔案文本的文本處理器,它是翻譯起始階段的一個組成部分。
預處理器并不在文法上分析處理源文本,但出于定位宏調用的目的,它将源文本分開語言符号。雖然編譯器一般在初次編譯時啟動預處理器,但預處理器也可以不經編譯,單獨地處理文本。
Microsoft特殊處
用/E或/EP編譯器選項進行預處理之後,你可以得到一個源代碼的清單。在多數情況下,啟動預處理器和輸出結果文本到輸出裝置,這兩種選項都是控制台指令,這兩種選項的差別在于/E包括了#line指令,/EP沒有這些指令。
Microsoft特殊處結束
特殊術語
在本書中,名詞“參量”指的是傳送給一個函數的實體。有時候,它用“actual”或“formal”修飾,它們分别用于表示函數調用時的參量表達式和在函數定義時的參量說明。名詞“變量”指的是一種簡單的C類型資料對象,名詞“對象”指的是C++對象和變量;它是一個含義廣泛的名詞。
翻譯階段
C和C++程式由一個或多個源檔案組成,它們都包含了程式的某些文本,一個不包含代碼部分的源檔案和它的包含檔案(用#indude預處理器指令包含的檔案),若被條件編譯指令(比如#if)調用,則稱其為一個“轉換單元”。
源檔案可被翻譯多次,翻譯過去的檔案事實上是很正常的。已經翻譯了的翻譯單元可儲存在單獨的對象檔案或對象代碼庫裡,這些單個的轉換單元可被連接配接形成一個可執行檔案或動态連結庫(DLL)。
轉換單元可采用下列形式通信:
* 調用具有外部連接配接的函數。
* 調用具有外部連接配接的類成員函數。
* 直接更改具有外部連接配接的對象。
* 檔案的直接更改。
* 内部外理通信(僅限于基于Microsoft Windows的應用程式)。以下是編譯器翻譯檔案的各個階段:
字元映射
源檔案中的字元被映射為内部源代碼的形式。此階段三字母序列被轉換為單字元的内部表現形式。
行拼接
在此階段,源檔案中所有以反斜杠(/)結尾且其後緊跟一換行符的行,将與下一行連接配接,進而由實體行生成邏輯行。所有非空源檔案結束于一個前面沒有反斜杠的換行符。
語言符号化
此階段源檔案被分為預處理語言符号和空白字元。源檔案中每個注釋被用一個空白字元代替。換行符被保留。
預處理
此階段執行預處理指令并将宏擴充至源檔案,#include語句調用對所有包括文本啟動前面三個翻譯步驟開頭的翻譯過程。
字元集映射
所有的源字元內建員和轉義序列将轉換為執行字元集中的等價形式,對于Microsoft C和C++來說,源字元集和執行字元集都是ASCII碼。
字元串合并
所有相鄰的字元串和寬字元文字都将被合并。例如:“String”“concatenation”合并為“Stringconcatenation”。
翻譯
所有的語言符号将按文法和語義規則進行分析;這些語言符号被轉換為目标代碼。
連結
此階段所有的外部引用被分解以生成一個可執行程式或一個動态連結庫。
編譯器在翻譯過程中遇到文法錯誤時,将發出一個警告或錯誤資訊。
連結器分解所有的外部引用,并把一個或多個分開處理的轉換單元和标準庫聯接起來,以生成一個可執行程式或動态連結庫(DLL)。
預處理器指令
預處理器指令如#define和#ifdef,一般被用在不同的運作環境下,使源程式易于更改和編譯。源檔案中的指令訓示預處理器執行特有的行為。例如,預處理器可替換文本中的語言符号,将其它的檔案内容插入源檔案中,或移走文本的一部分以抑制檔案中某部分的編譯。預處理器行在宏擴充之前被識别且執行。不過,如果宏擴充看起來象一個預處理器指令,該指令将不能被預處理器識别。
除轉義序列之外,預處理器語句采用與源檔案語句相同的字元集。在預處理器語句中的字元集和可執行程式的字元集是一樣的。預處理器也可識别負字元值。預處理器可識别如下指令:
#define #error #import #undef
#elif #if #include
#else #ifdef #line
#endif #ifndef #pragma
數字元号(#)是包含預處理器指令的行中的第一個非空白字元。空白字元可出現在數字元号和指令的第一個字母之間。某些指令包含參量和值。指令之後的任何文本(除作為指令一部分的參量或值之外)必須放在單行注釋分界符(//)之後或注釋分界符()之間。
預處理器指令可出現在源檔案的任何地方,但它們僅用于源檔案的剩餘部分。
#define指令
可以用#define指令給程式中的常量取一個有意義的名稱,其文法的兩種形式如下:
文法
#define 辨別符 語言符号字元串
opt#define 辨別符[(辨別符opt,...,辨別符opt)]語言符号字元串opt
#define指令用語言符号字元串替換源檔案中一個辨別符的所有出現,辨別符僅在它形成一個語言符号時被替換(參見“Microsoft Visual C++ 6.0參考庫”的“Microsoft Visual C++ 6.0語言參考手冊”卷的第1章“詞法規定”中的“語言符号”)。例如,若辨別符出現在一個注釋、一個字元串或作為一個長辨別符的一部分之中,它就不被替換。
一個不帶語言符号字元串的#define指令将移走源檔案中每次辨別符的出現。辨別符保留其定義且能用#defined和#ifdef測試。
語言符号字元串參量由一系列語言符号組成,如關鍵字、常量或完整的語句。一個或多個空白字元可将語言符号字元串和辨別符分開。空白字元不會被認為是被替換文本的一部分,文本最後語言符号之後的空白也不會認為是替換文本的一部分。
形式參數名稱出現在語言符号字元串中以标志出實際值被替換的位置,每個參數名稱可在語言符号字元串中出現多次,且可以以任何次序出現。調用時參量的數目必須與宏定義的參數數目相比對。圓括号的自由運用可確定正确地說明複雜的實際參量。
用第二種文法形式可建立類似函數的宏。這種形式接受一個用圓括号括起的可選參數表。在最初定義之後引用該辨別符,可以使用實際參量替代形式參數的語言符号字元串參量的形式,來替換辨別符(辨別符opt,...,辨別符opt)的每次出現。
表中的形式參數必須用逗号隔開。該表中的每個名稱都必須是唯一的,且此參量表必須包括在圓括号中,辨別符和左邊的圓括号之間不能有空格。對于占用多行的長指令可使用行連接配接,把反斜杠(/)放在換行符前。形式參數名稱的範圍延伸到結束語言符号字元串的換行符。
當一個宏以第二種文法形式定義時,參量表後的文本執行個體就構成一個宏調用。在源檔案中,一個辨別符執行個體後的實際參量必須與宏定義的相應形式參數比對。每個語言符号字元串之前無字元串化(#)、字元化(#@)或語言符号粘貼(##)操作符,或其後無##操作符的形式參量,都被相應的實際參量所替換。在指令替換形式參數之前,實際參量中的任何宏都将被擴充(本章之後的“預處理器操作符”中将介紹這些操作符)。
以下帶參量宏的例子說明了#define文法的第二種形式:
//定義光标行的宏
#define CURSOR(top,bottom) ((top) << 8) | bottom))
//擷取指定範圍中的一個随機整數的宏
#define getrandom(min,max) /
((rand()%(int)(((max)+1)-(min)))+(min))
有副作用的參量有時會導緻宏産生不希望的結果。一個給定的形式參量在語言符号字元串中可能出現多次。如果該形式參數被一個有副作用的表達式所替換,則該表達式及其副作用,可能被求值多次(參見本章後面“語言符号粘貼操作符##”中的例子)。
#undef指令可使一個辨別符的預處理器定義失效。有關的更多資訊參見#undef指令。若一個被定義的宏名稱出現在語言符号字元串中(即使是另一個宏擴充的結果),它将不被擴充。
除非第二次定義(#define)宏與原定義完全相同,否則重定義一個已定義過的宏将産生一個錯誤資訊。
Microsoft特殊處
Microsoft C/C++允許一個宏的重定義,但會産生一個警告資訊,說明新的定義與原定義相同。ANSI C認為宏的重定義是錯誤的。例如,下面的宏對C/C++是相同的,但會産生一個警告資訊:
#define test(f1,f2) (f1*f2)
#define test(a1,a2) (a1*a2
)Microsoft特殊處結束
這個例子用于說明#define指令:
#define WIDTH 80
#define LENGTH (WIDTH+10)
第一個說明定義辨別符WIDTH為整形常量80,且用WIDTH和整形常量10定義LENGTH。LENGTH的每次出現都用(WIDTH+10)所替換,接着,WIDTH+10的每次出現都用表達式(80+10)替換。WIDTH+10的圓括号非常重要,因為它們決定着如下語句的解釋。
var=LENGTH*20;
經預處理後該語句變為:
var=(80+10)*20;
求得值為1800,若無括号,結果為:
var=80+10*20其值為280。
Microsoft特殊處
在檔案開頭用/D編譯器選項定義宏和常量,和用一個#define預處理指令效果是一樣的。能用/D選項定義的宏可達30個。
Microsoft特殊處結束
#error指令
采用error指令可産生編譯錯誤資訊。
文法
#error 語言符号字元串
錯誤消息包括語言符号字元串參量,且從屬于宏擴充。這些指令對于檢測程式的前後沖突和預處理時的違犯限制是非常有用的,以下例子說明了預處理時的出錯處理:
#if !defined(__cplusplus)
#error C++ complier required.
#endif
當遇到#error指令時,編譯終止。
#if,#elif,#else和#endif指令
#if、#elif、#else和#endif指令控制源檔案中某部分的編譯。如果表達式(#if之後)有一個非0值,則緊跟在#if指令之後的行組将保留在轉換單元中。
文法
條件的:
if部分 elif部分opt else部分opt endif行
if部分:
if行 文
本if行:
#if 常量表達式
#ifdef 辨別符
#ifndef 辨別符
elif部分:
elif行 文本
elif部分 elif行 文本
elif行:
#elif 常量表達式
else部分:
else行 文本
else行:
#else
endif行:
#endif
源檔案中每個#if指令都必須與最近的一個#endif相比對。在#if和#endif指令之前的#elif指令的數目是不限的,但最多隻能有一個#else指令。
#else必須是#endif之前的最後一個指令。#if、#elif、#else和#endif指令可嵌套在其它#if指令的文本部分。每個嵌套的#else、#elif或#endif指令應屬于前面最近的一個#if指令。
所有的條件編譯指令,如#if和#ifdef,必須與檔案結束前最近的#endif指令比對;否則,将産生一個錯誤消息。當條件編譯指令包括在包含檔案中時,他們必須滿足相同的條件:在包含檔案結尾沒有不比對的條件編譯指令。
宏替換應在#elif指令後的指令行部分内進行,是以一個宏調用可用在常量表達式中。
預處理器選擇一個給定文本的出現之一作進一步的處理。文本中指定的一個塊可以是文本的任何序列。它可能占用一行以上。通常該文本是對于編譯和預處理器有意義的程式文本。
預處理器處理選擇文本并将其傳送給編譯器。若該文本包含預處理器指令,預處理器将執行這些指令。編譯器隻編譯預處理器標明的文本塊。
預處理器通過求值每個#if或#elif指令之後的常量表達式直到找到一個為真(非0)的常量表達式來選擇單個文本項。預處理器選擇所有的文本(包括以#開頭的其它預處理器指令)直到它關聯的#elif、#else或#endif。
如果常量表達式的所有出現都為假,或者如果沒有#elif指令,預處理器将選擇#else後的文本塊。如果#else被忽略,且所有#if塊中的常量表達式都為假,則不選擇任何文本塊。
常量表達式是一個有以下額外限制的整型常量表達式:
* 表達式必須是整型且可以包括整型常量,字元常量和defined操作符。
* 表達式不能使用sizeof或一個類型造型操作符。
* 目标環境不能表示整數的所有範圍。
* 在翻譯表示中,int類型和long類型以及unsigned int類型和unsigned long類型是相同的。
* 翻譯器可将字元常量翻譯成一組不同于目标環境的代碼值。為了确定目标環境的屬性, 應在為目标環境建立的應用程式中檢測LIMITS.H的宏值。
* 表達式不需執行所有的環境查詢,但需與目标計算機的執行過程細節隔離開。
預處理器操作符defined可用于特殊的常量表達式,文法如下:
文法
defined(辨別符)
defined 辨別符
若此辨別符目前已定義,則該常量表達式被認為是真(非0);否則,條件為假(0)。一個定義為空文本的辨別符可認為已定義。defined指令隻能用于#if和#endif指令。
在如下例子中,#if和#endif指令控制着三個函數調用中某一個的編譯:
#if defined (CREDIT)
credit();
#elif defined(DEBIT)
debit();
#else
printerror();
#endif
若辨別符CREDIT已定義,則對于credit的函數調用被編譯。若辨別符DEBIT被定義,則對于debit的函數調用被編譯。若未定義任何辨別符,将編譯對于printerror的函數調用。
注意,在C和C++中,CREDIT和credit是不同的辨別符,因為它們的大小寫不一樣。
如下例子中的條件編譯語句給出了一個名稱為DLEVEL的已定義的符号常量:
#if DLEVEL > 5
#define SIGNAL 1
#if STACKUSE == 1
#define STACK 200
#else
#define STACK 100
#endif#else
#define SIGNAL 0
#if STACKUSE==1
#define STACK 100
#else
#define STACK 50
#endif
#endif
#if DLEVEL==0
#define STACK 0
#elif DLEVEL==1
#define STACK 100
#elif DLEVEL > 5
display(debugptr)
;#else
#define STACK 200
#endif
第一個#if塊中有兩組嵌套的#if、#else和#endif指令。第一組指令僅當DLEVELl>5為真時執行;否則,執行#else之後的語句。
第二組中的#elif和#else指令選擇基于DLEVEL值的四個選項之一。常量STACK依據DLEVEL定義為0,100或200。若DLEVEL大于5,則編譯語句:
#elif DLEVEL > 5
display(debugptr);
且此時不定義STACK。
條件編譯一般用于防止同一頭檔案的多重包含。C++中在頭檔案内經常定義類的位置,可使用如下結構來防止多次定義。
//EXAMPLE.H例子頭檔案
#if !defined(EXAMPLE_H)
#define ExampleE_H
class Example
{
...
};
#endif //!defined(EXAMPLE_H)
上面的代碼用于檢查符号常量EXAMPLE_H是否已定義。若已定義,該檔案就已被包括且不需再處理;如果未定義,常量EXAMPLE_H将被定義,以标記EXAMPLE.H為已經處理。
Microsoft特殊處
條件編譯表達式被看作為signed long值,且這些表達式與C++中的表達式采用相同的規則求值。例如,表達式:
#if 0xFFFFFFFFL > 1UL
為真。
Microsoft特殊處結束
#ifdef和ifndef指令
#ifdef和#ifndef指令與使用defined(辨別符)操作符的作用是一樣的。
文法
#ifdef 辨別符
#ifndef 辨別符
等同于
#if defined 辨別符
#if !defined 辨別符
#if指令能用的任何地方都可以用#ifdef和#ifndef指令。當辨別符已被定義時,#ifdef辨別符語句等同于#if 1;而當辨別符未定義或用#undef指令對其反定義時,該語句等同于#if 0。這些指令僅用于檢查C或C++源代碼中是否出現該辨別符,而不是用于檢查C或C++源程式中該辨別符的說明。
提供這幾個指令隻為了與該語言的老版本相容。目前的趨勢是偏向于采用defined(辨別符)定義常量表達式的#if指令。
#ifndef指令測試與#ifdef相反的條件。若辨別符未定義(或已用#undef反定義),其條件為真(非0);反之,條件為假(0)。
Microsoft特殊處
可以使用/D選項從指令行傳送辨別符,采用/D選項至多可以指定30個宏。檢查一個定義是否存在是非常有用的,因為定義可從指令行傳送。例如:
//prog.cpp
#ifndef test //這三個語句放在你的代碼中
#define final
#endif
CL /Dtest prog.cpp //這是編譯的指令
Microsoft特殊處結束
#import 指令
C++特殊處
#import指令用于從一個類型庫中結合資訊。該類型庫的内容被轉換為C++類,主要用于描述COM界面。
文法
#import "檔案名" [屬性]
#import <檔案名> [屬性]
屬性:
屬性1,屬性2,...
屬性1 屬性2 ...
檔案名是一個包含類型庫資訊的檔案的名稱。一個檔案可為如下類型之一:
* 一個類型庫(.TLB或.ODL)檔案。
* 一個可執行(.EXE)檔案。
* 一個包含類型庫資源(如.OCX)的庫檔案(.DLL)。
* 一個包含類型庫的複合文檔。
* 其它可被LoadTypeLib API支援的檔案格式。
檔案名之前可以有一個目錄規格。檔案名必須是一個已存在檔案的名稱。兩種格式的差別是當路徑未完全說明時,預處理器檢索類型庫檔案的順序不同。
動作
文法格式
引号格式 這種格式讓預處理器首先搜尋與包含#import語句的檔案同一目錄的類型庫檔案,然後在所有包括(#include)該檔案的目錄中搜尋,最後在如下路徑中搜尋
尖括号格式 這種格式訓示預處理器沿以下路徑搜尋類型庫檔案
編譯器在以下目錄中搜尋已命名的檔案:
1. PATH環境變量路徑表。
2. LIB環境變量路徑表。
3. 用/I(額外的包括目錄)編譯器選項指定的路徑。#import可以任選地包含一個或多個屬性。這些屬性使編譯器改變類型庫頭檔案的内容。一個反斜杠(/)符可用在一個單一的#import語句中包含額外的行,例如:
#import "test.lib" no_namespace /
rename("OldName","NewName")
#import屬性列出如下:
exclude | high_method_prefix |
high_property_prefixes | implementation_only |
include(...) | inject_statement |
named_guids | no_auto_exclude |
no_implementation | no_namespace |
raw_dispinterfaces | raw_interfaces_only |
raw_method_prefix | raw_native_types |
raw_property_prefixes | rename |
rename_namespace |
#import指令可建立兩個在C++源代碼中重構類型庫内容的頭檔案,第一個頭檔案和用Microsoft接口定義語言(MIDL)編譯器生成的頭檔案類似,但有額外的編譯器生成代碼和資料。第一個頭檔案與類型庫有相同的基本名,其擴充名為.TLH。第二個頭檔案也有與類型庫相同的基本名,其擴充名為.TLI。它包括了編譯器生成成員函數的實作,且被包含在(#include)的第一個頭檔案内。
兩個頭檔案都在用/Fo(命名對象檔案)選項指定的輸出目錄中。随後它們被讀出和編譯,就像第一個頭檔案被#include指令命名一樣。
以下是伴随#import指令的編譯器優化:
* 頭檔案被建立時,将被配置設定與類庫相同的時間标志。
* 處理#import時,編譯器首先測試頭檔案是否存在,是否過期。若條件為真,就不需重新建立。
* 編譯器延遲對于OLE子系統的初始化,直到碰到第一個#import指令。
#import指令也可參與最小重建且可被置于一個預編譯頭檔案中。
基本類型庫頭檔案
基本類型庫頭檔案由七個部分組成:
1. 頭部固定正文:由注釋、COMDEF.H(定義用在頭部的一些标準宏)的#include語句和其它繁雜的安裝資訊組成。
2.前向引用和類型定義:由象struct IMyinterface之類的結構說明和用于一些TKIND_ALIAS項的類型定義組成。
3.靈敏指針說明:子產品類_com_ptr_t是一個封裝接口指針和消除調用AddRef、Release 和QueryInterface函數需求的靈敏指針實作。此外,它隐藏了建立一個新COM對象中的CoCreateInstance調用。此部分采用宏語句_COM_SMARTPTR_TYPEDEF将COM接口的類型定義建立為_com_ptr_t模闆類的模闆特例化。例如,對于界面IFoo,.TLH檔案包含有:
_COM_SMARTPTR_TYPEDEF(IFoo,_ _uuidof(IFoo));
編譯器将其擴充為:type def _com_ptr_t<_com_IIID<IFoo,_ _uuidof(IFoo) >> IFooPtr;
類型IFooPtr可以用在原始的界面指針IFoo*的地方。結果,就不需調用各種IUnknown成員函數。
4. 類型資訊(typeinfo)說明:主要由類定義和其它項組成,這些項說明由ITyptLib:GetTypeInfo傳回的單個的資訊類型項目。在這部分,每個來自于類型庫的資訊類型都以一種依賴于TYPEKIND資訊的格式反映在該頭部。
5. 任選舊式GUID定義:包含命名的GUID常量的初始化過程,這些定義是格式CLSID_CoClass和IID_Interface的名稱,與那些由MIDL編譯器産生的類似。
6. 用于第二個類型庫頭部的#include語句。
7. 結尾固定正文:目前包括#pragma pack(pop)。
以上這些部分除頭部固定正文和結尾固定正文部分之外,都被包括在原來的IDL檔案中以library語句指定其名稱的名稱空間中。你可以通過用名稱空間顯式限定或包括如下語句從類型庫頭部使用該名稱。
using namespace MyLib
在源代碼的#import語句之後立即
名稱空間可用#import指令的no_namespace屬性來阻止。但阻止的名稱空間可能導緻名稱沖突。名稱空間也可用rename_namespace屬性重新換名。
編譯器提供完全路徑給需要依賴目前正在處理的類型庫的任何類型庫。路徑以注釋格式寫入到由編譯器為每個處理的類型庫生成的類型庫頭部(.TLH)。
如果一個類型庫包含了對其它類型庫定義的類型引用,.TLH檔案将包括以下注釋:
//
//Cross-referenced type libraries:
//
//#import "c:/path/typelib0.tlb"
//
在#import注釋中的實際檔案名是存儲在寄存器中交叉引用的類型庫全路徑。如果你遇到由于省略類型定義的錯誤時,檢查.TLH頭部的注釋,看哪一種依賴類型庫需要先輸入。在編譯該.TLI檔案時可能的錯誤有文法錯誤(例如C2143,C2146,C2321)、C2501(缺少說明訓示符)或C2433(在資料說明中禁止′inline′)。
你必須确定哪些依賴注釋是不被系統頭部給出的,而是在依賴類型庫的#import指令前的某處給出一個#import指令以消除這些錯誤。
exclude屬性exclude(“稱1”[,“名稱2”,...])
名稱1
被排斥的第一個項
名稱2
被排斥的第二個項(如有必要)
類型庫可能包含在系統頭部或其它類型庫内定義的項的定義。該屬性可用于從生成的類型庫頭檔案中排斥這些項。這個屬性可帶任意數目的參量,每個參量是一個被排斥的進階類型庫項目:
high_method_prefix屬性
high_method_prefix("Prefix")
Prefix
被使用的字首
在預設的情況下,進階錯誤處理屬性和方法用一個無字首命名的成員函數來展示。這個名稱來自于類型庫。high_method_prefix屬性說明一個字首以用于命名這些進階屬性和方法。
high_property_prefixes屬性
high_property_prefixes("GetPrefix,""PutPrefix,""PutRefPrefix")
GetPrefix
用于propget方法的字首
PutPrefix
用于propput方法的字首
PutRefPrefix
用于propputref方法的字首
在預設情況下,進階錯誤處理方法,如propget、propput和propputref,分别采用以字首Get、Put和PutRef命名的成員函數來說明。high_property_prefixes屬性用于分别說明這三種屬性方法的字首。
implementation_only屬性
implementation_only屬性禁止.TLH頭檔案(基本頭檔案)的生成。這個檔案包括了所有用于展示類型庫内容的說明。該.TLI頭檔案和wrapper成員函數的實作,将被生成且包含在編譯過程中。
當指定該屬性時,該.TLI頭部的内容将和用于存放普通.TLH頭部的内容放在相同的名稱空間。此外,該成員函數不會作為聯編說明。implementation_only屬性一般希望與no_implementation屬性配對使用,以跟蹤預編譯頭檔案(PCH)之外的實作。一個有no_implementation屬性的#import語句被置于用來建立pch的源區域中,結果PCH将被一些源檔案所用。一個帶implementation_only屬性的#import語句随後被用在PCH區域之外。在一個源檔案裡隻需用一次這種語句。這将生成不需對每個源檔案進行額外重編譯的所有必要的wrapper成員函數。
注意:一個#import語句中的implementation_only屬性必須和相同類型庫中no_implementation屬性的另一個#import語句配套使用。否則,将産生編譯錯誤。這是因為帶no_implementation屬性的#import語句生成的wrapper類定義需要編譯implementation_only屬性生成的語句實作。
include(...)屬性
Include(名稱1[,名稱2,...])
名稱1
第一個被強制包含的項
名稱2
第二個被強制包含的項(如果必要)
類型庫可能包含在系統頭部或其它類型庫中定義的項的定義。#import指令試圖用自動排斥這些項來避免多重定義錯誤。若這些項已經被排斥,象警告C4192所指出的那樣,且它們不應該被排斥,則這個屬性可用于禁止自動排斥。該屬性可帶任意數目的參量,每個參量應是被包括的類型庫項的名稱。
inject_statement屬性
inject_statement("source_text")
source_text
被插入到類型庫頭檔案的源文本。
inject_statement屬性将其參量作為源文本插入類型庫頭部。此文本被置于包括頭檔案中類型庫内容的名稱空間說明的起始處。
named_guids屬性
named_guids屬性讓編譯器定義和初始化模闆LIBID_MyLib、CLSID_MyCoClass、IID_MyInterface和DIID_MyDispInterface的舊式格式的GUID變量。
no_implementation屬性
該屬性阻止.TLI頭檔案的生成,這個檔案包含wrapper成員函數的實作。如果指定這個屬性,則展示類型庫項說明的.TLH頭将生成沒有一個#include語句包括該.TLI頭檔案。
該屬性與implementation_only屬性配套使用。
no_auto_exclude屬性
類型庫可能包括在系統頭部或其它類型庫中定義的項的定義。#import試圖通過自動排斥這些項來避免多重定義錯誤。當這樣做時,每個被排斥的項都将生成一個C4192警告資訊。你可禁止這個屬性使用自動排斥。
no_namespace屬性
#import頭檔案中的類型庫内容一般定義在一個名稱空間裡。名稱空間的名稱在原來IDL檔案的library語句中指定。如果指定no_namespace屬性,編譯器就不會生成這個名稱空間。
如果你想使用一個不同的名稱空間,應代替使用rename_namespace屬性。
raw_dispinterfaces屬性
raw_dispinterfaces屬性讓編譯器生成一個低級wrapper函數。該函數用于調用IDispatch::Invoke和傳回HRESULT錯誤代碼的dispinterface方法和屬性。如果未指定此屬性,則隻生成進階wrapper,它在失敗時丢棄該C++異常。
raw_interfaces_only屬性
raw_interfaces_only屬性禁止生成錯誤處理wrapper函數以及使用這些wrapper函數的_ _declspec(屬性)說明。
raw_interfaces_only屬性也導緻删除在命名non__property函數中的預設字首。通常該字首是raw_。若指定此屬性,函數名稱将直接從類型庫中生成。該屬性隻允許展示類型庫的低級内容。
raw_method_prefix屬性
raw_method_prefix("Prefix")
Prefix
被使用的字首
用raw_作為預設字首的成員函數展示低層屬性和方法,以避免與進階錯誤處理成員函數的名稱沖突。raw_method_prefix屬性用于指定一個不同的字首。注意: raw_method_prefix屬性的效果不會因raw_method_prefix屬性的存在而改變。在說明一個字首時,raw_method_prefix總是優先于raw_interfaces_only。若兩種屬性用在同一個#import語句中時,則采用raw_method_prefix指定的字首。
raw_native_types屬性
在預設情況下,進階錯誤處理方法在BSTR和VARIANT資料類型和原始COM界面指針的地方使用COM支援類_bctr_t和_variant_t。這些類封裝了配置設定和取消配置設定這些資料類型的存儲器存儲的細節,并且極大地簡化了類型造型和轉換操作。raw_native_types屬性在進階wrapper函數中禁止使用這些COM支援類,并強制替換使用低級資料類型。
raw_property_prefix屬性
raw_property_prefix("GetPrefix","PutPrefix","PutRefPrefix")
GetPrefix
用于propget方法的字首
PutPrefix
用于propput方法的字首
PutRefPrefix
用于propputref方法的字首
在預設情況下,低級方法propget、propput和propputref分别用字尾為get_、put_和putref_的成員函數來展示。這些字首與MIDL生成的頭檔案中的名稱是相容的。raw_property_prefixes屬性分别用于說明這三個屬性方法的字首。
rename屬性
rename("OldName,""NewName")
OldName
類型庫中的舊名
NewName
用于替換舊名的名稱
rename屬性用于解決名稱沖突的問題。若該屬性被指定,編譯器将在類型庫中的OldName的所有出現處用結果頭檔案中使用者提供的NewName替換。
此屬性用于類型庫中的一個名稱和系統頭檔案中的宏定義重合時。若這種情況未被解決,則将産生大量文法錯誤,如C2059和C2061。
注意:這種替換用于類型庫的名稱,而不是用于結果頭檔案中的名稱。
這裡有一個例子:假設類型庫中有一個名稱為MyParent的屬性,且頭檔案中定義了一個用在#import之前的宏GetMyParent。由于GetMyParent是用于錯誤處理屬性get的一個wrapper函數的預設名稱,是以将産生一個名稱沖突。為解決這個問題,使用#import語句中的以下屬性:
rename("MyParent","MyParentX")
該語句将重新命名類型庫中的名稱MyParent,而試圖重新命名GetMyParentwrapper名稱将會出錯:
rename("GetMyParent","GetMyParentX")
這是因為名稱GetMyParent隻出現在結果類型庫頭檔案中。
rename_namespace屬性
rename_namespace("NewName")
NewName
名稱空間的新名稱
rename_namespace屬性用于重新命名包含類型庫内容的名稱空間。它帶有一個指定名稱空間新名newname的參量。
消除名稱空間可以使用no_namespace屬性。
C++特殊處結束
#include指令
#include指令告訴預處理器處理一個指定檔案的内容,就象這些内容以前就在這條指令出現的源程式中。你可以把常量和宏定義放在包含檔案中,然後用#include指令把這些定義加到任何源檔案中。包含檔案對于外部變量和複雜資料類型結合的說明也是有用的。
你隻需在為此目的建立的一個包含檔案中定義和命名這些類型一次。
文法
#include "path-spec"
#include
path_spec是一個前面有目錄說明的任選檔案名。這個檔案名必須命名一個現存檔案。
path_spec的文法依賴于編譯該程式的作業系統。
這兩種文法格式都導緻用已說明的包含檔案的全部内容來替換該指令。兩種格式的差別在于路徑未完整指定時預處理器搜尋頭檔案的順序。
文法格式 動作
引号格式 這種格式訓示預處理器先在包含#include語句的檔案的相同目錄内搜尋,然後在任何包括該檔案的目錄中搜尋。随後預處理器沿着/I編譯器選項指定的路徑搜尋,最後是在INCLUDE環境變量說明的路徑搜尋
尖括号格式 這種格式訓示預處理器首先在/I編譯器選項指定的路徑中搜尋包含檔案。然後在INCLUDE環境變量說明的路徑中搜尋
一旦預處理器找到指定檔案,它就立即停止搜尋。如果用雙引号給出一個明确完整的包含檔案的路徑,預處理器将隻搜尋該路徑規格而忽略标準目錄。
如果在雙引号間的檔案名不是一個完整的路徑規格,預處理器将先搜尋“父”檔案的目錄。父檔案是一個包含#include指令的檔案。例如,如果你把名稱為file2的檔案包括在一個名稱為file1的檔案中,file1就是父檔案。
包含檔案可被嵌套;這指的是一個#include指令出現在以另一個#include指令命名的檔案裡。例如,以上的檔案file2,可包含檔案file3,在這種情況下,file1是file2的父檔案,而且是file3的祖父檔案。
當包含檔案嵌套時,目錄搜尋首先由父檔案的目錄開始,然後,搜尋祖父檔案的目錄。
是以,搜尋從包含目前處理源檔案的目錄開始,若檔案未找到,搜尋就轉到/I編譯器選項指定的目錄,最後搜尋include環境變量指定的目錄。
下面的例子給出使用尖括号的檔案包括:
#include
這個例子把名稱為STDIO.H的檔案内容加入到源程式中。尖括号訓示預處理器在搜尋完/I編譯器選項說明的目錄之後,搜尋STDIO.H的環境變量指定的目錄。下面的例子給出用引号格式的檔案包括:#include "defs.h"
這個例子把DEFS.H指定的檔案内容加入源程式。雙引号标記意味着預處理器首先搜尋包含父源檔案的目錄。
包含檔案的嵌套可高達10層,隻要在處理嵌套的#include指令時,預處理器就會不斷地把包含檔案加入到最初的源檔案中。
Microsoft特殊處
為了定位可包括源檔案,預處理器首先搜尋/I編譯器選項指定的目錄。若/I選項未給定或已失敗,預處理器就用INCLUDE環境變量搜尋尖括号内的包含檔案。INCLUDE環境變量和/I編譯器選項可包含用分号分開的多個路徑。若在/I選項的部分或在INCLUDE環境變量裡有多于一個的目錄,預處理器将以它們出現的順序對它們進行搜尋。
例如,指令:
CL /ID:/MSVC/INCLUDE MYPROG.C
導緻預處理器在目錄D:/MSVC/INCLUDE中搜尋諸如STDIO.H的包含檔案。指令:SET INCLUDE=D:/MSVC/INCLUDE
CL MYPROG.C
有相同的作用。如果所有搜尋都失敗了,将産生一個緻命編譯錯誤。
如果用包括一個冒号的路徑(例如,F:/MSVC/SPECIAL/INCL/TEST.H)來完整地說明一個包含檔案的檔案名,預處理器将沿此路徑搜尋。
對于指定為#include "path_spec"的包含檔案,目錄搜尋将從父檔案的目錄開始,然後搜尋祖父檔案的目錄。是以,搜尋将從包含目前處理的#include指令的源檔案的目錄開始,如果沒有祖父檔案或檔案未找到,搜尋将繼續,就像檔案名包括在尖括号中一樣。
Microsoft特殊處結束
#line指令
#line指令告訴預處理器将編譯器内部存儲的行号和檔案名轉變為一個給定的行号和檔案名。編譯器使用該行号和檔案名指出編譯過程中發現的錯誤。行号一般指的是目前輸入行,檔案名指目前輸入檔案。每處理一行,行号就增1。
文法
#line
數字序列 “檔案名”opt
數字序列的值可以是任何整型常數。宏替換可在預處理語言符号中執行,但結果必須求值為正确的文法。檔案名可以是任意字元的組合,且應括在雙引号(“”)間。如果省略檔案名,則前面的檔案名保持不變。
你可以通過編寫一個#line指令來改動源行号和檔案名。翻譯器使用行号和檔案名來确定預定義宏__FILE_ _和_ _LINE_ _的值。你可以使用這些宏把自描述錯誤消息加入到程式文本中。有關這些宏的更多資訊參見預定義的宏。
__FILE_ _宏擴充成内容為用雙引号(“”)括起的檔案名的一個字元串。
如果你改變行号和檔案名,編譯器将忽略原有的值,用新值繼續處理。#line指令通常被程式生成器用來生成指向最初源程式的錯誤消息,而不是生成程式。下面的例子用于說明#line以及_ _LINE_ _和_ _FILE_ _宏。在這個語句中,内部存儲的行号設定為151,檔案名改為copy.c。
#line 151 "copy.c"
在這個例子中,若一個給定的“斷言”(assertion)不為真,則宏ASSERT使用預定義宏__LINE_ _和_ _FILE_ _列印出一個關于源檔案的錯誤消息。
#define ASSERT(cond)
if( !(cond) ) /
{ printf("assertion error line %d, file(%s)/n",/
__LINE_ _,_ _FILE_ _); }
Null指令
空預處理器指令是一行中一個單獨的數字标号(#),無任何作用。
文法
#
#undef指令
正如其名所隐含的,#undef指令取消(反定義)一個原來由#define指令建立的名稱。
文法
#undef
辨別符
#undef指令取消辨別符的目前定義。其結果是,辨別符的每次出現都将被預處理器所忽略。為取消一個用#undef的宏定義,隻須給出宏的辨別符,不須給出參數表。
你也可以将#undef指令用于一個原來未定義的辨別符。這将确認這個辨別符是未定義的。宏替換不能在#undef語句中執行。
#undef指令通常和一個#define指令比對,以在源程式中建立一個區域,在這個區域中一個辨別符有其特定的含義。例如,源程式的一個特有函數可以使用顯式常量定義不影響程式餘下部分的環境特定值。#undef指令也可與#if指令配對以控制源程式的條件編譯過程。有關更多資訊參見“#if、#elif、#else和#endif指令”。
下面的例子中,#undef指令取消了一個符号常量和一個宏的定義,注意該指令隻給出了宏的辨別符。
#define WIDTH 80
#define ADD(X,Y) (X)+(Y)
...#undef WIDTH
#undef ADD
Microsoft特殊處
宏可通過采用/U選項的指令行反定義,此指令行後跟反定義的宏名稱。此指令與在檔案開頭處的#undef 宏名稱語句序列的作用是相等的。
Microsoft特殊處結束
預處理器操作符
#define指令的文本中有四種預處理器特有的操作符(它們的總結參見下面的表)。
字元化、字元串化和語言符号粘貼操作符将在下面三章中讨論。defined操作符的資訊參見“#if、#elif、#else和#endif指令”。
運算符 動作
字元串化操作符(#) 将相應實參置于雙引号内
字元化操作符(#@) 将相應的參量置于單引号内,且将其作為字元處理(Microsoft特殊處)
語言符号粘貼操作符(##) 可将語言符号作為實參使用,且将其合并為其它 的語言符号
續表
定義的操作符 簡化在某特定宏指令中複合表達式的寫法
字元串化操作符(#)
數字元号或“字元串化”操作符(#)将宏參數(擴充後)轉化為字元串常量。它隻用于帶參量的宏。如果它在宏定義中的一個形式參量之前,宏調用傳給的實際參量就被括在雙括号中,且被看作為一個字元串文字。然後該字元串文字将替換該宏定義中操作符和形參組合的每次出現。
實參的第一個語言符号之前和最後一個語言符号之後的空白被忽略。實參中語言符号之間的所有空白在結果字元串語義中都被看作為一個空格。是以,若實參中的一個注解出現在兩個語言符号之間,它将被看作為一個空格。結果字元串文字自動地與任何僅用空格分開的相鄰字元串文字連接配接。
此外,如果一個包含在參量裡的字元在用作一個字元串文字(例如,雙引号(")或反斜杠(/)字元)時通常需要一個轉義序列,必要的轉義反斜杠被自動地插入字元之前。下面的例子給出了一個包含字元串化操作符的宏定義和一個調用該宏的main函數:
#define stringer(x) printf(#x "/n")
void main( )
{
stringer(In quotes in the printf function call/n);
stringer("In quotes when printed to the screen"/n);
stringer("This:/" prints an escaped double quote");
}
這種調用在預處理時會被擴充,産生如下代碼:
void main()
{
printf("In quotes in the printf function call/n" "/n");
printf("/"In quotes when printed to the screen/"/n" "/n");
printf("/"This; ///" prints an escaped double quote /"" "/n");
}
當運作該程式時,每行的螢幕輸出如下:
In quotes in the printf function call
"In quotes when printed to the screen"
"This; /" prints an escaped double quotation mark"
Microsoft特殊處
Microsoft C(版本6.0及更早版本)擴充ANSI C的标準,ANSI C擴充在字元串文字和字元常量中出現的宏形式參量不再被支援。依賴于此擴充的代碼應該使用字元串化操作符(#)重寫。
Microsoft特殊處結束
字元化操作符(#@)
Microsoft特殊處
字元化操作符隻可用于宏參量,若宏定義中#@在一個形參前,則實參應被放在單引号中,在宏擴充時作為一個字元處理。例如:
#define makechar(x) #@x
将語句:
a=makechar(b);
擴充為:
a='b';
單引号字元不能用于字元化操作符。
Microsoft特殊處結束語
言符号粘貼操作符(##)
雙數字語言符号或“語言符号粘貼”操作符(##),有時稱作“合并”操作符,用于類對象宏和類函數宏中。它允許将分開的語言符号加入一個單個語言符号中,是以不能是宏定義的第一個語言符号或最後一個語言符号。
如果一個宏定義中的形參在語言符号粘貼操作符的前後,則形參将立即被未擴充的實參替換。在替換之前不對參量執行宏擴充。
然後,語言符号字元串中語言符号粘貼操作符的每次出現将被删除,其前後的語言符号将被合并。其結果語言符号必須是一個有效的語言符号。若其有效,如果該語言符号代表一個宏名稱,則掃描它以發現可能的替換。該辨別符表示在替換前程式中己知合并的語言符号的名稱。每個語言符号都代表一個在程式中或在編譯器指令行中定義的語言符号。
該操作符前後的空白是任意的。
如下例子說明了程式輸出中字元串化操作符和語言符号粘貼操作符的用法:#define paster(n) printf("token" #n "=%d",taken##n)
int token9=9;
若一個宏用一個類似于下面的數值參量調用:
paster(9);
宏将生成:
printf("token" "9" "=%d",token9);
它變成為:
printf("token9 = %d", token9 );
宏
對宏擴充的預處理在所有那些不是預處理指令的行(第一個非空白字元不是#的行),以及其指令并未作為條件編譯的一部分而忽略的行中進行。“條件編譯”指令允許通過檢測一個常量表達式或辨別符以決定在預處理過程中哪個文本塊送入編譯器、哪個文本塊從源檔案中删除,并以此種方式控制一個源檔案中某部分的編譯。
#define指令通常使用有意義的辨別符與常量、關鍵字、常用語句和表達式關聯。表示常量的辨別符有時被稱作“符号常量”或“顯式”常量。表示語句或表達式的常量稱為“宏”。在本預處理器文檔中,隻使用術語“宏”。
當宏的名稱在程式源文本或在某些其它預處理器指令的參量中被識别時,它被處理為對該宏的調用。宏名稱被宏體的一個拷貝所替換。若該宏接受參量,宏名稱後的實參就會替換宏體中的形參。用宏體中處理的拷貝來替換一個宏調用的過程,稱為宏調用的“擴充”。
實際的術語中有兩種類型的宏。“類對象”宏不帶參量,而“類函數”宏可定義為帶參量。是以它們的形式和功能都象函數調用,由于宏不生成實際的函數調用,是以有時可用宏替代函數調用使程式運作得更快,(在C++中,inline函數通常是一個好方法),然而,如果不小心的定義和使用宏,也可能造成麻煩。在帶參量的宏定義時,你必須使用括号以保持一個表達式中正常的優先級,同時宏也不能正确地處理具有副作用的表達式。有關更多的資訊參見“#define指令”中的例子getrandom。
一旦你定義了一個宏,你不能不經取消該宏原有定義,而重新定義它為一個不同的值。但可用正好相同的定義來重定義該宏,是以,一個程式中宏的相同定義可出現多次。
#undef指令用于取消宏的定義。一旦取消該宏的定義,就可重新定義該宏為一個不同的值。#define和#undef兩節分别詳細讨論了#define和#undef指令。
宏和C++
C++提供了一些新的功能。其中有些功能替代了原來由ANSI C所提供的功能。這些新的功能增強了類型安全性和該語言的可預測性:
* 在C++中,以const說明的對象可用于常量表達式中,這使程式說明有類型和值資訊的常量,以及能被調試器逐個字元檢查的枚舉值的常量。使用預處理器指令#define定義常量并不精确。除非在程式中找到一個帶位址的表達式,否則一個const對象将不配置設定任何存儲。
* C++聯編函數替代了函數類型宏,相對于宏來說使用聯編函數的優勢在于:
* 類型安全性。聯編函數和一般函數一樣需進行相同的類型檢測,宏無類型安全性檢測。
* 糾正具有副作用的參量處理。聯編函數在進入函數體之前對參量的表達式求值。是以,一個有副作用的表達式将是安全的。
對于聯編函數的更多資訊參見inline、_ _inline節。為了向下相容,Microsoft C++保留了所有在ANSI C和更早C++規格中的預處理器功能。
預定義宏
編譯器可識别六種預定義的ANSI C宏(參見表1.1),而Microsoft C++實作提供更多的預定義宏(參見表1.2)。這些宏不帶參量,但不能被重定義。它們的值(除__LINE_ _和_ _FILE_ _外)必須是經過編譯的常量。下面列出的一些預定義宏須用多個值來定義,它們的值可在Visual C++開發環境中選擇相應的菜單選項來設定或采用指令行開關。更多的資訊參見下表。
表1.1 ANSI 預定義宏
宏 | 說明 |
__DATE | _ _目前源檔案的編譯日期。日期是格式為Mmm dd yyyy的字元串文字。月份名稱Mmm與在TIME.H中說明的庫函數asctime産生的日期一樣 |
__FILE_ _ | 目前源檔案名稱。__FILE_ _擴充為用雙引号括起的一個字元串 |
__LINE_ _ | 目前源檔案的行号。該行号是一個十進制整型常量。可用一個#line指令修改 |
__STDC_ _ | 指出與ANSI C标準的完全一緻性。僅當給出/Za編譯器選項且不編譯C++代碼時定義為整型量1;否則是不确定的 |
__TIME_ _ | 目前檔案的最近編譯時間。該時間是格式為hh:mm:ss的字元串文字 |
__TIMESTAMP_ _ | 目前源檔案的最近修改日期。日期是格式為Ddd Mmm Datehh:mm:ss yyyy的字元串文字,這裡Ddd是星期幾的簡寫,Date是從1到31的一個整數表 |
表1.2 Microsoft特殊預定義的宏
宏 | 說明 |
__CHAR_UNSIGNED | 預設char類型是無符号的,當指定/J時定義的 |
__cplusplus | 僅為C++程式定義 |
__CPPRTTI | 定義為用/GR編譯的代碼(允許運作時類型資訊) |
__CPPUNWIND | 定義為用/GX編譯的代碼(允許異常處理) |
__DLL | 指定/MD或/MDd(多線程DLL)時定義的 |
__M_ALPHA | 為DEC ALPHA平台定義,使用ALPHA編譯器時定義為1,若使用另一個編譯器時不定義 |
__M_IX86 | 為x86處理器定義,參見表1.3 |
__M_MPPC | 為Power Macintosh平台定義,預設為601(/QP601)參見表1.4 |
__M_MRX000 | 為MIPS平台定義,預設為4000(/QMR4000),參見表1.5 |
__M_PPC | 為PowerPC平台定義,預設為604(/QP604),參見表1.6__MFC_VER為MFC版本定義,為Microsoft Founndation類庫4.21定義為0x0421,它總是定義的 |
__MSC_EXTENSIONS | 該宏在使用/Ze編譯選項(預設值)時定義,定義時其值總為1 |
__MSC_VER | 定義編譯器版本,對于Microsoft Visual C++ 6.0定義為1200,它總是定義的 |
__MT | 當指定/MD或/MDd(多線程DLL)或/MT或/MTd(多線程)選項時定義 |
__WIN32 | 為Win32應用程式而定義。它總是定義的 |
如下表所示,編譯器對反映處理器選項的預處理器辨別符産生一個值。
表1.3 _M_IX86的值
開發者的選項 | 指令行選項 | 傳回值 |
Blend | /GB | _M_IX86=500(預設值。将來的編譯器将給出一個不同的值以影響主處理器) |
Pentium | /G5 | _M_IX86=500 |
Pentiumpro | /G6 | _M_IX86=600 |
80386 | /G3 | _M_IX86=300 |
80486 | /G4 | _M_IX86=400 |
表1.4 _M_MPPC的值
開發者的選項 | 指令行選項 | 傳回值 |
PowerPC 601 | /QP601 | _M_MPPC=601(預設值) |
PowerPC 603 | /QP603 | _M_MPPC=603 |
PowerPC 604 | /QP604 | _M_MPPC=604 |
PowerPC 620 | /QP620 | _M_MPPC=620 |
表1.5 _M_MRX000的值
開發者選項 | 指令行選項 | 傳回值 |
R4000 | /QMR4000 | _M_MRX000=4000(預設值) |
R4100 | /QMR4100 | _M_MRX000=4100 |
R4200 | /QMR4200 | _M_MRX000=4200 |
R4400 | /QMR4400 | _M_MRX000=4400 |
R4600 | /QMR4600 | _M_MRX000=4600 |
R10000 | /QMR10000 | _M_MRX000=10000 |
表1.6 _M_PPC的值
開發者選項 | 指令行選項 | 傳回值 |
R4000 | /QMR4000 | _M_MRX000=4000(預設值) |
R4100 | /QMR4100 | _M_MRX000=4100 |
R4200 | /QMR4200 | _M_MRX000=4200 |
R4400 | /QMR4400 | _M_MRX000=4400 |
R4600 | /QMR4600 | _M_MRX000=4600 |
R10000 | /QMR10000 | _M_MRX000=10000 |