在 C 語言中,可以採用命令 #define 來定義宏。該命令允許把一個名稱指定成任何所需的文字,例如一個常數值或者一條語句。在定義了宏之後,無論宏名稱出現在原始碼的何處,前處理器都會把它用定義時指定的文字替換掉。
關於宏的一個常見應用就是,用它定義數值常數的名稱:
#define ARRAY_SIZE 100
double data[ARRAY_SIZE];
這兩行程式碼為值 100 定義了一個宏名稱 ARRAY_SIZE,並且在陣列 data 的定義中使用了該宏。
慣例將宏名稱每個字母採用大寫,這有助於區分宏與一般的變數。上述簡單的範例也展示了宏是怎樣讓 C 程式更有彈性的。
通常情況下,程式中往往多次用到陣列(例如上述 data)的長度,例如,採用陣列元素來控制 for 迴圈遍歷次數。當每次用到陣列長度時,用宏名稱而不要直接用數位,如果程式的維護者需要修改陣列長度,只需要修改宏的定義即可,即 #define 命令,而不需要修改程式中每次用到每個陣列長度的地方。
在翻譯的第三個步驟中,前處理器會分析原始檔,把它們轉換為前處理器記號和空白符。如果遇到的記號是宏名稱,前處理器就會展開(expand)該宏;也就是說,會用定義的文字來取代宏名稱。
出現在字串字面量中的宏名稱不會被展開,因為整個字串字麵量算作一個前處理器記號。
無法通過宏展開的方式建立前處理器命令。即使宏的展開結果會生成形式上有效的命令,但前處理器不會執行它。
在宏定義時,可以有引數,也可以沒有引數。
沒有引數的宏
沒有引數的宏定義,採用如下形式:
#define 宏名稱 替換文字
“替換文字”前面和後面的空格符不屬於替換文字中的內容。
替代文字本身也可以為空。下面是一些範例:
#define TITLE "*** Examples of Macros Without Parameters ***"
#define BUFFER_SIZE (4 * 512)
#define RANDOM (-1.0 + 2.0*(double)rand() / RAND_MAX)
標準函數 rand()返回一個偽隨機整數,範圍在 [0,RAND_MAX] 之間。rand()的原型和 RAND_MAX 宏都定義在標準庫標頭檔案 stdlib.h 中。
下面的語句展示了上述宏的一種可能使用方式:
#include <stdio.h>
#include <stdlib.h>
/* ... */
// 顯示標題
puts( TITLE );
// 將流fp設定成“fully buffered”模式,其具有一個緩衝區,
// 緩衝區大小為BUFFER_SIZE個位元組
// 宏_IOFBF在stdio.h中定義為0
static char myBuffer[BUFFER_SIZE];
setvbuf( fp, myBuffer, _IOFBF, BUFFER_SIZE );
// 用ARRAY_SIZE個[-10.0, +10.0]區間內的亂數值填充陣列data
for ( int i = 0; i < ARRAY_SIZE; ++i )
data[i] = 10.0 * RANDOM;
用替換文字取代宏,前處理器生成下面的語句:
puts( "*** Examples of Macros Without Parameters ***" );
static char myBuffer[(4 * 512)];
setvbuf( fp, myBuffer, 0, (4 * 512) );
for ( int i = 0; i < 100; ++i )
data[i] = 10.0 * (-1.0 + 2.0*(double)rand() / 2147483647);
在上例中,該實現版本中的 RAND_MAX 宏值是 2147483647。如果採用其他的編譯器,RAND_MAX 的值可能會不一樣。
如果編寫的宏中包含了一個有運算元的表示式,應該把表示式放在圓括號內,以避免使用該宏時受運算子優先順序的影響,進而產生意料之外的結果。例如,RANDOM 宏最外側的括號可以確保 10.0*RANDOM 表示式產生想要的結果。如果沒有這個括號,宏展開後的表示式變成:
10.0 * -1.0 + 2.0*(double)rand() / 2147483647
這個表示式生成的亂數值範圍在 [-10.0,-8.0] 之間。
帶引數的宏
你可以定義具有形式引數(簡稱“形參”)的宏。當前處理器展開這類宏時,它先使用呼叫宏時指定的實際引數(簡稱“實參”)取代替換文字中對應的形參。
帶有形參的宏通常也稱為類函數宏(function-like macro)。
可以使用下面兩種方式定義帶有引數的宏:
#define 宏名稱( [形參列表] ) 替換文字
#define 宏名稱( [形參列表 ,] ... ) 替換文字
“形參列表”是用逗號隔開的多個識別符號,它們都作為宏的形參。當使用這類宏時,實參列表中的實引數量必須與宏定義中的形引數量一樣多(然而,C99 允許使用“空實參”,下面會進一步解釋)。這裡的省略號意味著一個或更多的額外形參。
當定義一個宏時,必須確保宏名稱與左括號之間沒有空白符。如果在名稱後面有任何空白,那麼命令就會把宏作為沒有引數的宏,且從左括號開始採用替換文字。
常見的兩個函數 getchar()和 putchar(),它們的宏定義在標準庫標頭檔案 stdio.h 中。它們的展開值會隨著實現版本不同而有所不同,但不論何種版本,它們的定義總是類似於以下形式:
#define getchar() getc(stdin)
#define putchar(x) putc(x, stdout)
當“呼叫”一個類函數宏時,前處理器會用呼叫時的實參取代替換文字中的形參。C99 允許在呼叫宏的時候,宏的實參列表可以為空。在這種情況下,對應的替換文字中的形參不會被取代;也就是說,替換文字會刪除該形參。然而,並非所有的編譯器都支援這種“空實參”的做法。
如果呼叫時的實參也包含宏,在正常情況下會先對它進行展開,然後才把該實參取代替換文字中的形參。對於替換文字中的形參是 # 或 ## 運算子運算元的情況,處理方式會有所不同。下面是類函數宏及其展開結果的一些範例:
#include <stdio.h> // 包含putchar()的定義
#define DELIMITER ':'
#define SUB(a,b) (a-b)
putchar( DELIMITER );
putchar( str[i] );
int var = SUB( ,10);
如果 putchar(x)定義為 putc(x,stdout),前處理器會按如下方式展開最後三行程式碼:
putc(':', stdout);
putc(str[i], stdout);
int var = (-10);
如下例所示,
替換文字中所有出現的形參,應該使用括號將其包圍。這樣可以確保無論實參是否是表示式,都能正確地被計算:
#define DISTANCE( x, y ) ((x)>=(y) ? (x)-(y) : (y)-(x))
d = DISTANCE( a, b+0.5 );
該宏呼叫展開如下所示:
d = ((a)>=(b+0.5) ? (a)-(b+0.5) : (b+0.5)-(a));
如果 x 與 y 沒有採用括號,那麼擴充套件後將出現表示式 a-b+0.5,而不是表示式(a)-(b+0.5),這與期望的運算不同。
可選引數
C99 標準允許定義有省略號的宏,省略號必須放在參數列的後面,以表示可選引數。你可以用可選引數來呼叫這類宏。
當呼叫有可選引數的宏時,前處理器會將所有可選引數連同分隔它們的逗號打包在一起作為一個引數。在替換文字中,識別符號 __VA_ARGS__ 對應一組前述打包的可選引數。
識別符號 __VA_ARGS__ 只能用在宏定義時的替換文字中。
__VA_ARGS__ 的行為和其他宏引數一樣,唯一不同的是,它會被呼叫時所用的參數列中剩下的所有引數取代,而不是僅僅被一個引數取代。下面是一個可選引數宏的範例:
// 假設我們有一個已開啟的紀錄檔檔案,準備採用檔案指標fp_log對其進行寫入
#define printLog(...) fprintf( fp_log, __VA_ARGS__ )
// 使用宏printLog
printLog( "%s: intVar = %dn", __func__, intVar );
前處理器把最後一行的宏呼叫替換成下面的一行程式碼:
fprintf( fp_log, "%s: intVar = %dn", __func__, intVar );
預定義的識別符號 __func__ 可以在任一函數中使用,該識別符號是表示當前函數名的字串。因此,該範例中的宏呼叫會將當前函數名和變數 intVar 的內容寫入紀錄檔檔案。
字串化運算子
一元運算子 # 常稱為
字串化運算子(stringify operator 或 stringizing operator),因為它會把宏呼叫時的實參轉換為字串。
# 的運算元必須是宏替換文字中的形參。當形參名稱出現在替換文字中,並且具有字首 # 字元時,前處理器會把與該形參對應的實參放到一對雙引號中,形成一個字串字面量。
實參中的所有字元本身維持不變,但下面幾種情況是例外:
(1) 在實參各記號之間如果存在有空白符序列,都會被替換成一個空格符。
(2) 實參中每個雙引號(")的前面都會新增一個反斜線()。
(3) 實參中字元常數、字串字面量中的每個反斜線前面,也會新增一個反斜線。但如果該反斜線本身就是通用字元名的一部分,則不會再在其前面新增反斜線。
下面的範例展示了如何使用#運算子,使得宏在呼叫時的實參可以在替換文字中同時作為字串和算術表示式:
#define printDBL( exp ) printf( #exp " = %f ", exp )
printDBL( 4 * atan(1.0)); // atan()在math.h中定義
上面的最後一行程式碼是宏呼叫,展開形式如下所示:
printf( "4 * atan(1.0)" " = %f ", 4 * atan(1.0));
因為編譯器會合並緊鄰的字串字面量,上述程式碼等效為:
printf( "4 * atan(1.0) = %f ", 4 * atan(1.0));
該語句會生成下列文字並在控制台輸出:
4 * atan(1.0) = 3.141593
在下面的範例中,呼叫宏 showArgs 以演示 # 運算子如何修改宏實參中空白符、雙引號,以及反斜線:
#define showArgs(...) puts(#__VA_ARGS__)
showArgs( onen, "2n", three );
前處理器使用下面的文字來替換該宏:
puts("onen, "2n", three");
該語句生成下面的輸出:
one
, "2n", three
記號貼上運算子
運算子是一個二元運算子,可以出現在所有宏的替換文字中。該運算子會把左、右運算元結合在一起,作為一個記號,因此,它常常被稱為記號
貼上運算子(token-pasting operator)。如果結果文字中還包含有宏名稱,則前處理器會繼續進行宏替換。出現在 ## 運算子前後的空白符連同 ## 運算子本身一起被刪除。
通常,使用 ## 運算子時,至少有一個運算元是宏的形參。在這種情況下,實參值會先替換形參,然後等記號貼上完成後,才進行宏展開。如下例所示:
#define TEXT_A "Hello, world!"
#define msg(x) puts( TEXT_ ## x )
msg(A);
無論識別符號 A 是否定義為一個宏名稱,前處理器會先將形參 x 替換成實參 A,然後進行記號貼上。當這兩個步驟做完後,結果如下:
puts( TEXT_A );
現在,因為 TEXT_A 是一個宏名稱,後續的宏替換會生成下面的語句:
puts( "Hello, world!" );
如果宏的形參是 ## 運算子的運算元,並且在某次宏呼叫時,並沒有為該形參準備對應的實參,那麼預處理使用預留位置(placeholder)表示該形參被空字串取代。
把一個預留位置和任何記號進行記號貼上操作的結果還是原來的記號。如果對兩個預留位置進行記號貼上操作,則得到一個預留位置。
當所有的記號貼上運算都做完後,前處理器會刪除所有剩下的預留位置。下面是一個範例,呼叫宏時傳入空的實參:
msg();
這個呼叫會被展開為如下所示的程式碼:
puts( TEXT_ );
如果TEXT_不是一個字串型別的識別符號,編譯器會生成一個錯誤資訊。
字串化運算子和記號貼上運算子並沒有固定的運算次序。如果需要採取特定的運算次序,可以將一個宏分解為多個宏。
在宏內使用宏
在替換實參,以及執行完 # 和 ## 運算之後,前處理器會檢查操作所得的替換文字,並展開其中包含的所有宏。但是,宏不可以遞回地展開:如果前處理器在 A 宏的替換文字中又遇到了 A 宏的名稱,或者從巢狀在 A 宏內的 B 宏內又遇到了 A 宏的名稱,那麼 A 宏的名稱就會無法展開。
類似地,即使展開一個宏生成有效的命令,這樣的命令也無法執行。然而,前處理器可以處理在完全展開宏後出現 _Pragma 運算子的操作。
下面的範例程式以表格形式輸出函數值:
// fn_tbl.c: 以表格形式輸出一個函數的值。該程式使用了巢狀的宏
// -------------------------------------------------------------
#include <stdio.h>
#include <math.h> // 函數cos()和exp()的原型
#define PI 3.141593
#define STEP (PI/8)
#define AMPLITUDE 1.0
#define ATTENUATION 0.1 // 聲波傳播的衰減指數
#define DF(x) exp(-ATTENUATION*(x))
#define FUNC(x) (DF(x) * AMPLITUDE * cos(x)) // 震動衰減
// 針對函數輸出:
#define STR(s) #s
#define XSTR(s) STR(s) // 將宏s展開,然後字串化
int main()
{
double x = 0.0;
printf( "nFUNC(x) = %sn", XSTR(FUNC(x)) ); // 輸出該函數
printf("n %10s %25sn", "x", STR(y = FUNC(x)) ); // 表格的標題
printf("-----------------------------------------n");
for ( ; x < 2*PI + STEP/2; x += STEP )
printf( "%15f %20fn", x, FUNC(x) );
return 0;
}
該範例輸出下面的表格:
FUNC(x) = (exp(-0.1*(x)) * 1.0 * cos(x))
x y = FUNC(x)
-----------------------------------------
0.000000 1.000000
0.392699 0.888302
...
5.890487 0.512619
6.283186 0.533488
宏的作用域和重新定義
你
無法再次使用 #define 命令重新定義一個已經被定義為宏的識別符號,除非重新定義所使用的替換文字與已經被定義的替換文字完全相同。如果該宏具有形參,重新定義的形參名稱也必須與已定義形參名稱的一樣。
如果想改變一個宏的內容,必須首先使用下面的命令取消現在的定義:
#undef 宏名稱
執行上面的命令之後,識別符號“宏名稱”可以再次在新的宏定義中使用。如果上面指定的識別符號並非一個已定義的宏名稱,那麼前處理器會忽略這個 #undef 命令。
標準庫中的多個函數名稱也被定義成了宏。如果想直接呼叫這些函數,而不是呼叫同名稱的宏,可以使用 #undef 命令取消對這些宏的定義。即使準備取消定義的宏是帶有引數的,也不需要在 #undef 命令中指定參數列。如下例所示:
#include <ctype.h>
#undef isdigit // 移除任何使用該名稱的宏定義
/* ... */
if ( isdigit(c) ) // 呼叫函數isdigit()
/* ... */
當某個宏首次遇到它的 #undef 命令時,它的作用域就會結束。如果沒有關於該宏的 #undef 命令,那麼它的作用域在該翻譯單元結束時終止。