do{...}while(0U)的作用於意義
很多初學者,以及有一定工作經驗的朋友都不知道這個“do{...}while(0U)”的作用和意義,甚至覺得這樣寫的程式碼複雜且沒有意義。
相信看過STM32的HAL庫原始碼和Linux核心原始碼的朋友,都知道原始碼裡面有許多do{...}while(0U)的巨集定義語句。除了用於巨集定義之外,do{...}while(0U)還有很多作用。
2.1 確保巨集定義正確展開
Google的Robert Love(先前從事Linux核心開發)解釋到:
do{...}while(0U)在C中是唯一的構造程式,讓你定義的巨集總是以相同的方式工作,這樣不管怎麼使用巨集(尤其在沒有用大括號包圍呼叫巨集的語句),巨集後面的分號也是相同的效果 。
簡單理解就是:使用do{...}while(0U)構造後的巨集定義不受使用的大括號、分號等的影響,而總會按照你期望的意圖執行。
例如:
#define foo(x) bar(x); baz(x)
當如下呼叫:
foo(wolf);
則會被展開為:
bar(wolf); baz(wolf);
看來是沒有問題。但要是結合if語句呼叫:
if(!value) foo(wolf);
則會被展開為:
if(!value) bar(wolf); baz(wolf);
這顯然就違揹我們期望的效果了。但如果我們使用do{...}while(0U)來重新定義巨集:
#define foo(x) do {bar(x); baz(x);}while(0U)
則展開的效果為:
if(!value) do { bar(x); baz(x); }while(0U)
2.2 實現區域性作用域
我們知道巨集定義只是做一個識別符號和字串的替換,儘管巨集定義的形式可以類似函式,但是它實際並不具備與函式類似的區域性作用域。
我們當然可以通過使用大括號的形式(如:#define func(x) {...}),來實現區域性作用域,但是這樣會帶來新的麻煩:
#define swap(a, b) {a = a + b; b = a - b; a = a -b} int main(void) { int a = 1, b = 2; if(1) swap(a, b); else a = b = 0; return 0; }
上面程式碼咋一看沒有問題,但是對其進行巨集展開後如下:
int main(void) { int a = 1, b = 2; if(1) { a = a + b; b = a - b; a = a - b; }; else a = b = 0; return 0; }
這下問題就明顯了:在if後的程式碼後面多出了一個‘;’,這會引發編譯錯誤。使用該巨集定義時不在後面加‘;’可以解決這個問題,但這顯然不符合我們的編碼習慣,且要求巨集編寫者和使用者必須使用統一的標準。而在巨集定義中使用do{...}while(0U)就可以解決這個問題:
#define swap(a, b) do{ \ a = a + b; \ b = a - b; \ a = a - b; \ }while(0U) int main(void) { int a = 1, b = 2; if(1) swap(a, b); else a = b = 0; return 0; }
展開後的效果為:
int main(void) { int a = 1, b = 2; if(1) do{ a = a + b; b = a - b; a = a - b; }while(0U); else a = b = 0; return 0; }
這樣我們就可以放心地在巨集定義後面使用分號而不會造成問題了。
2.3 避免使用goto語句
在C/C++語言程式中,我們可能會發生錯誤以後做一些特殊處理(比如資源釋放),如果順利執行則直接退出,我們常使用goto來實現:
#include <stdio.h> #include <stdlib.h> int main(void) { int ret = -1; ret = func1(); if(0 != ret) goto failed; ret = func2(); if(0 != ret) goto failed; ret = func3(); if(0 != ret) goto failed; return 0; failed: do_err(); }
但是,不建議在程式中大量使用goto。雖然使用goto語句可以解決程式碼冗餘,也能提高程式的靈活性與簡潔性,但這樣也會使程式結構變得混亂不易維護 (我們更看重的是可維護性)。我們可以使用do{...}while(0U)來代替goto實現相同的功能:
#include <stdio.h> #include <stdlib.h> int main(void) { int ret = -1; do { ret = func1(); if(0 != ret) break; ret = func2(); if(0 != ret) break; ret = func3(); if(0 != ret) break; return 0; }while(0U); do_err(); }
2.4 避免空宣告在編譯時出現警告
在Linux核心原始碼中,經常看到如下巨集定義以避免在編譯時出現警告:
#define FOO do { }while(0U)