你只用do-while來實現循環?太浪費了!


這是道哥的第010篇原創

前言

這篇文章講解的知識點很小,可是在一些編程場合中很是適用,你們能夠把這篇短文當作甜品來品味一下。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語句,你會不會想把寫代碼的那個傢伙拎過來扇幾巴掌?

函數功能:經過TCP Socket鏈接服務器

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段代碼面對的問題。

解決多個return的問題

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;
}

解決goto的問題

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


【原創聲明】

> 做者:道哥(公衆號: IOT物聯網小鎮)
> 知乎:道哥
> B站:道哥分享
> 掘金:道哥分享
> CSDN:道哥分享

若是以爲文章不錯,請轉發、分享給您的朋友。


我會把十多年嵌入式開發中的項目實戰經驗進行總結、分享,相信不會讓你失望的!

轉載:歡迎轉載,但未經做者贊成,必須保留此段聲明,必須在文章中給出原文鏈接。
長按下圖二維碼關注,每篇文章都有乾貨。




<
推薦閱讀

[1] 原來gdb的底層調試原理這麼簡單
[2] 生產者和消費者模式中的雙緩衝技術
[3] 深刻LUA腳本語言,讓你完全明白調試原理
[4] 一步步分析-如何用C實現面向對象編程

相關文章
相關標籤/搜索