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