這篇文章講解的知識點很小,可是在一些編程場合中很是適用,你們能夠把這篇短文當作甜品來品味一下。html
地球人都知道,do-while語句是C/C++中的一個循環語句,特色是:編程
至少執行一次循環體;
在循環的尾部進行結束條件的判斷。緩存
其實do-while還能夠用在其餘一些場合中,很是巧妙的處理你的一些難題,好比:安全
在宏定義中寫複雜的語句;
在函數體中停止代碼段的處理。服務器
好像有點抽象,那咱們就來具體一些,經過代碼來聊聊這些用法。socket
也強烈建議您在日常的項目中把這些小技巧用起來,模仿是第一步,先僵化-再優化-最後固化,這是提升編程能力的最有效方法。
時間久了,用的多了,這些東西就是屬於你的。tcp
// 目的:把兩個參數分別自增一下 #define OPT(a, b) a++; b++; int main(int argc, char *argv[]) { int i = 1; int j = 1; OPT(i, j); printf("i = %d, j = %d \n", i, j); return 0; }
測試一下,結果沒有問題(代碼的目的就是讓i和j這個2個變量都自增1):函數
i = 2, j = 2學習
並且OPT(i, j);
中,最後的分號還能夠省略,編譯和結果都沒有問題。測試
可是估計沒有誰會在項目中這麼使用宏吧?!看一下下面這個例子:
在調用OPT宏的外層添加一個if條件判斷:
#define OPT(a, b) a++; b++; int main(int argc, char *argv[]) { int i = 1; int j = 1; if(0) OPT(i, j); printf("i = %d, j = %d \n", i, j); return 0; }
打印結果是:
i = 1, j = 2
問題出現了:咱們的本意是if條件爲假,這2個變量都不要自增,可是輸出結果倒是:第二個參數自增了。
其實問題很明顯,把宏擴展開就一目瞭然了。
if(0) a++; b++;
錯誤緣由一目瞭然:因爲if語句沒有用大括號{}把須要執行的代碼所有包裹住,致使只有a++;
語句是在if
語句的控制範圍,而b++;
語句不管如何都被執行了。
也許你會說,這個簡單,使用if
時,必須加上大括號{}。道理是沒錯,若是這個宏定義只有你本身使用,這不成問題。可是若是宏定義是你寫的,而使用者是你的同事,那麼你怎麼要求別人必須按照你所規定的格式來編碼?畢竟每一個人的習慣是不同的。
不少時候,要求別人是不現實的。更有效的方法是優化本身的輸出,提供更安全的代碼,讓別人想犯錯誤都沒機會。
怎麼作才能更安全?更通用呢?使用do-while!
#define OPT(a, b) do{a++;b++;}while(0)
也就是說,只要宏定義中存在多條語句,就能夠用do-while把這些語句所有包裹起來,這樣不管怎麼使用這個宏,都不會有問題。
例如:
if(0) OPT(i, j);
宏擴展以後代碼爲:
if(0) do { a++; b++; }while(0);
若是給if加上大括號,視覺上會更好一些:
if(0) { OPT(i, j); }
宏擴展以後代碼爲:
if(0) { do { a++; b++; }while(0); }
能夠看到,不管是否加上大括號{},從語法和語義上都不存在問題。
這裏還有一個小細節能夠留意一下:OPT(i,j);
這行代碼中,尾部是加了分號的。
若是沒有加分號,那麼宏擴展以後代碼爲:
if(0) do { a++; b++; }while(0) // 注意:這裏沒有分號
由於while(0)沒有分號,因此編譯會出錯。爲了避免對宏的使用者提出要求,能夠在宏的最後加一個分號便可,以下:
#define OPT(a, b) do{a++;b++;}while(0);
小結:使用do-while語句來包裹宏定義中的多行語句,解決了宏定義的安全問題。
可是,任何事情都不多是完美的,例如:在宏定義中使用do-while就沒法返回一個結果。
也就是說:若是咱們須要從宏定義中返回一個結果,那麼do-while就派不上用場了。那應該怎麼辦?
若是宏定義須要返回一個結果,最好的方式就是:使用({...})
把宏定義中的多行語句包裹起來。以下:
#define ADD(a, b, c) ({ ++a; ++b; c=a+b; }) int i = 1; int j = 2; int k; printf("k = %d \n", ADD(i, j, k));
下面這張圖來自GNU官方文檔:
翻譯過來就是:
GNU C中,在圓括號()中寫複雜語句是合法的,這樣你就能夠在一個表達式中使用循環、switch、局部變量了。
什麼是複雜語句呢?就是被大括號{}包裹的多行語句。
在上面的實例中,圓括號要放在大括號的外層。
使用({...})
定義宏,由於是多行語句,能夠返回一個結果,比do-while更勝一籌。
這裏既然提到了在宏定義中使用局部變量,那咱們再提供一個小技巧來提升代碼的執行效率。
看一下這個宏定義:
#define max(a,b) ({ (a) > (b) ? (a) : (b) }) float i = 1.234; float j = 4.321; float max = max((i / 0.8 + 5) / 3, (j * 0.8) / 1.5);
宏擴展以後, a或者b中,確定有一個被計算2次。固然,這裏的示例比較簡單,體現不出差距。若是是對時間要求特別苛刻的場合,計算量又很大,那麼這個宏中因爲兩次計算所耗費的時間就必須考慮了,那應該如何優化呢?使用局部變量!
#define max(a,b) ({ int _a = (a), _b = (b); _a > _b ? _a : _b; })
經過增長局部變量_a和_b來緩存計算結果,就消除了2次計算的問題。
這個例子還能夠再繼續優化,這裏的局部變量類型是int,這是寫死的,只能比較兩個整型的變量。若是寫成這樣:
#define max(a, b) ({ typeof(a) _a = (a), _b = (b); _a > _b ? _a : _b; })
也就是用typeof來動態獲取比較變量的類型,這樣的話,任何數值類型的變量均可以使用這個宏了。
關於typeof的說明,請看GNU的這張圖,在文末的參考連接中,能夠看到更加詳細的官方說明。
先來看2段代碼。
char *get_error_msg(int err_code) { if (1 == err_code) { return "invalid name"; } else if (2 == err_code) { return "invalid password"; } else if (3 == err_code) { return "network error"; } return "unkown error"; }
思考:一個設計良好的函數只有一個出口,也就是return語句,可是這個函數有這麼多的return語句,是否是顯得很亂?示例代碼體積很小,彷佛沒有感受。可是上百行的函數在項目中仍是比較常見的,在這種狀況下若是給你來個十幾個return語句,你會不會想把寫代碼的那個傢伙拎過來扇幾巴掌?
void connect_server(char *ip, int port) { int ret, sockfd; sockfd = socket(...); if (sockfd < 0) { printf("socket create failed! \n"); goto end; } ret = connect(sockfd, ...); if (ret < 0) { printf("connect failed! \n"); goto end; } ret = send(sockfd, ...) if (ret < 0) { printf("send failed! \n"); goto end; } end: 其餘代碼 }
思考:TCP socket編程中,須要按照固定的順序調用多個系統函數。這段代碼中調用系統函數後,對結果進行了檢查,這是很是好的習慣。若是在某個調用中發生錯誤,須要停止後面的操做,進行錯誤處理。雖然C語言中不由止goto語句的使用,可是看到這麼多的goto,難道就沒有美觀、更優雅的作法嗎?
總結一下上面這2段代碼,它們共同的特色是:
停止執行,咱們首先想到的就是break關鍵字,它主要用在循環和switch語句中。do-while循環語句首先執行循環體,在尾部才進行循環的判斷。 那麼就能夠利用這一點來解決這2段代碼面對的問題。在一連串的語句中,只須要執行一部分的語句,也就是從代碼塊的某個中間位置停止執行。
char *get_error_msg(int err_code) { char *msg; do { if (1 == err_code) { msg = "invalid name"; break; } else if (2 == err_code) { msg = "invalid password"; break; } else if (3 == err_code) { msg = "network error"; break; } else { msg = "unkown error"; break; } }while(0); return msg; }
void connect_server(char *ip, int port) { int ret, sockfd; do { sockfd = socket(...); if (sockfd < 0) { printf("socket create failed! \n"); break; } ret = connect(sockfd, ...); if (ret < 0) { printf("connect failed! \n"); break; } ret = send(sockfd, ...) if (ret < 0) { printf("send failed! \n"); break; } }while(0); 其餘代碼 }這樣的代碼,是否是看起來順眼多了?
do-while的主要做用是循環處理,可是在這篇文章中,咱們利用的點並非循環功能,而是代碼塊的包裹和停止執行的功能。這些細小的點在一些牛逼的開源代碼中很常見,看到了咱們就要學習、模仿、使用,用的多了它就是你的了!
是否是開始喜歡上do-while語句了?
參考文檔:
[1] https://gcc.gnu.org/onlinedocs/gcc/Typeof.html
[2] https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html
[3] https://stackoverflow.com/questions/9495962/why-use-do-while-0-in-macro-definition
[4] https://gcc.gnu.org/onlinedocs/gcc-6.2.0/gcc/Statement-Exprs.html#Statement-Exprs
若是以爲文章不錯,請轉發、分享給您的朋友。
我會把十多年嵌入式開發中的項目實戰經驗進行總結、分享,相信不會讓你失望的!
<
推薦閱讀
[1] 原來gdb的底層調試原理這麼簡單
[2] 生產者和消費者模式中的雙緩衝技術
[3] 深刻LUA腳本語言,讓你完全明白調試原理
[4] 一步步分析-如何用C實現面向對象編程