|
一.前言 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è)下劃線. 2 __FILE__:用%s進(jìn)行輸出,輸出結(jié)果為源程序名。 2 __LINE__:用%d進(jìn)行輸出,輸出結(jié)果為文件當(dāng)前行號(hào)。 2 __DATE__:用%s進(jìn)行輸出,輸出結(jié)果為文件被編譯的日期 2 __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非常長,可以將其分成幾行,除了最后一行之外,每行的末尾都要加一個(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)一條空語句。 Eg: DEBUG_PRINT; 此時(shí),編譯器替換后會(huì)都一條空語句. 1>有時(shí)候只允許出現(xiàn)一條語句,如果放入兩條語句就會(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); 最后一條語句把值25加到變量sum5上。注意這種連接必須產(chǎn)生一個(gè)合法的標(biāo)識(shí)符。否則,其結(jié)果就是未定的。 五.宏與函數(shù) 1.宏非常頻繁地用于執(zhí)行簡單的計(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á)式上使用。因此,上面的宏可以用于整型、長整型、單浮點(diǎn)型、雙浮點(diǎn)型以及其他類型中。既:宏是與類型無關(guān)的。 3>使用宏的不好之處在于,一份宏定義代碼的拷貝都將插入到程序中。除非宏非常短,否則使用宏可能會(huì)大幅度增加程序的長度。 4>還有一些任務(wù)根本無法用函數(shù)實(shí)現(xiàn) Eg:#define MALLOC(n,type) \\ ((type *)malloc( (n)*sizeof(type) ) ) 此宏中的第二個(gè)參數(shù)是一種類型,它無法作為函數(shù)參數(shù)進(jìn)行傳遞。 5>宏參數(shù)具有副作用。 3.宏與函數(shù)的區(qū)別 1>代碼長度: 2 #define宏:每次使用時(shí),宏代碼都被插入到程序中。除了非常小的宏之外,程序的長度將大幅度增加 2 函數(shù):函數(shù)代碼只出現(xiàn)于一個(gè)地方;每次使用這個(gè)函數(shù)時(shí),都調(diào)用那個(gè)地方的同一份代碼 2>執(zhí)行速度: 2 #define宏:更塊 2 函數(shù): 存在函數(shù)調(diào)用/返回的額外開銷 3>操作符優(yōu)先級(jí) 2 #define宏參數(shù)的求值是在所有周圍表達(dá)式的上下文環(huán)境里,除非它們加上括號(hào),否則鄰近操作符的優(yōu)先級(jí)可能會(huì)產(chǎn)生不可預(yù)料的結(jié)果 2 函數(shù):函數(shù)參數(shù)只在函數(shù)調(diào)用時(shí)求值一次,它的結(jié)果傳遞給函數(shù)。表達(dá)式的求值結(jié)果更容易預(yù)測。 4>參數(shù)求值 2 #define宏:參數(shù)每次用于宏定義時(shí),它們都將重新求值。由于多次求值,具有副作用的參數(shù)可能會(huì)產(chǎn)生不可預(yù)料的結(jié)果。 2 函數(shù):參數(shù)在函數(shù)被調(diào)用前只求值一次。在函數(shù)中多次使用參數(shù)并不會(huì)導(dǎo)致多種求值過程。參數(shù)的副作用并不會(huì)造成任何特殊的問題。 5>參數(shù)類型 2 #define宏:宏與類型無關(guān)。只要對(duì)參數(shù)的操作是合法的,它可以適用于任何參數(shù)類型 2 函數(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è)某種長度的數(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í),如果可以選擇某條語句或某組語句進(jìn)行翻譯或者被忽略,常常顯得很方便。用于調(diào)試程序的語句就是一個(gè)明顯的例子。它門不應(yīng)該出現(xiàn)在程序的產(chǎn)品版本中,但是,如果以后做一些維護(hù)性修改時(shí),又可能需要重新調(diào)試該語句。因此就需要條件編譯。 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í)行期間無法獲得它們的值,那么它們?nèi)绻霈F(xiàn)在常量表達(dá)式中就是一非法的。因?yàn)樗鼈兊臄?shù)值在編譯時(shí)是不可預(yù)測的。 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>語法功能是: #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子句中的語句只有當(dāng)前面所有的常量表達(dá)式值都為假時(shí)才會(huì)被編譯,在其他情況下它都會(huì)被編譯。 4.是否被定義 1>測試一個(gè)符號(hào)石佛已經(jīng)被定義是可能的。在條件編譯中完成這個(gè)任務(wù)往往更為方便,因?yàn)槌绦蛉绻⒉恍枰刂凭幾g的符號(hào)所控制的特性,它就不需要被定義。 Eg: if defined(symbol) #ifdef symbol 九.文件包含 1.函數(shù)庫文件包含兩種不同類型的#include文件包含:函數(shù)庫文件和本地文件。 1>函數(shù)庫頭文件 ? 函數(shù)庫頭文件使用的語法 #include ? 對(duì)于fiename,并不存在任何限制,標(biāo)準(zhǔn)庫文件以一個(gè).h后綴結(jié)尾。編譯器在標(biāo)準(zhǔn)位置處查找函數(shù)頭文件 ? 編譯器通過觀察由編譯器定義的“一系列標(biāo)準(zhǔn)位置”查找函數(shù)庫頭文件。在編譯器的文檔中應(yīng)該說明這些位置是什么,以及怎樣修改它們或者在列表中添加其他位置。 ? Eg:在Linux系統(tǒng)上的C編譯器在/user/include目錄查找函數(shù)庫頭文件,該編譯器有一個(gè)命令行選項(xiàng),允許把其他目錄添加到這個(gè)列表中,這樣就可以創(chuàng)建自己的頭文件函數(shù)庫。 2>本地文件包含 ? 語法格式: #include “filename” ? 標(biāo)準(zhǔn)允許編譯器自行決定是否把本地形式的#include和函數(shù)庫形式的#include區(qū)別對(duì)待。 ? 可以對(duì)本地頭文件先使用一種特殊的處理方式,如果失敗,編譯器再按照函數(shù)庫頭文件的處理方式對(duì)它們進(jìn)行處理。 ? 處理本地頭文件的一種常見策略就是在源文件所在的當(dāng)前目錄進(jìn)行查找,如果該頭文件并未找到,編譯器就像查找函數(shù)庫頭文件一樣在標(biāo)準(zhǔn)位置查找本地頭文件。 ? 可以在所有的#include語句中使用雙括號(hào)而不是尖括號(hào)。但是,使用這種方法,有些編譯器在查找函數(shù)庫頭文件時(shí)可能會(huì)浪費(fèi)時(shí)間。 2.對(duì)函數(shù)庫頭文件使用尖括號(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 使用該條語句,與前面的#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è)文件中,這樣文件中的語句就不會(huì)意外的訪問應(yīng)該屬于私有的函數(shù)或變量。 總結(jié): 1.#argument結(jié)構(gòu)由預(yù)處理器轉(zhuǎn)換為字符串常量”argument”. 2.##操作符用于把它兩邊的文本粘切成同一個(gè)標(biāo)識(shí)符。 3.有些任何既可以用宏也可以用函數(shù)實(shí)現(xiàn)。但是宏與類型無關(guān)。宏的執(zhí)行速度快于函數(shù),因?yàn)樗嬖诤瘮?shù)調(diào)用/返回的開銷。但是,使用宏通常會(huì)增加程序的長度,但函數(shù)確不會(huì)。 4.#include指令用于實(shí)現(xiàn)文件包含。它具有兩種形式。 ? 如果文件名位于一對(duì)尖括號(hào)中,編譯器將在由編譯器定義的標(biāo)準(zhǔn)位置查找這個(gè)文件,這種形式通常用于包含函數(shù)庫頭文件。 ? 另一種形式,文件名出現(xiàn)在一對(duì)雙括號(hào)內(nèi)。不同的編譯器可以用不同的方式處理這種形式。但是,如果用于處理本地頭文件的任何特殊處理方式無法找到這個(gè)頭文件,那么編譯器接下來就使用標(biāo)準(zhǔn)查找過程來尋找它。這種形式通常用于包含自己編寫的頭文件。 5.文件包含可以嵌套,但很少需要進(jìn)行超過一層或兩層的文件包含嵌套。嵌套的包含文件將會(huì)增加多次包含同一個(gè)文件的危險(xiǎn),而且很難以確定某個(gè)特定的源文件依賴的究竟是那個(gè)頭文件。 6.不要在一個(gè)宏定義的末尾加上分號(hào),使其成為一條完整的語句。 7.頭文件只應(yīng)該包含一組函數(shù)函數(shù)和(或)數(shù)據(jù)的聲明 8.把不同集合的聲明分離到不同的頭文件中可以改善信息隱蔽 |
|
|