C語言中文網 目錄
首頁 > 編程筆記 > C語言筆記 閱讀:8,576

C語言宏的定義和宏的使用方法(#define)

在 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 = %d\n", __func__, intVar );

預處理器把最后一行的宏調用替換成下面的一行代碼:
fprintf( fp_log, "%s: intVar = %d\n", __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( one\n,       "2\n", three );

預處理器使用下面的文本來替換該宏:
puts("one\n, \"2\\n\", three");

該語句生成下面的輸出:
one
, "2\n", 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) = %s\n", XSTR(FUNC(x)) );          // 輸出該函數
  printf("\n %10s %25s\n", "x", STR(y = FUNC(x)) );             // 表格的標題
  printf("-----------------------------------------\n");

  for ( ; x < 2*PI + STEP/2; x += STEP )
    printf( "%15f %20f\n", 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 命令,那么它的作用域在該翻譯單元結束時終止。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo