第1章 預 處 理 器
預處理器是一種處理源文件文本的文本處理器,它是翻譯起始階段的一個組成部分。
預處理器並不在語法上分析處理源文本,但出於定位宏調用的目的,它將源文本分開語言符號。雖然編譯器一般在初次編譯時啓動預處理器,但預處理器也可以不經編譯,單獨地處理文本。
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 |
兩個頭文件都在用/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 |