你真的瞭解巨集嗎:淺談巨集定義(#define語句)
阿新 • • 發佈:2019-01-28
寫在前面:
本文所有程式碼均在Linux環境下執行
Linux版本為CentOS 7.4
巨集定義
語法
#define name Stuff
#define PI 3.14
//定義一個M,值為3.14
#define DO_FOREVER for(;;)
//定義一個死迴圈
#define REG register
//定義REG來作為register的別名
#define CASE break;case
//在switch中用CASE來補上break;
#define DEBUG_PRINT printf("file:%s\tline:%d\tdate:%s\ttime:%s\n",\
__FILE__, __LINE__, __DATE__, __TIME__);
//測試預定義符號
1、巨集的作用範圍
先看下面程式碼:
讓我們檢視上圖中程式碼經過預處理後的樣子
可以發現巨集只對巨集定義後的行數起作用,且與定義在哪裡無關,即使函式不被呼叫,也可以使用巨集
2、巨集替換的原則
在程式中擴充套件#define定義符號和巨集時,需要涉及幾個步驟。
1. 在呼叫巨集時,首先對引數進行檢查,看看是否包含任何由#define定義的符號。如果是,它們首先被替換。
2. 替換文字隨後被插入到程式中原來文字的位置。對於巨集,引數名被他們的值替換。
3. 最後,再次對結果檔案進行掃描,看看它是否包含任何由#define定義的符號。如果是,就重複上述處理過程。
注意:
1、巨集函式不能出現遞迴
2、巨集定義的符號,即name部分不會在預處理替換的時候被搜尋
3、巨集定義#define後不需要加;
例如:
#define M 100;
這裡在100後面加上了
;
在句子中有時候就會出現問題
觀察下面程式碼片段:
#define M 100;
if (condition)
m = M;
else
max = 0;
在該選擇語句中會出現語法錯誤,沒有與else與之匹配的if語句
4、巨集函式申明
巨集函式申明格式:
#define name ( parament-list ) stuff
parament-list為引數表,可以包含多個引數,他們會在stuff出現
例如:
#define SQARE( X ) X*X
//定義一個計算乘方的巨集函式
但是這麼定義是會出現預料之外的錯誤的,觀察下面程式碼片段
#define SQUARE(X) X*X
int main()
{
int a = 5;
printf("%d\n", SQUARE(a+1));
return 0;
}
本來結果應該為6^2 = 36
但是實際計算機輸出的結果為11
我們用gcc觀察預處理後的程式碼片段是怎麼樣的
使用命令$ gcc -E test.c -o test.i
來檢視預處理後的程式碼
可以看到這裡原式被替換成 a + 1 * a + 1 = 11,而這個結果顯然不是我們期望的
這裡我們提出解決方案,將 X 用括號括起來(X),這樣就避免上述程式碼因符號優先順序帶來的錯誤
至此,上面程式碼解決了,來看下面的巨集函式定義:
#define DOUBLE(X) (X) + (X)
int main()
{
int a = 5;
printf("%d\n", 10 * DOUBLE(a));
return 0;
}
期望結果為100,而看程式執行結果:
程式再次出現預料之外的結果,原因是原式被替換成了
10 * 5 + 5 = 55
解決方法:
在巨集函式定義時,對Stuff中的引數以及結果均用括號來避免因符號運算優先順序帶來的問題
5、巨集中的#和##
#的用法
首先要理解一個原則,即鄰近字串連線原則
在C語言中
printf(“hello”” world!”“\n”);
這句話是合法的
列印結果為:
hello world!
按照上述原則,我們可以寫出下列程式碼:
#define PRINT(FORMAT, VALUE) printf("the value of " #VALUE " is "FORMAT"\n", VALUE)
int main()
{
int i = 0;
PRINT("%d", i + 3);
return 0;
}
看下gcc編譯後的程式碼,程式正常執行:
我們再通過gcc生成test.i檔案看下預處理是怎麼樣的:
所以我們可以看出來#的作用:
將一個巨集引數變成一個對應的字串
在上述例子中:
#VALUE被替換成了“i + 3”
別忘了被替換的時候i + 3兩邊加上了雙引號
##的用法
看下面的程式碼片段
#define ADD_TO_AN(num, value) a##num += value
int main()
{
int a1 = 0;
int a2 = 0;
ADD_TO_AN(1, 5);
ADD_TO_AN(2, 6);
return 0;
}
解釋一下上面這段程式碼
假設有一個變數叫a1
此時程式碼片段為ADD_TO_AN(1, 5)
即替換為a1 += 5,給a1變數增加5
同理ADD_TO_AN(2, 6)則替換成a2 += 6
##的作用
將##兩邊的字元連在一起作為一個識別符號
前提連線後的識別符號必須合法,否則編譯出現識別符號未定義
6、巨集和函式
巨集通常被應用於執行簡單的運算
和函式相比,巨集有他的優點
巨集的優點:
1. 用於呼叫函式和從函式返回的程式碼可能比實際執行這個小型計算工作所需要的時間更多。所以巨集比函式在程式的規模和速度方面更勝一籌
2. 更為重要的是函式的引數必須宣告為特定的型別。所以函式只能在型別合適的表示式上使用。反之這個巨集怎可以適用於整形、長整型、浮點型等可以用於
>
來比較的型別。巨集的引數與型別無關的3. 巨集引數可以使用變數型別,而函式不可以,例如:
#define MALLOC(num, type) (type *)malloc(num * sizeof(type))
MALLOC(10, int);//型別作為引數
//前處理器替換之後:
(int *)malloc(10 * sizeof(int));
但是,巨集引數與型別無關是一把雙刃劍,和函式比較也有他的缺點
巨集的缺點:
1. 每次使用巨集的時候,一份巨集定義的程式碼將插入到程式中。除非巨集比較短,否則可能大幅度增加程式的長度
2. 巨集是沒法除錯的
3. 巨集由於型別無關,也就不夠嚴謹
4. 巨集可能會帶來運算子優先順序的問題,導致程式容易出現問題
我的建議:
當有一部分功能既可以用函式實現也可以用巨集實現,且在呼叫函式的過程與巨集使用過程所消耗的資源相當時,優先考慮程式碼的嚴謹性,使用函式
7、巨集引數的副作用
當巨集引數在巨集的定義中出現超過一次的時候,如果引數帶有副作用,那麼你在使用這個巨集的時候就可能出現危險,導致不可預測的後果。副作用就是表示式求值的時候出現的永久性效果。
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
//結果為x=6 y=10 z=9
可以看到上面程式碼中y++帶來了副作用,所以在使用巨集的時候,避免使用++
運算和--
運算
8、巨集的命名
巨集和函式的使用很類似,我們平時的使用習慣是:
1. 巨集名全部大寫
2. 函式名不全大寫