編譯C程序有很多步驟,其中第一步為預(yù)處理(preprocessing)階段
一.前言
1.編譯一個(gè)C程序涉及很多步驟。其中第一步驟稱為預(yù)處理(preprocessing)階段。C預(yù)處理器(preprocessor)在源代碼編譯之前對(duì)其進(jìn)行文本性質(zhì)的操作。
2.它的主要任務(wù)包括刪除注釋、插入被#include指令包含的內(nèi)容、定義和替換由#define指令定義的符號(hào)以及確定代碼的部分內(nèi)容是否應(yīng)該根據(jù)一些條件編譯指令經(jīng)行編譯。
二.預(yù)定義符號(hào)
1.以下為預(yù)處理器定義的符號(hào)。它們的值或者是字符串常量,或者是十進(jìn)制數(shù)字常量。
2.__FILE__和__LINE__在確認(rèn)調(diào)試輸出時(shí)很有用。__DATE__和__TIME__常常用于在被編譯的程序中加入版本信息。
3.__STDC__用于那些在ANSI環(huán)境和非ANSI環(huán)境都必須進(jìn)行編譯的程序中結(jié)合條件編譯。
注:
此處的前綴是兩個(gè)下劃線.
² __FILE__:用%s進(jìn)行輸出,輸出結(jié)果為源程序名。
² __LINE__:用%d進(jìn)行輸出,輸出結(jié)果為文件當(dāng)前行號(hào)。
² __DATE__:用%s進(jìn)行輸出,輸出結(jié)果為文件被編譯的日期
² __STDC__:用%d進(jìn)行輸出,如果編譯器遵循ANSIC,其數(shù)值為1。否則未定義。
三.#define
1.#define的用法:
#define name stuff
有了這條指令以后,每當(dāng)有符號(hào)name出現(xiàn)在這條指令后面時(shí),預(yù)處理器就會(huì)把它替換成stuff。
2.替換文本并不僅限于數(shù)值字面值常量。使用#define指令,可以把文本替換到程序中。
3.如果定義中的stuff非常長(zhǎng),可以將其分成幾行,除了最后一行之外,每行的末尾都要加一個(gè)反斜杠。
Eg:
#define DEBUG_PRINT printf(“File %s line%d:” \
”x=%d,y=%d,z=%d”,\
__FILE__,__LINE__,\
x,y,z)
說明:此處利用了相鄰的字符串常量被自動(dòng)連接為一個(gè)字符串的這個(gè)特性。
4.在宏定義的末尾不要加上分號(hào)。如果加了則會(huì)出現(xiàn)一條空語(yǔ)句。
Eg:
DEBUG_PRINT;
此時(shí),編譯器替換后會(huì)都一條空語(yǔ)句.
1>有時(shí)候只允許出現(xiàn)一條語(yǔ)句,如果放入兩條語(yǔ)句就會(huì)出現(xiàn)問題
Eg:
if(…)
DEBUG_PRINT;
else
…..
四.宏
1.#define機(jī)制包括了一個(gè)規(guī)定,只允許把參數(shù)替換到文本中,這種實(shí)現(xiàn)通常稱為宏或定義宏(defined macro)
2.宏的聲明方式:
#define name(parament-list) stuff
1>其中,parament-list(參數(shù)列表)是一個(gè)由逗號(hào)分隔的符號(hào)列表,它們可能出現(xiàn)在stuff中。參數(shù)列表的左括號(hào)必須與name緊鄰。如果兩者之間有任何空白存在,參數(shù)列表就會(huì)解釋為stuff的一部分。
2>當(dāng)宏被調(diào)用時(shí),名字后面是一個(gè)由逗號(hào)分隔的列表,每個(gè)值都與宏定義中的一個(gè)參數(shù)相對(duì)應(yīng),整個(gè)列表用一對(duì)括號(hào)包圍。但參數(shù)出現(xiàn)在程序中時(shí),與每個(gè)參數(shù)對(duì)應(yīng)的實(shí)際值都將被替換到stuff中。
eg:
#define SQUARE(x) ( (x)*(x))
如果在上述聲明之后,調(diào)用
SQUARE(5)
預(yù)處理器就會(huì)用用下面這個(gè)表達(dá)式進(jìn)行替換:
5*5。
說明:
在完整定義的參數(shù)宏中要加上括號(hào),并且對(duì)宏定義中每個(gè)參數(shù)的兩邊也加上括號(hào)
3.#define替換
在程序中擴(kuò)展#define定義符號(hào)和宏時(shí),需要涉及幾個(gè)步驟
1>在調(diào)用宏時(shí),首先對(duì)參數(shù)進(jìn)行檢查,看看是否包含了任何由#define定義的符號(hào)。如果是,它們首先被替換
2>替換文本隨后被插入到程序中原來文本的位置。對(duì)于宏,參數(shù)名被它們的值所替代
3>最后,再次對(duì)結(jié)果文本進(jìn)行掃描,看看它是否包含了任何由#define定義的符號(hào)。如果是,就崇光伏上述處理過程。
因此,宏參數(shù)和#define定義可以包含其他#define定義的符號(hào)。但是宏不可以出現(xiàn)遞歸。
說明:
1.當(dāng)預(yù)處理器搜索#define定義的符號(hào)時(shí),字符串常量的內(nèi)容并不進(jìn)行檢查。如果想要把宏參數(shù)插入到字符串常量中,可以使用如下方法:
1>使用鄰近字符串自動(dòng)連接的特性,把一個(gè)字符串分成幾段,每段實(shí)際上都是一個(gè)宏參數(shù)。
eg:
#include
#define PRINT(FORMAT,VALUE) \
printf(“thevalue is “ FORMAT “\n”,VALUE)
int main()
{
int x= 12;
PRINT(“%d”,x+3);
}
說明:
此技巧只有字符串常量作為宏參數(shù)給出時(shí)才能使用。
2>第二個(gè)技巧使用預(yù)處理器把一個(gè)宏參數(shù)轉(zhuǎn)換為一個(gè)字符串。#argument這種結(jié)構(gòu)被預(yù)處理器翻譯為”argument”.
eg:
#define PRINT(FORMAT,VALUE) \
printf(“thevalue of #VALUE \
“ is “ FORMAT “\n”,VALUE)
int main()
{
int x= 12;
PRINT(“%d”,x+3);
}
輸出結(jié)果為:
the value of x+3 is 15
3>## 結(jié)構(gòu)則執(zhí)行一種不同的任務(wù)。它把位于它兩邊的符號(hào)連接成一個(gè)符號(hào)。作為用途之一,它允許宏定義從分離的文本片段創(chuàng)建標(biāo)識(shí)符。
下面的實(shí)例使用這種連接把一個(gè)值添加到幾個(gè)變量之一:
#define ADD_TO_SUM(sum_number,value) \
sum ## sum_number += value
….
ADD_TO_SUM(5,25);
最后一條語(yǔ)句把值25加到變量sum5上。注意這種連接必須產(chǎn)生一個(gè)合法的標(biāo)識(shí)符。否則,其結(jié)果就是未定的。
五.宏與函數(shù)
1.宏非常頻繁地用于執(zhí)行簡(jiǎn)單的計(jì)算,比如在兩個(gè)表達(dá)式中尋找其中較大或較小的一個(gè):
可以用:
#define MAX(a,b) ((a) > (b) ? (a) : (b) )
2此處不用函數(shù)的原因是:
1>首先用于調(diào)用和從函數(shù)返回的代碼很可能比實(shí)際執(zhí)行這個(gè)小型計(jì)算工作的代碼更大
2>函數(shù)的參數(shù)必須聲明為一種特定的類型,所以它只能在類型合適的表達(dá)式上使用。因此,上面的宏可以用于整型、長(zhǎng)整型、單浮點(diǎn)型、雙浮點(diǎn)型以及其他類型中。既:宏是與類型無(wú)關(guān)的。
3>使用宏的不好之處在于,一份宏定義代碼的拷貝都將插入到程序中。除非宏非常短,否則使用宏可能會(huì)大幅度增加程序的長(zhǎng)度。
4>還有一些任務(wù)根本無(wú)法用函數(shù)實(shí)現(xiàn)
Eg:#define MALLOC(n,type) \
((type *)malloc( (n)*sizeof(type) ) )
此宏中的第二個(gè)參數(shù)是一種類型,它無(wú)法作為函數(shù)參數(shù)進(jìn)行傳遞。
5>宏參數(shù)具有副作用。
3.宏與函數(shù)的區(qū)別
1>代碼長(zhǎng)度:
² #define宏:每次使用時(shí),宏代碼都被插入到程序中。除了非常小的宏之外,程序的長(zhǎng)度將大幅度增加
² 函數(shù):函數(shù)代碼只出現(xiàn)于一個(gè)地方;每次使用這個(gè)函數(shù)時(shí),都調(diào)用那個(gè)地方的同一份代碼
2>執(zhí)行速度:
² #define宏:更塊
² 函數(shù): 存在函數(shù)調(diào)用/返回的額外開銷
3>操作符優(yōu)先級(jí)
² #define宏參數(shù)的求值是在所有周圍表達(dá)式的上下文環(huán)境里,除非它們加上括號(hào),否則鄰近操作符的優(yōu)先級(jí)可能會(huì)產(chǎn)生不可預(yù)料的結(jié)果
² 函數(shù):函數(shù)參數(shù)只在函數(shù)調(diào)用時(shí)求值一次,它的結(jié)果傳遞給函數(shù)。表達(dá)式的求值結(jié)果更容易預(yù)測(cè)。
4>參數(shù)求值
² #define宏:參數(shù)每次用于宏定義時(shí),它們都將重新求值。由于多次求值,具有副作用的參數(shù)可能會(huì)產(chǎn)生不可預(yù)料的結(jié)果。
² 函數(shù):參數(shù)在函數(shù)被調(diào)用前只求值一次。在函數(shù)中多次使用參數(shù)并不會(huì)導(dǎo)致多種求值過程。參數(shù)的副作用并不會(huì)造成任何特殊的問題。
5>參數(shù)類型
² #define宏:宏與類型無(wú)關(guān)。只要對(duì)參數(shù)的操作是合法的,它可以適用于任何參數(shù)類型
² 函數(shù):函數(shù)的參數(shù)是與類型有關(guān)的。如果參數(shù)的類型不同,就需要使用不同的函數(shù),即使它們執(zhí)行的任務(wù)是相同的。
六.#undef
1.該預(yù)處理指令用于移除一個(gè)宏定義
#undef name
2.如果一個(gè)現(xiàn)存的名字需要被重新定義,那么它的舊定義首先必須用#undef移除。
七.命令行定義
1.許多C編譯器都可以實(shí)現(xiàn):允許在命令行中定義符號(hào),用于啟動(dòng)編譯過程。當(dāng)同一個(gè)源文件被編譯成一個(gè)程序的不同版本時(shí),該特性很有用。
Eg:假定某個(gè)程序聲明了一個(gè)某種長(zhǎng)度的數(shù)組。如果某個(gè)機(jī)器的內(nèi)存很有限,這個(gè)數(shù)組必須很小,但在另一個(gè)內(nèi)存很多的機(jī)器上,可能希望數(shù)組能夠大寫。
1>定義數(shù)組為:
Int array[ARRAY_SIZE];
那么我們希望ARRY_SIZE在命令行中定義。
例如:
gcc -DARRY_SIZE=100 tiger.c。
即可實(shí)現(xiàn)在命令行中指定數(shù)組的大小為100。
2.在Linux編譯器中,使用-D選項(xiàng)來完成該功能。
可以用兩種方式使用該選項(xiàng):
Ø -Dname
Ø -Dname=stuff
說明
1>此處的name即為程序中的標(biāo)量名。
2>第一種形式定義了符號(hào)name,它的數(shù)值為。也可以用于條件編譯中
3>第二中形式把該符號(hào)的值定義為等號(hào)后面的stuff。
3.提供符號(hào)命令行定義的編輯器通常也提供在命令行中去除符號(hào)的定義。在Linux編譯器上,-U選項(xiàng)用于執(zhí)行這項(xiàng)任務(wù)。指定-Uname將導(dǎo)致程序中符號(hào)name的定義被忽略。當(dāng)它與條件編譯結(jié)合使用時(shí),該特性很有用。
八.條件編譯
1.在編譯一個(gè)程序時(shí),如果可以選擇某條語(yǔ)句或某組語(yǔ)句進(jìn)行翻譯或者被忽略,常常顯得很方便。用于調(diào)試程序的語(yǔ)句就是一個(gè)明顯的例子。它門不應(yīng)該出現(xiàn)在程序的產(chǎn)品版本中,但是,如果以后做一些維護(hù)性修改時(shí),又可能需要重新調(diào)試該語(yǔ)句。因此就需要條件編譯。
2.條件編譯(conditional compilation)用于實(shí)現(xiàn)該目的。使用條件編譯,可以選擇代碼的一部分是被正常編譯還是完全忽略。
3.用于支持條件編譯的基本結(jié)構(gòu)是#if指令和其匹配的#endif指令。
#if constant-expression
statements
#endif
1>其中constant-expression(常量表達(dá)式)由預(yù)處理器進(jìn)行求值。如果它的值是非0值(真),那么statements部分被正常編譯,否則預(yù)處理器就安靜地刪除它們。
2>所謂常量表達(dá)式,就是說它或者是字面值常量,或者是一個(gè)由#define定義的符號(hào)。如果變量在執(zhí)行期間無(wú)法獲得它們的值,那么它們?nèi)绻霈F(xiàn)在常量表達(dá)式中就是一非法的。因?yàn)樗鼈兊臄?shù)值在編譯時(shí)是不可預(yù)測(cè)的。
Eg:
Ø 將所有的調(diào)試代碼都以下面的形式出現(xiàn)
#if DEBUG
printf(“x=%d ,y=%d\n”,x,y);
#endif
1>如果我們想編譯這個(gè)代碼,可以用下面的代碼實(shí)現(xiàn)
#define DEBUG 1
2>如果想忽略,則只要把這個(gè)符號(hào)定義為0就可以了。
Ø 條件編譯的另一個(gè)用途是在編譯時(shí)選擇不同的代碼部分。為了支持該功能,#if指令還具有可選的#elif和#else字句。
1>語(yǔ)法功能是:
#if constant-expression
statements
#elif constant-expriession
other statements ….
#else
other statements
#endif
#elif字句出現(xiàn)的次數(shù)可以不限。每個(gè)constant-expression(常量表達(dá)式)只有當(dāng)前面所有常量表達(dá)式的值都為假時(shí)才會(huì)被編譯。#else子句中的語(yǔ)句只有當(dāng)前面所有的常量表達(dá)式值都為假時(shí)才會(huì)被編譯,在其他情況下它都會(huì)被編譯。
4.是否被定義
1>測(cè)試一個(gè)符號(hào)石佛已經(jīng)被定義是可能的。在條件編譯中完成這個(gè)任務(wù)往往更為方便,因?yàn)槌绦蛉绻⒉恍枰刂凭幾g的符號(hào)所控制的特性,它就不需要被定義。
Eg:
if defined(symbol)
#ifdef symbol
九.文件包含
1.函數(shù)庫(kù)文件包含兩種不同類型的#include文件包含:函數(shù)庫(kù)文件和本地文件。
1>函數(shù)庫(kù)頭文件
Ø 函數(shù)庫(kù)頭文件使用的語(yǔ)法
#include
Ø 對(duì)于fiename,并不存在任何限制,標(biāo)準(zhǔn)庫(kù)文件以一個(gè).h后綴結(jié)尾。編譯器在標(biāo)準(zhǔn)位置處查找函數(shù)頭文件
Ø 編譯器通過觀察由編譯器定義的“一系列標(biāo)準(zhǔn)位置”查找函數(shù)庫(kù)頭文件。在編譯器的文檔中應(yīng)該說明這些位置是什么,以及怎樣修改它們或者在列表中添加其他位置。
Ø Eg:在Linux系統(tǒng)上的C編譯器在/user/include目錄查找函數(shù)庫(kù)頭文件,該編譯器有一個(gè)命令行選項(xiàng),允許把其他目錄添加到這個(gè)列表中,這樣就可以創(chuàng)建自己的頭文件函數(shù)庫(kù)。
2>本地文件包含
Ø 語(yǔ)法格式:
#include “filename”
Ø 標(biāo)準(zhǔn)允許編譯器自行決定是否把本地形式的#include和函數(shù)庫(kù)形式的#include區(qū)別對(duì)待。
Ø 可以對(duì)本地頭文件先使用一種特殊的處理方式,如果失敗,編譯器再按照函數(shù)庫(kù)頭文件的處理方式對(duì)它們進(jìn)行處理。
Ø 處理本地頭文件的一種常見策略就是在源文件所在的當(dāng)前目錄進(jìn)行查找,如果該頭文件并未找到,編譯器就像查找函數(shù)庫(kù)頭文件一樣在標(biāo)準(zhǔn)位置查找本地頭文件。
Ø 可以在所有的#include語(yǔ)句中使用雙括號(hào)而不是尖括號(hào)。但是,使用這種方法,有些編譯器在查找函數(shù)庫(kù)頭文件時(shí)可能會(huì)浪費(fèi)時(shí)間。
2.對(duì)函數(shù)庫(kù)頭文件使用尖括號(hào)的另一個(gè)較好的理由是可以給人們提示為函數(shù)頭文件而不是本地文件。
3.UNIX系統(tǒng)和Borland C編譯器也支持使用絕對(duì)路徑名(absolute pathname),它不僅指定文件的名字,而且指定了文件的位置。
1>UNIX系統(tǒng)中的絕對(duì)路徑名是以一個(gè)斜杠頭開頭,如下所示:
Eg:/home/fred/c/my_proj/declaration2.h
2>在MS-DOS系統(tǒng)中,它所使用的是反斜杠而不是斜杠。
3>如果一個(gè)絕對(duì)路徑名出現(xiàn)在任何一種形式的#include,那么正常的目錄查找就被跳過,因?yàn)檫@個(gè)路徑名指定了頭文件的位置。
4.嵌套文件包含
1>嵌套#include文件的一個(gè)不利之處在于它使得很難判斷源文件之間的真正依賴關(guān)系。
2>嵌套#include文件的另一個(gè)不利之處在于一個(gè)頭文件可能會(huì)被多次包含。
3>多重包含在絕大多數(shù)情況下出現(xiàn)于大型程序中,它往往需要使用很多頭文件,因此要發(fā)現(xiàn)這種情況并不容易。要解決這個(gè)問題,可以使用條件編譯,這樣編寫:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H 1
/*
**All the stuff thatyou want in the header file
*/
#endif
那么,多重包含的危險(xiǎn)就被消除了。當(dāng)頭文件第1次被包含時(shí),它被正常處理,符號(hào)_HEADERNAME_H 被定義為1。如果頭文件被再次包含,通過條件編譯,它的所有內(nèi)容被忽略。符號(hào)_HEADERNAME_H 按照被包含文件的文件名進(jìn)行取名,以避免由于頭文件使用相同的符號(hào)而引起的沖突。
說明:
前面的例子也可以改為
#define _HEADERNAME_H
使用該條語(yǔ)句,與前面的#define _HEADNAME_H 1效果是等同的。
說明:
1.當(dāng)頭文件被包含時(shí),位于頭文件內(nèi)所有內(nèi)容都要被編譯。因此,每個(gè)頭文件只應(yīng)該包含一組函數(shù)或數(shù)據(jù)的聲明。
2.使用幾個(gè)頭文件,每個(gè)頭文件包含用于某個(gè)特定函數(shù)或模塊的聲明的做法會(huì)更好一些。
3.只把必要的聲明包含于一個(gè)文件中,這樣文件中的語(yǔ)句就不會(huì)意外的訪問應(yīng)該屬于私有的函數(shù)或變量。
總結(jié):
1.#argument結(jié)構(gòu)由預(yù)處理器轉(zhuǎn)換為字符串常量”argument”.
2.##操作符用于把它兩邊的文本粘切成同一個(gè)標(biāo)識(shí)符。
3.有些任何既可以用宏也可以用函數(shù)實(shí)現(xiàn)。但是宏與類型無(wú)關(guān)。宏的執(zhí)行速度快于函數(shù),因?yàn)樗嬖诤瘮?shù)調(diào)用/返回的開銷。但是,使用宏通常會(huì)增加程序的長(zhǎng)度,但函數(shù)確不會(huì)。
4.#include指令用于實(shí)現(xiàn)文件包含。它具有兩種形式。
Ø 如果文件名位于一對(duì)尖括號(hào)中,編譯器將在由編譯器定義的標(biāo)準(zhǔn)位置查找這個(gè)文件,這種形式通常用于包含函數(shù)庫(kù)頭文件。
Ø 另一種形式,文件名出現(xiàn)在一對(duì)雙括號(hào)內(nèi)。不同的編譯器可以用不同的方式處理這種形式。但是,如果用于處理本地頭文件的任何特殊處理方式無(wú)法找到這個(gè)頭文件,那么編譯器接下來就使用標(biāo)準(zhǔn)查找過程來尋找它。這種形式通常用于包含自己編寫的頭文件。
5.文件包含可以嵌套,但很少需要進(jìn)行超過一層或兩層的文件包含嵌套。嵌套的包含文件將會(huì)增加多次包含同一個(gè)文件的危險(xiǎn),而且很難以確定某個(gè)特定的源文件依賴的究竟是那個(gè)頭文件。
6.不要在一個(gè)宏定義的末尾加上分號(hào),使其成為一條完整的語(yǔ)句。
7.頭文件只應(yīng)該包含一組函數(shù)函數(shù)和(或)數(shù)據(jù)的聲明
8.把不同集合的聲明分離到不同的頭文件中可以改善信息隱蔽