謝邀。
C語言中的define宏定義可以像函數那樣接收參數(這種宏定義常被稱作“函數式宏定義”),不過不能像函數那樣提供參數的類型檢查,這個特點在有些程序員看來是不安全的。
C語言中的“函數式宏定義”
但是,函數式宏定義不關心參數類型這個特點,有時候也會被利用起來,寫出一些適用性更廣的C語言代碼,例如:
上面這段C語言宏定義代碼實現了一個max()方法,它接收兩個參數,并返回較大的那個參數,max()方法不關心參數的類型,因此__a和__b可以是int型的,也可以是char型或者double型以及其他數據類型的。
如果使用max()方法提供的功能以C語言函數的方式來寫,就稍顯麻煩些了,程序員不得不為每一種數據類型實現一個max()函數。更加糟糕的是,C語言并不支持函數的重載,因此max()這個函數名一旦被使用,其他函數就不能再使用了,因此相關的C語言代碼可能是下面這樣的:
這樣對比起來,顯然使用define宏來定義max()方法更加方便一些。不過,C語言中的宏定義不提供參數類型檢查的確也是一個缺點,它可能會導致程序的不安全,讀者不應忽視這一點。因此如果不是必須要使用define宏定義才能解決問題,應該盡可能的使用函數,若是希望能夠得到較高效率的代碼,可以使用inline函數。
關于inline函數,我之前的文章較為詳細的討論過。
使用C語言中宏定義的注意事項
C語言中的“函數式宏定義”雖然使用起來很像函數,但它實際上并不是函數,讀者千萬不能忽視這一點,不然可能會寫出具有隱患,甚至嚴重錯誤的C語言程序。請看下面這個例子:
上面這段C語言代碼編譯并執行,會輸出什么呢?
在main()函數中,變量a和b都被初始化為2。接著調用了max()宏,傳遞的參數分別是++a和b,粗略來看,此時執行max(++a,b),就相當于執行max(3,2),那上面這段C語言程序會輸出3,2,3了?得到答案最簡單粗暴的方法就是編譯并執行這段代碼,請看:
沒有經驗的讀者看到實際輸出估計會大吃一驚,a和m怎么不是3而是4呢?并沒有第二處給a再加一啊?上一節曾討論,編譯器會將C語言中的宏定義展開到被調用處,而不是像函數那樣編譯后,再通過call指令調用。使用gcc-E命令查看編譯器將上述C語言代碼預處理后的代碼,得到如下結果,請看:
顯然,這里就是C語言中“函數式宏定義”的注意事項了,傳遞給max()的參數++a會被展開到宏定義中所有的__a處,這就解釋了為何a和m最后都等于4而不是3了。
“函數式宏定義”還有其他與真正函數不同的地方,例如“函數式宏定義”就不適合用于遞歸等。
使用do{}while(0)包裹代碼
盡管C語言中的“函數式宏定義”和真正的函數相比有一些缺點,但只要小心使用還是會顯著提高代碼的執行效率的,畢竟省去了分配和釋放棧幀、傳參、傳返回值等一系列工作。正因為如此,Linux內核中有相當多的方法是使用define宏定義實現的,并且,在內核C語言代碼中,“函數式宏定義”經常借助do{}while(0)實現,例如:
為什么要用do{}while(0)包裹C語言代碼呢?不使用do{}while(0)包裹起來有什么不好嗎?請看下面這幾行C語言代碼:
宏定義被編譯器展開后,會產生下面這樣的C語言代碼:
這可能就與程序員的意圖不一致了,這種情況下__release(lock);并沒有在if(cond)的作用范圍內。可能讀者會說,那像函數一樣,使用{}包裹代碼不就可以了嗎?請再來看看下面這幾行C語言代碼:
問題就出在spin_unlock(lock);后面的這個分號“;”,如果不寫就不像函數調用,如果寫了就會引發語法錯誤——if語句會被這個“;”提前結束,else無法與其配對。這么看來,在C語言的“函數式宏定義”中使用do{}while(0)包裹C語言代碼顯然就是一個不錯的方法了。
小結
“函數式宏定義”并不是真正的函數,它與真正的函數是有區別的,如果弄不清楚這一點,很容易迷惑。在最后,我們一起分析了常用do{}while(0)包裹宏定義的代碼的原因,讀者今后在C語言程序開發中,也可以使用該技巧。
歡迎在評論區一起討論,質疑。文章都是手打原創,每天最淺顯的介紹C語言、linux等嵌入式開發,喜歡我的文章就關注一波吧,可以看到最新更新和之前的文章哦。