宏定義(無參宏定義和帶參宏定義),C語言宏定義詳解

2020-07-16 10:04:21
宏定義是比較常用的預處理指令,即使用“識別符號”來表示“替換列表”中的內容。識別符號稱為宏名,在預處理過程中,前處理器會把源程式中所有宏名,替換成宏定義中替換列表中的內容。

常見的宏定義有兩種,不帶引數的宏定義帶引數的宏定義

無參宏定義

無引數宏定義的格式為:

#define 識別符號 替換列表

替換列表可以是數值常數、字元常數、字串常數等,故可以把宏定義理解為使用識別符號表示一常數,或稱符號常數。

說明:
1) # 可以不在行首,但只允許它前面有空格符。例如:
#define PI 3.1416 //正確,該行#前允許有空格
int a;#define N 5 //錯誤,該行#前不允許有空格外的其他字元

2) 識別符號和替換列表之間不能加賦值號 =,替換列表後不能加分號
#define N =5 //雖語法正確,但前處理器會把N替換成=5
int a[N]; //錯誤,因為宏替換之後為 int a[=5];
宏定義不是語句,是預處理指令,故結尾不加分號。如果不小心新增了分號,雖然有時該宏定義沒問題,但在宏替換時,可能導致 C 語法錯誤,或得不到預期結果。例如:
#define N 5; //雖語法正確,但會把N替換成5;
int a[N]; //語法錯誤,宏替換後,為int a[5;];錯誤

3) 由於宏定義僅是做簡單的文字替換,故替換列表中如有表示式,必須把該表示式用括號括起來,否則可能會出現邏輯上的“錯誤”。例如:
#define N 3+2
int r=N*N;
宏替換後為:
int r=3+2*3+2; //r=11
如果採用如下形式的宏定義:
#define N (3+2)
int r=N*N;
則宏替換後,為:
int r=(3+2)*(3+2); //r=25

4) 當替換列表一行寫不下時,可以使用反斜線作為續行符延續到下一行。例如:
#define USA "The United 
States of 
America"
該宏定義中替換列表為字串常數,如果該串較長,或為了使替換列表的結構更清晰,可使用續行符 把該串分若干行來寫,除最後一行外,每行行尾都必須加續行符 。

如果呼叫 printf 函數,以串的形式輸出該符號常數,即:
printf("%sn",USA);
則輸出結果為:The United States of America

注意:續行符後直接按確認鍵換行,不能含有包括空格在內的任何字元,否則是錯誤的宏定義形式。

帶參宏定義

帶引數的宏定義格式為:

#define 識別符號(引數1,引數2,...,引數n) 替換列表

例如,求兩個引數中最大值的帶參宏定義為:
#define MAX(a,b) ((a)>(b)?(a) : (b))
當有如下語句時:
int c=MAX(5,3);
前處理器會將帶引數的宏替換成如下形式:
int c=((5)>(3)?(5) : (3));
故計算結果c=5。

刪除宏定義的格式為:

#undef 識別符號

說明:
1) 識別符號與參數列的左括號之間不能有空格,否則前處理器會把該宏理解為普通的無參宏定義,故以下是錯誤的帶參宏定義形式。
#define MAX (a,b) ( (a) > (b) ? (a) : (b) ) //錯誤的帶參宏定義格式
2) 宏替換列表中每個引數及整個替換列表,都必須用一對小括號 () 括起來,否則可能會出現歧義。

【例 1】以下程式試圖定義求兩個引數乘積的宏定義,欲使用該宏求 3 與 6 的乘積,分析該程式能否實現預期功能,如果不能,請給出修改方案。
#include <stdio.h>
#define MUL(a,b) (a*b)
int main (void)
{
    int c;
    c=MUL(3,5+1);
    printf("c=%dn",c);
    return 0;
}
分析:
1) 由於該宏定義中的替換列表中的引數沒有加括號,故宏呼叫時,如果引數是個表示式,可能會出現歧義,得不到預期結果。

本例中宏呼叫 c=MUL(3,5+1); 會替換成 c=(3*5+1)=16;,與預期功能不符。

2) 雖然把宏呼叫時的引數 5+1 括起來,可達到題目要求的效果,但這屬於治標不治本。為統一程式設計規範,把替換列表中的每個引數均加括號,整個替換列表也加括號。

同時,為達到標本兼治,在宏定義時,除單一值引數外,應顯式加括號。

修改程式碼為:
#include <stdio.h>
#define MUL(a,b) ((a)*(b))//修改處1
int main (void)
{
    int c;
    c=MUL(3,(5+1);//修改處2
    printf("c=%dn",c);
    return 0;
}

帶參宏定義 VS 函教呼叫

接下來將從呼叫發生時間、引數型別檢查、引數是否需要空間、執行速度等幾個主要方面進行對比分析帶參宏定義與函數呼叫的差異。

呼叫發生的時間

在源程式進行編譯之前,即預處理階段進行宏替換;而函數呼叫則發生在程式執行期間。

引數型別檢查

函數引數型別檢查嚴格。程式在編譯階段,需要檢查實參與形參個數是否相等及型別是否匹配或相容,若引數個數不相同或型別不相容,則會編譯不通過。

在預處理階段,對帶參宏呼叫中的引數不做檢查。即宏定義時不需要指定引數型別,既可以認為這是宏的優點,即適用於多種資料型別,又可以認為這是宏的一個缺點,即型別不安全。故在宏呼叫時,需要程式設計者自行確保宏呼叫引數的型別正確。

引數是否需要空間

函數呼叫時,需要為形參分配空間,並把實參的值複製一份賦給形參分配的空間中。而宏替換,僅是簡單的文字替換,且替換完就把宏名對應識別符號刪除掉,即不需要分配空間。

執行速度

函數在編譯階段需要檢查引數個數是否相同、型別等是否匹配等多個語法,而宏替換僅 是簡單文字替換,不做任何語法或邏輯檢查。

函數在執行階段引數需入棧和出棧操作,速度相對較慢。

程式碼長度

由於宏替換是文字替換,即如果需替換的文字較長,則替換後會影響程式碼長度;而函數不會影響程式碼長度。

故使用較頻繁且程式碼量較小的功能,一般採用宏定義的形式,比採用函數形式更合適。前面章節頻繁使用的 getchar(),準確地說,是宏而非函數。

為了使該宏呼叫像函數呼叫,故把該宏設計成了帶引數的宏定義:

#define getchar() getc(stdin)

故呼叫該宏時,需要加括號,即傳空引數:getchar()。