預處理過程掃描源代碼,對其進行初步的轉換,產生新的源代碼提供給編譯器。
在C語言中,並沒有任何內在的機制來完成如下一些功能:在編譯時包含其他源文件、定義宏、根據條件決定編譯時是否包含某些代碼。要完成這些工作,就 需要使用預處理程序。儘管在目前絕大多數編譯器都包含了預處理程序,但通常認爲它們是獨立於編譯器的。預處理過程讀入源代碼,檢查包含預處理指令的語句和 宏定義,並對源代碼進行響應的轉換。預處理過程還會刪除程序中的註釋和多餘的空白字符。
預處理指令是以#號開頭的代碼行。#號必須是該行除了任何空白字符外的第一個字符。#後是指令關鍵字,在關鍵字和#號之間允許存在任意個數的空白字符。整行語句構成了一條預處理指令,該指令將在編譯器進行編譯之前對源代碼做某些轉換。下面是部分預處理指令:
指令用途
#空指令,無任何效果
#include包含一個源代碼文件
#define定義宏
#undef取消已定義的宏
#if如果給定條件爲真,則編譯下面代碼
#ifdef如果宏已經定義,則編譯下面代碼 (很少使用)
#ifndef如果宏沒有定義,則編譯下面代碼(#ifndef 它是一個判斷語句,就想if(){} else{}一樣,它的功能是判斷後面的頭文件是被定義爲宏,如果被定義宏,則跳出,不執行下面的語句,否則,就執行下面的語句)
#elif如果前面的#if給定條件不爲真,當前條件爲真,則編譯下面代碼
#endif結束一個#if……#else條件編譯塊
#error停止編譯並顯示錯誤信息
一、文件包含
#include預處理指令的作用是在指令處展開被包含的文件。包含可以是多重的,也就是說一個被包含的文件中還可以包含其他文件。標準C編譯器至少支持八重嵌套包含。
預處理過程不檢查在轉換單元中是否已經包含了某個文件並阻止對它的多次包含。這樣就可以在多次包含同一個頭文件時,通過給定編譯時的條件來達到不同的效果。例如:
#defineAAA
#include"t.c"
#undefAAA
#include"t.c"
爲了避免那些只能包含一次的頭文件被多次包含,可以在頭文件中用編譯時條件來進行控制。例如:
/*my.h*/
#ifndef MY_H
#define MY_H
……
#endif
在程序中包含頭文件有兩種格式:
#include<my.h>
#include"my.h"
第 一種方法是用尖括號把頭文件括起來。這種格式告訴預處理程序在編譯器自帶的或外部庫的頭文件中搜索被包含的頭文件。第二種方法是用雙引號把頭文件括起來。 這種格式告訴預處理程序在當前被編譯的應用程序的源代碼文件中搜索被包含的頭文件,如果找不到,再搜索編譯器自帶的頭文件。
採用兩種不同包含格式的理由在於,編譯器是安裝在公共子目錄下的,而被編譯的應用程序是在它們自己的私有子目錄下的。一個應用程序既包含編譯器提供的公共頭文件,也包含自定義的私有頭文件。採用兩種不同的包含格式使得編譯器能夠在很多頭文件中區別出一組公共的頭文件。
二、宏
宏 定義了一個代表特定內容的標識符。預處理過程會把源代碼中出現的宏標識符替換成宏定義時的值。宏最常見的用法是定義代表某個值的全局符號。宏的第二種用法 是定義帶參數的宏,這樣的宏可以象函數一樣被調用,但它是在調用語句處展開宏,並用調用時的實際參數來代替定義中的形式參數。
1.#define指令
#define預處理指令是用來定義宏的。該指令最簡單的格式是:首先神明一個標識符,然後給出這個標識符代表的代碼。在後面的源代碼中,就用這些代碼來替代該標識符。這種宏把程序中要用到的一些全局值提取出來,賦給一些記憶標識符。
#define MAX_NUM 10
intarray[MAX_NUM];
for(i=0;i<MAX_NUM;i++)/*……*/
在 這個例子中,對於閱讀該程序的人來說,符號MAX_NUM就有特定的含義,它代表的值給出了數組所能容納的最大元素數目。程序中可以多次使用這個值。作爲 一種約定,習慣上總是全部用大寫字母來定義宏,這樣易於把程序紅的宏標識符和一般變量標識符區別開來。如果想要改變數組的大小,只需要更改宏定義並重新編 譯程序即可。
宏表示的值可以是一個常量表達式,其中允許包括前面已經定義的宏標識符。例如:
#define ONE1
#define TWO2
#define THREE (ONE+TWO)
注意上面的宏定義使用了括號。儘管它們並不是必須的。但出於謹慎考慮,還是應該加上括號的。例如:
six=THREE*TWO;
預處理過程把上面的一行代碼轉換成:
six=(ONE+TWO)*TWO;
如果沒有那個括號,就轉換成six=ONE+TWO*TWO;了。
宏還可以代表一個字符串常量,例如:
#define VERSION"Version1.0 Copyright(c)2003"
2.帶參數的#define指令
帶參數的宏和函數調用看起來有些相似。看一個例子:
#define Cube(x)(x)*(x)*(x)
可以時任何數字表達式甚至函數調用來代替參數x。這裏再次提醒大家注意括號的使用。宏展開後完全包含在一對括號中,而且參數也包含在括號中,這樣就保證了宏和參數的完整性。看一個用法:
intnum=8+2;
volume=Cube(num);
展開後爲(8+2)*(8+2)*(8+2);
如果沒有那些括號就變爲8+2*8+2*8+2了。
下面的用法是不安全的:
volume=Cube(num++);
如果Cube是一個函數,上面的寫法是可以理解的。但是,因爲Cube是一個宏,所以會產生副作用。這裏的擦書不是簡單的表達式,它們將產生意想不到的結果。它們展開後是這樣的:
volume=(num++)*(num++)*(num++);
很顯然,結果是10*11*12,而不是10*10*10;
那麼怎樣安全的使用Cube宏呢?必須把可能產生副作用的操作移到宏調用的外面進行:
intnum=8+2;
volume=Cube(num);
num++;
3.#運算符
出現在宏定義中的#運算符把跟在其後的參數轉換成一個字符串。有時把這種用法的#稱爲字符串化運算符。例如:
#define PASTE(n)"adhfkj"#n
main()
{
printf("%s/n",PASTE(15));
}
宏定義中的#運算符告訴預處理程序,把源代碼中任何傳遞給該宏的參數轉換成一個字符串。所以輸出應該是adhfkj15。
4.##運算符
##運算符用於把參數連接到一起。預處理程序把出現在##兩側的參數合併成一個符號。看下面的例子:
#define NUM(a,b,c) a##b##c
#define STR(a,b,c) a##b##c
main()
{
printf("%d/n",NUM(1,2,3));
printf("%s/n",STR("aa","bb","cc"));
}
最後程序的輸出爲:
123
aabbcc
千萬別擔心,除非需要或者宏的用法恰好和手頭的工作相關,否則很少有程序員會知道##運算符。絕大多數程序員從來沒用過它。
三、條件編譯指令
條件編譯指令將決定那些代碼被編譯,而哪些是不被編譯的。可以根據表達式的值或者某個特定的宏是否被定義來確定編譯條件。
1.#if指令
#if指令檢測跟在製造另關鍵字後的常量表達式。如果表達式爲真,則編譯後面的代碼,知道出現#else、#elif或#endif爲止;否則就不編譯。
2.#endif指令
#endif用於終止#if預處理指令。
#define DEBUG0
main()
{
#if DEBUG
printf("Debugging/n");
#endif
printf("Running/n");
}
由於程序定義DEBUG宏代表0,所以#if條件爲假,不編譯後面的代碼直到#endif,所以程序直接輸出Running。
如果去掉#define語句,效果是一樣的。
3.#ifdef和#ifndef
#define DEBUG
main()
{
#ifdef DEBUG
printf("yes/n");
#endif
#ifndefDEBUG
printf("no/n");
#endif
}
#ifdefined等價於#ifdef;#if!defined等價於#ifndef
4.#else指令
#else指令用於某個#if指令之後,當前面的#if指令的條件不爲真時,就編譯#else後面的代碼。#endif指令將中指上面的條件塊。
#define DEBUG
main()
{
#ifdef DEBUG
printf("Debugging/n");
#else
printf("Notdebugging/n");
#endif
printf("Running/n");
}
5.#elif指令
#elif預處理指令綜合了#else和#if指令的作用。
#define TWO
main()
{
#ifdef ONE
printf("1/n");
#elifdefined TWO
printf("2/n");
#else
printf("3/n");
#endif
}
程序很好理解,最後輸出結果是2。
6. #error指令
#error指令將使編譯器顯示一條錯誤信息,然後停止編譯。
#error message :編譯器遇到此命令時停止編譯,並將參數message輸出。該命令常用於程序調試。
#error指令 語法格式如下:
#error token-sequence
編譯程序時,只要遇到 #error 就會跳出一個編譯錯誤,既然是編譯錯誤,要它幹嘛呢?其目的就是保證程序是按照你所設想的那樣進行編譯的。
下面舉個例子:
程序中往往有很多的預處理指令
#ifdef XXX
...
#else
#endif
當程序比較大時,往往有些宏定義是在外部指定的(如makefile),或是在系統頭文件中指定的,當你不太確定當前是否定義了 XXX 時,就可以改成如下這樣進行編譯:
#ifdef XXX
...
#error "XXX has been defined"
#else
#endif
這樣,如果編譯時出現錯誤,輸出了XXX has been defined,表明宏XXX已經被定義了。
其實就是在編譯的時候輸出編譯錯誤信息token-sequence,從方便程序員檢查程序中出現的錯誤。
簡單的例子
#include "stdio.h"
int main(int argc, char* argv[])
{
#define CONST_NAME1 "CONST_NAME1"
printf("%s/n",CONST_NAME1);
#undef CONST_NAME1
#ifndef CONST_NAME1
#error No defined Constant Symbol CONST_NAME1
#endif
......
return 0;
}
在編譯的時候輸出如編譯信息
fatal error C1189: #error : No defined Constant Symbol CONST_NAME1
7.#pragma指令
#pragma指令沒有正式的定義。編譯器可以自定義其用途。典型的用法是禁止或允許某些煩人的警告信息。
在所有的預處理指令中,#pragma 指令可能是最複雜的了,它的作用是設定編譯器的狀態或者是指示編譯器完成一些特定的動作。
#pragma指令對每個編譯器給出了一個方法,在保持與C和C++語言完全兼容的情況下,給出主機或操作系統專有的特徵。
依據定義,編譯指示是機器或操作系統專有的,且對於每個編譯器都是不同的。
其格式一般爲: #pragma para
其中para爲參數,下面來看一些常用的參數。
(1)message 參數
message參數是我最喜歡的一個參數,它能夠在編譯信息輸出窗口中輸出相應的信息,
這對於源代碼信息的控制是非常重要的。其使用方法爲:
#pragma message("消息文本")
當編譯器遇到這條指令時就在編譯輸出窗口中將消息文本打印出來。
當我們在程序中定義了許多宏來控制源代碼版本的時候,我們自己有可能都會忘記有沒有正確的設置這些宏,
此時我們可以用這條指令在編譯的時候就進行檢查。假設我們希望判斷自己有沒有在源代碼的什麼地方定義了_X86這個宏,
可以用下面的方法:
#ifdef _X86
#pragma message("_X86 macro activated!")
#endif
我們定義了_X86這個宏以後,應用程序在編譯時就會在編譯輸出窗口裏顯示"_86 macro activated!"。
我們就不會因爲不記得自己定義的一些特定的宏而抓耳撓腮了。
(2)另一個使用得比較多的pragma參數是code_seg
格式如:
#pragma code_seg( ["section-name" [, "section-class"] ] )
它能夠設置程序中函數代碼存放的代碼段,當我們開發驅動程序的時候就會使用到它。
(3)#pragma once (比較常用)
只要在頭文件的最開始加入這條指令就能夠保證頭文件被編譯一次,這條指令實際上在VC6中就已經有了,
但是考慮到兼容性並沒有太多的使用它。
(4)#pragma hdrstop
表示預編譯頭文件到此爲止,後面的頭文件不進行預編譯。BCB可以預編譯頭文件以加快鏈接的速度,
但如果所有頭文件都進行預編譯又可能佔太多磁盤空間,所以使用這個選項排除一些頭文件。
有時單元之間有依賴關係,比如單元A依賴單元B,所以單元B要先於單元A編譯。
你可以用#pragma startup指定編譯優先級,如果使用了#pragma package(smart_init),
BCB就會根據優先級的大小先後編譯。
(5)#pragma resource "*.dfm"
表示把*.dfm文件中的資源加入工程。*.dfm中包括窗體
外觀的定義。
(6)#pragma warning( disable: 4507 34; once: 4385; error: 164 )
等價於:
#pragma warning( disable: 4507 34 ) // 不顯示4507和34號警告信息
#pragma warning( once: 4385 ) // 4385號警告信息僅報告一次
#pragma warning( error: 164 ) // 把164號警告信息作爲一個錯誤。
同時這個pragma warning 也支持如下格式:
#pragma warning( push [, n ] )
#pragma warning( pop )
這裏n代表一個警告等級(1---4)。
#pragma warning( push )保存所有警告信息的現有的警告狀態。
#pragma warning( push, n )保存所有警告信息的現有的警告狀態,並且把全局警告等級設定爲n。
#pragma warning( pop )向棧中彈出最後一個警告信息,在入棧和出棧之間所作的一切改動取消。例如:
#pragma warning( push )
#pragma warning( disable: 4705 )
#pragma warning( disable: 4706 )
#pragma warning( disable: 4707 )
//.......
#pragma warning( pop )
在這段代碼的最後,重新保存所有的警告信息(包括4705,4706和4707)。
(7)#pragma comment(...)
該指令將一個註釋記錄放入一個對象文件或可執行文件中。
常用的lib關鍵字,可以幫我們連入一個庫文件。如:
#pragma comment(lib, "comctl32.lib")
#pragma comment(lib, "vfw32.lib")
#pragma comment(lib, "wsock32.lib")
每個編譯程序可以用#pragma指令激活或終止該編譯程序支持的一些編譯功能。
例如,對循環優化功能:
#pragma loop_opt(on) // 激活
#pragma loop_opt(off) // 終止
有時,程序中會有些函數會使編譯器發出你熟知而想忽略的警告,
如“Parameter xxx is never used in function xxx”,可以這樣:
#pragma warn —100 // Turn off the warning message for warning #100
int insert_record(REC *r)
{ /* function body */ }
#pragma warn +100 // Turn the warning message for warning #100 back on
函數會產生一條有唯一特徵碼100的警告信息,如此可暫時終止該警告。
每個編譯器對#pragma的實現不同,在一個編譯器中有效在別的編譯器中幾乎無效。可從編譯器的文檔中查看。
補充 —— #pragma pack 與 內存對齊問題
許多實際的計算機系統對基本類型數據在內存中存放的位置有限制,它們會要求這些數據的首地址的值是某個數k
(通常它爲4或8)的倍數,這就是所謂的內存對齊,而這個k則被稱爲該數據類型的對齊模數(alignment modulus)。
Win32平臺下的微軟C編譯器(cl.exe for 80x86)在默認情況下采用如下的對齊規則:
任何基本數據類型T的對齊模數就是T的大小,即sizeof(T)。比如對於double類型(8字節),
就要求該類型數據的地址總是8的倍數,而char類型數據(1字節)則可以從任何一個地址開始。
Linux下的GCC奉行的是另外一套規則(在資料中查得,並未驗證,如錯誤請指正):
任何2字節大小(包括單字節嗎?)的數據類型(比如short)的對齊模數是2,而其它所有超過2字節的數據類型
(比如long,double)都以4爲對齊模數。
ANSI C規定一種結構類型的大小是它所有字段的大小以及字段之間或字段尾部的填充區大小之和。
填充區就是爲了使結構體字段滿足內存對齊要求而額外分配給結構體的空間。那麼結構體本身有什麼對齊要求嗎?
有的,ANSI C標準規定結構體類型的對齊要求不能比它所有字段中要求最嚴格的那個寬鬆,可以更嚴格。
如何使用c/c++中的對齊選項
vc6中的編譯選項有 /Zp[1|2|4|8|16] ,/Zp1表示以1字節邊界對齊,相應的,/Zpn表示以n字節邊界對齊。
n字節邊界對齊的意思是說,一個成員的地址必須安排在成員的尺寸的整數倍地址上或者是n的整數倍地址上,取它們中的最小值。
也就是:
min ( sizeof ( member ), n)
實際上,1字節邊界對齊也就表示了結構成員之間沒有空洞。
/Zpn選項是應用於整個工程的,影響所有的參與編譯的結構。
要使用這個選項,可以在vc6中打開工程屬性頁,c/c++頁,選擇Code Generation分類,在Struct member alignment可以選擇。
要專門針對某些結構定義使用對齊選項,可以使用#pragma pack編譯指令:
(1) #pragma pack( [ n ] )
該指令指定結構和聯合成員的緊湊對齊。而一個完整的轉換單元的結構和聯合的緊湊對齊由/Zp 選項設置。
緊湊對齊用pack編譯指示在數據說明層設置。該編譯指示在其出現後的第一個結構或聯合說明處生效。
該編譯指示對定義無效。
當你使用#pragma pack ( n ) 時, 這裏n 爲1、2、4、8 或16。
第一個結構成員之後的每個結構成員都被存儲在更小的成員類型或n 字節界限內。
如果你使用無參量的#pragma pack, 結構成員被緊湊爲以/Zp 指定的值。該缺省/Zp 緊湊值爲/Zp8 。
(2) 編譯器也支持以下增強型語法:
#pragma pack( [ [ { push | pop } , ] [ identifier, ] ] [ n] )
若不同的組件使用pack編譯指示指定不同的緊湊對齊, 這個語法允許你把程序組件組合爲一個單獨的轉換單元。
帶push參量的pack編譯指示的每次出現將當前的緊湊對齊存儲到一個內部編譯器堆棧中。
編譯指示的參量表從左到右讀取。如果你使用push, 則當前緊湊值被存儲起來;
如果你給出一個n 的值, 該值將成爲新的緊湊值。若你指定一個標識符, 即你選定一個名稱,
則該標識符將和這個新的的緊湊值聯繫起來。
帶一個pop參量的pack編譯指示的每次出現都會檢索內部編譯器堆棧頂的值,並且使該值爲新的緊湊對齊值。
如果你使用pop參量且內部編譯器堆棧是空的,則緊湊值爲命令行給定的值, 並且將產生一個警告信息。
若你使用pop且指定一個n的值, 該值將成爲新的緊湊值。若你使用p o p 且指定一個標識符,
所有存儲在堆棧中的值將從棧中刪除, 直到找到一個匹配的標識符, 這個與標識符相關的緊湊值也從棧中移出,
並且這個僅在標識符入棧之前存在的緊湊值成爲新的緊湊值。如果未找到匹配的標識符,
將使用命令行設置的緊湊值, 並且將產生一個一級警告。缺省緊湊對齊爲8 。
pack編譯指示的新的增強功能讓你編寫頭文件, 確保在遇到該頭文件的前後的
緊湊值是一樣的。
(3) 棧內存對齊
在vc6中棧的對齊方式不受結構成員對齊選項的影響。它總是保持對齊,而且對齊在4字節邊界上。
8.#line指令
#line指令可以改變編譯器用來指出警告和錯誤信息的文件號和行號。
補充:
預處理就是在進行編譯的第一遍詞法掃描和語法分析之前所作的工作。說白了,就是對源文件進行編譯前,先對預處理部分進行處理,然後對處理後的代碼進行編譯。這樣做的好處是,經過處理後的代碼,將會變的很精短。
關於預處理命令中的文件包含(#include),宏定義(#define),書上已經有了詳細的說明,在這裏就不詳述了。這裏主要是對條件編譯(#ifdef,,#ifndef,#else,#endif,#if等)進行說明。以下分3種情況:
1:情況1:
#ifdef _XXXX
...程序段1...
#else
...程序段2...
#endif
這表明如果標識符_XXXX已被#define命令定義過則對程序段1進行編譯;否則對程序段2進行編譯。
例:
#define NUM
.............
.............
.............
#ifdef NUM
printf("之前NUM有過定義啦!:) /n");
#else
printf("之前NUM沒有過定義!:( /n");
#endif
}
如果程序開頭有#define NUM這行,即NUM有定義,碰到下面#ifdef NUM的時候,當然執行第一個printf。否則第二個printf將被執行。
我認爲,用這種,可以很方便的開啓/關閉整個程序的某項特定功能。
2:情況2:
#ifndef _XXXX
...程序段1...
#else
...程序段2...
#endif
這裏使用了#ifndef,表示的是if not def。當然是和#ifdef相反的狀況(如果沒有定義了標識符_XXXX,那麼執行程序段1,否則執行程序段2)。例子就不舉了。
3:情況3:
#if 常量
...程序段1...
#else
...程序段2...
#endif
這裏表示,如果常量爲真(非0,隨便什麼數字,只要不是0),就執行程序段1,否則執行程序段2。
我認爲,這種方法可以將測試代碼加進來。當需要開啓測試的時候,只要將常量變1就好了。而不要測試的時候,只要將常量變0。