1. 程式人生 > >C語言巨集的定義和巨集的使用方法(#define)

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

1、巨集的功能介紹

  • 在 C 語言中,可以採用命令 #define 來定義巨集。該命令允許把一個名稱指定成任何所需的文字,例如一個常量值或者一條語句。在定義了巨集之後,無論巨集名稱出現在原始碼的何處,前處理器都會把它用定義時指定的文字替換掉。
  • 關於巨集的一個常見應用就是,用它定義數值常量的名稱:
#define         ARRAY_SIZE 100
double   data[ARRAY_SIZE];
  • 這兩行程式碼為值 100 定義了一個巨集名稱 ARRAY_SIZE,並且在陣列 data 的定義中使用了該巨集。慣例將巨集名稱每個字母採用大寫,這有助於區分巨集與一般的變數。
    上述簡單的示例也展示了巨集是怎樣讓 C 程式更有彈性的。
  • 在翻譯的第三個步驟中,前處理器會分析原始檔,把它們轉換為前處理器記號和空白符。如果遇到的記號是巨集名稱,前處理器就會展開(expand)該巨集;也就是說,會用定義的文字來取代巨集名稱。出現在字串字面量中的巨集名稱不會被展開,因為整個字串字面量算作一個前處理器記號。
  • 無法通過巨集展開的方式建立前處理器命令。即使巨集的展開結果會生成形式上有效的命令,但前處理器不會執行它。
  • 在巨集定義時,可以有引數,也可以沒有引數。

2、沒有引數的巨集

  • 沒有引數的巨集定義,採用如下形式:
#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] 之間。

3、帶引數的巨集

  • 你可以定義具有形式引數(簡稱“形參”)的巨集。當前處理器展開這類巨集時,它先使用呼叫巨集時指定的實際引數(簡稱“實參”)取代替換文字中對應的形參。帶有形參的巨集通常也稱為類函式巨集(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),這與期望的運算不同。
  • 3.1 可選引數

    • 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 的內容寫入日誌檔案。
  • 3.2 字串化運算子

    • 一元運算子 # 常稱為字串化運算子(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
  • 3.3 記號貼上運算子

    • 運算子是一個二元運算子,可以出現在所有巨集的替換文字中。該運算子會把左、右運算元結合在一起,作為一個記號,因此,它常常被稱為記號貼上運算子(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_不是一個字串型別的識別符號,編譯器會生成一個錯誤資訊。
    • 字串化運算子和記號貼上運算子並沒有固定的運算次序。如果需要採取特定的運算次序,可以將一個巨集分解為多個巨集。

4、在巨集內使用巨集

  • 在替換實參,以及執行完 # 和 ## 運算之後,前處理器會檢查操作所得的替換文字,並展開其中包含的所有巨集。但是,巨集不可以遞迴地展開:如果前處理器在 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

5、巨集的作用域和重新定義

  • 你無法再次使用 #define 命令重新定義一個已經被定義為巨集的識別符號,除非重新定義所使用的替換文字與已經被定義的替換文字完全相同。如果該巨集具有形參,重新定義的形參名稱也必須與已定義形參名稱的一樣。
  • 如果想改變一個巨集的內容,必須首先使用下面的命令取消現在的定義:
#undef 巨集名稱
  • 執行上面的命令之後,識別符號“巨集名稱”可以再次在新的巨集定義中使用。如果上面指定的識別符號並非一個已定義的巨集名稱,那麼前處理器會忽略這個 #undef 命令。
  • 標準庫中的多個函式名稱也被定義成了巨集。如果想直接呼叫這些函式,而不是呼叫同名稱的巨集,可以使用 #undef 命令取消對這些巨集的定義。即使準備取消定義的巨集是帶有引數的,也不需要在 #undef 命令中指定引數列表。如下例所示:
#include <ctype.h>
#undef isdigit          // 移除任何使用該名稱的巨集定義
/* ... */
if ( isdigit(c) )               // 呼叫函式isdigit()
/* ... */
  • 當某個巨集首次遇到它的 #undef 命令時,它的作用域就會結束。如果沒有關於該巨集的 #undef 命令,那麼它的作用域在該翻譯單元結束時終止。