在之前的文章裡,我們曾討論C語言程序開發中 define 宏定義的「陷阱」之一就是可能會產生多次「副作用」,這也是C語言中函數式宏定義與真正函數的主要區別之一。顯然,define 宏定義的這種「陷阱」會導致程序存在隱患,而且這種隱患造成的危害不亞於「野指針」。
C語言函數式宏定義的缺陷
例如這面這個經典的例子,請看相關C語言代碼:
#define max(a, b) ( (a)>(b)?(a):(b) )max 宏接收兩個參數,並且返回較大的參數值。如果該宏在一個較大的C語言項目中較為頻繁的使用,很難保證每次傳遞給 max 的兩個參數不是計算表達式,也就是說 max 宏的參數 a 和 參數 b 有可能是一個計算表達式,例如:
int val = 3;int m = max(val++, 2);上面這兩行C語言代碼常常會給程式設計師一種 val++ 只會執行一次的錯覺,但是事實上編譯器會將上述代碼預處理為:
int val = 3;int m = ( (val++)>(2)?(val++):(2) );也就是說,val++ 會被執行兩次(即產生兩次副作用),執行完這兩條語句後,val 是等於 5 ,而不是等於 4 的。編寫C語言代碼測試之:
編譯並執行這段C語言代碼,得到如下輸出:
# gcc t.c# ./a.out val = 5, m = 4這樣的錯誤雖然很簡單,但是人常常會對這種「擺在眼前的錯誤」視而不見,所以花費大量時間才能定位到它也不足為奇。另外,這樣的錯誤又會顯得「飄忽不定」,因為如果傳遞給 max 的兩個參數,後一個數比前一個數大,則 val++ 又會只執行一次了,例如:
int val = 3; int m = max(val++, 6);// val=4, m=6這種類型的錯誤在實際的C語言項目開發中,相當煩人。
事實上,我就遇到過這樣的錯誤,而且花了一些時間才找到問題代碼。
避免多次副作用
C99 對 C語言做了一定的擴展,」({ … })」 就是其中之一(這個符號我們之前討論過),可以把這個符號包裹的代碼理解為一句,例如:
val = ({ a = 3; c = a+b; c;})上面這段C語言代碼相當於下面這句:
a = 3;val = a+b;所以基於此,我們可以對前面提到的有「缺陷」的 max 宏做一點改進,請看:
#define maxint(a, b) ({ int _a = a, _b = b; _a>_b?_a:_b; })使用中間變量 _a 和 _b 看似麻煩,但是有兩個好處:可以防止傳入計算表達式時產生的多次「副作用」,而且還使 maxint 宏具備了參數類型檢查的功能。
C語言是一門高效的程式語言,因此它關心數據的類型,不同類型的數據相比較有時候會產生不預期的結果。這其實也屬於C語言中宏的「缺陷」,因此一般能夠使用函數完成的工作都不建議再使用宏。如果某個功能的代碼比較簡單,希望提升其效率,可以使用 inline 函數(內聯函數)定義。
總之,除非某個宏能夠提供非常大的便利,否則非常不建議使用宏。
經過改進的 maxint 宏能夠提供參數類型檢查,這主要得益於中間變量的使用。因此如果傳遞給 maxint 宏一個浮點數,maxint 宏會將其截斷成 int 型再做比較,例如:
val = maxint(5, 8.14);執行完畢後,val 是等於 8 的。
另外一個小技巧
在使用三目運算符「?:」時,可以考慮下面這個小技巧,請看:
p = x>y?:y;將 ?: 之間的數值省去,也是C99中的一個新特性,至於該技巧有哪些性質,以及可以應用於哪些場合,留給讀者自己思考了。
小結
本節主要討論了C語言中 define 宏的兩個「缺陷」——可能產生多次「副作用」,以及難以提供參數的類型檢查。不過也應該明白,這些缺點有時候會成為非常有用的特點,它們可以與函數互補,提供更加靈活的功能。但是如果不希望某個宏具有這兩個特點,可以考慮本節提供的小技巧。