在第二天的文章中,我們提到巨集可以定義函數,但為何不用一般的函數宣告就好,還要用巨集定義函數?
用巨集定義函數的好處是函數會直接在呼叫處展開執行,而不會出現一般函數的堆疊與參數傳遞,有助於提升效能,示意圖如下:
正常函數與用巨集定義函數的比較
透過上圖,我們可以看到若使用左邊的正常函數,則主函數 main() 呼叫 add() 函數時,會離開主函數,去 add() 函數處理當中的內容,這時主要會發生兩件耗效能的事情:
main() 跳去執行 add() 函數又跳回 main()。x = 3 跟 y = 5 要從 main() 傳到 add() 之後,再把運算結果傳回 main()。相較之下,若使用右邊的巨集,則巨集裡定義的函數在預處理階段就已經展開,就好像是一般在主函數中的運算式,不會有跳來跳去與參數傳遞的額外成本。
在 C99 之後,C 語言另外推出「內嵌函數(inline function)」,其語法以 inline 開頭,例如:
#include <stdio.h>
inline int add_inline(int a, int b) {
return a + b;
}
int main() {
int x = 3, y = 5;
int result = add_inline(x, y);
printf("result = %d\n", result);
return 0;
}
當中的 add_inline 函數會直接在 main() 函數裡面展開,運作流程與巨集相似:
既然巨集跟內嵌函數都能讓函數直接展開、那為何還要區分這兩者呢?
| 巨集 | 內嵌函數 |
|---|---|
| 無型別檢查 | 有型別檢查 |
| 參數可被計算超過一次 | 參數只算一次 |
| 較難除錯 | 較易除錯 |
| 不可遞迴 | 可遞迴 |
第四點是說內嵌函數可以自我呼叫、但巨集不行,比較好理解,前三點倒是可以透過一個實際範例說明。
第二天的文章中,我們提到如下程式碼可能產生副作用:
#include <stdio.h>
#define MIN(a, b) ((a) < (b) ? (a) : (b)) // 使用巨集無型別檢查
int main() {
int arr[] = {5, 8, 12};
int *p = arr;
int b = 10;
int least;
least = MIN(*p++, b);
printf("least = %d\n", least);
printf("p points to = %d\n", *p);
return 0;
}
這個 *p++ 會被執行兩次:
*p++ 跟 b 兩者大小,而 (*p++) 代表 arr[1],其值為是 8,因此 (*p++) < (b) 成立。(a),而 (a) 是 *p++,因此還最多計算一次 *p++,跑到 arr[2]。*p++ 在第二步多執行一次的結果導致最終 p 指標指向 arr[2]、而非上一步跟 b 比較大小時使用的 arr[1],形成副作用。
而若使用內嵌函數:
#include <stdio.h>
inline int min(int a, int b) { // 使用內嵌函數有型別檢查
return a < b ? a : b;
}
int main() {
int arr[] = {5, 8, 12};
int *p = arr;
int b = 10;
int least;
least = min(*p++, b);
printf("least = %d\n", least);
printf("p points to = %d\n", *p);
return 0;
}
在進行 least = min(*p++, b); 時,*p++ 跟 b 都已經先求好其值分別為 8 跟 10 ,才接著進行 min() 運算,所以三元運算式中問號右方的 a 已經是 8、而非 *p++,因此不會多算一次。
內嵌函數的型別檢查與避免副作用的機制,讓我們得以比巨集更容易除錯,因此,除非要定義的函數非常簡單、或者使用 C99 以後版本的 C 語言,建議優先使用內嵌函數。