C/C++預處理指令和宏定義#define及 do while(0)

本文主要寫了預處理指定和#define 宏替換及宏函數,以及爲什麼會用到 do/while(0); 讀者需要那部分知識可以直接點擊目錄裏面的鏈接

本文參考:https://www.cnblogs.com/flowingwind/p/8304668.html
https://www.cnblogs.com/bytebee/p/8205707.html
https://www.cnblogs.com/wuweierzhi/p/11591999.html

目錄

一、預編譯指令

二、宏定義

三、do while(0);


一、預編譯指令

常見的預編譯指令
命令 命令效果
#空指令 無任何效果
#include 包含一個源代碼文件
#define 定義宏
#undef 取消已定義的宏
#if 如果給定條件爲真,則編譯下面代碼
#ifdef 如果宏已經定義,則編譯下面代碼
#ifndef 如果宏沒有定義,則編譯下面代碼
#elif 如果前面的#if給定條件不爲真,當前條件爲真,則編譯下面代碼
#endif 結束一個#if……#else條件編譯塊
#error 停止編譯並顯示錯誤信息

   條件編譯命令最常見的形式爲:

  • #ifdef 標識符 
  • 程序段1 
  • #else 
  • 程序段2 
  • #endif

例:

#ifndef bool
#define ture 1
#define false 0
#endif

在早期vc中bool變量用1,0表示,即可以這麼定義,保證程序的兼容性

在頭文件中使用#ifdef和#ifndef是非常重要的,可以防止雙重定義的錯誤。

//main.cpp文件

#include "cput.h"
#include "put.h"
int main()
{
    cput();
    put();
    cout << "Hello World!" << endl;
    return 0;
}
//cput.h 頭文件

#include <iostream>
using namespace std;
int cput()
{
    cout << "Hello World!" << endl;
    return 0;
}
//put.h頭文件

#include "cput.h"
int put()
{
    cput();
    return 0;
}

編譯出錯;在main.cpp中兩次包含了cput.h

嘗試模擬還原編譯過程;

當編譯器編譯main.cpp時

//預編譯先將頭文件展開加載到main.cpp文件中

//展開#include "cput.h"內容
#include <iostream>
using namespace std;
int cput()
{
    cout << "Hello World!" << endl;
    return 0;
}

//展開#include "put.h"內容
//put.h包含了cput.h先展開
#include <iostream>
using namespace std;
int cput()
{
    cout << "Hello World!" << endl;
    return 0;
}
int put()
{
    cput();
    return 0;
}

int main()
{
    cput();
    put();
    cout << "Hello World!" << endl;
    return 0;
}

很明顯合併展開後的代碼,定義了兩次cput()函數;

如果將cput.h改成下面形式:

#ifndef _CPUT_H_
#define _CPUT_H_
#include <iostream>
using namespace std;
int cput()
{
    cout << "Hello World!" << endl;
    return 0;
}
#endif

當編譯器編譯main.cpp時合併後的main.cpp文件將會是這樣的:

#ifndef _CPUT_H_
#define _CPUT_H_
#include <iostream>
using namespace std;
int cput()
{
    cout << "Hello World!" << endl;
    return 0;
}
#endif

#ifndef _CPUT_H_
#define _CPUT_H_
#include <iostream>
using namespace std;
int cput()
{
    cout << "Hello World!" << endl;
    return 0;
}
#endif
int put()
{
    cput();
    return 0;
}

int main()
{
    cput();
    put();
    cout << "Hello World!" << endl;
    return 0;
}

 這次編譯通過運行成功;因爲在展開put.h中包含的cput.h,會不生效,前面已經定義了宏_CPUT_H_

 

二、宏定義

#define 宏定義,簡單理解就是直接替換。

宏定義可以幫助我們防止出錯,提高代碼的可移植性和可讀性等。
  在軟件開發過程中,經常有一些常用或者通用的功能或者代碼段,這些功能既可以寫成函數,也可以封裝成爲宏定義。那麼究竟是用函數好,還是宏定義好?這就要求我們對二者進行合理的取捨。
  我們來看一個例子,比較兩個數或者表達式大小,首先我們把它寫成宏定義:

#define MAX(a, b) ((a)>(b) ? (a):(b))

其次,把它用函數來實現:

int max(int a, int b)
{
  return ((a > b) ? a : b)
}

很顯然,我們不會選擇用函數來完成這個任務,原因有兩個:

1.函數調用會帶來額外的開銷,它需要開闢一片棧空間,記錄返回地址,將形參壓棧,從函數返回還要釋放堆棧。這種開銷不僅會降低代碼效率,而且代碼量也會大大增加,而使用宏定義則在代碼規模和速度方面都比函數更勝一籌;

2.函數的參數必須被聲明爲一種特定的類型,所以它只能在類型合適的表達式上使用,我們如果要比較兩個浮點型的大小,就不得不再寫一個專門針對浮點型的比較函數。反之,上面的那個宏定義可以用於整形、長整形、單浮點型、雙浮點型以及其他任何可以用“>”操作符比較值大小的類型,也就是說,宏是與類型無關的。

和使用函數相比,使用宏的不利之處在於每次使用宏時,一份宏定義代碼的拷貝都會插入到程序中。除非宏非常短,否則使用宏會大幅度增加程序的長度。

還有一些任務根本無法用函數實現,但是用宏定義卻很好實現。比如參數類型沒法作爲參數傳遞給函數,但是可以把參數類型傳遞給帶參的宏。看下面的例子:

/* \ 表示換行符*/
#define MALLOC(n, type) \ 
    ((type *) malloc((n) * sizeof(type))   \*強制類型轉換*\

利用這個宏,我們就可以爲任何類型分配一段我們指定的空間大小,並返回指向這段空間的指針。我們可以觀察一下這個宏確切的工作過程:

int *ptr;
  ptr = MALLOC(5, int);

將這宏展開以後的結果:

ptr = (int *)malloc((5) * sizeof(int));

這個例子是宏定義的經典應用之一,完成了函數不能完成的功能,但是宏定義也不能濫用,通常,如果相同的代碼需要出現在程序的幾個地方,更好的方法是把它實現爲一個函數。

example:

define的單行定義:

#define maxi(a,b) ((a>b) ? a:b)

define的多行定義

define可以替代多行的代碼,例如MFC中的宏定義:

#define MACRO(arg1, arg2) do{\
                             \
    stmt1;                   \
    stmt2;                   \
                             \
}while(0)  

宏定義寫出swap(x,y)交換函數

#define swap(x, y)\
x = x + y;        \
y = x - y;        \
x = x - y;

zigbee裏多行define有如下例子

#define FillAndSendTxOptions(TRANSSEQ, ADDR, ID, LEN, TxO ){ \
afStatus_t stat;                                             \
ZDP_TxOptions = (TxO);                                       \
stat = fillAndSend( (TRANSSEQ), (ADDR), (ID), (LEN) );       \
ZDP_TxOptions = AF_TX_OPTIONS_NONE;                          \
return stat;                                                 \
}

 

三、do while(0);

1.幫助定義複雜的宏以避免錯誤

舉例來說,假設你需要定義這樣一個宏:

#define DOSOMETHING(x) foo1(x); foo2(x)

有如下調用語句

DOSOMETHING(value);

這時將宏展開爲:

fool(value); fool(value);

但是如果你在調用的時候這麼寫:

if(a>0)
    DOSOMETHING(value);

因爲宏在預處理的時候會直接被展開,你實際上寫的代碼是這個樣子的:

if(a>0)
    foo1(value);
foo2(value);

這就出現了問題,因爲無論a是否大於0,foo2(value)都會被執行,導致程序出錯。
那麼僅僅使用{}將foo1(x)和foo2(x)包起來行麼?比如:

#define DOSOMETHING(x) { foo1(x); foo2(x); }

我們在寫代碼的時候都習慣在語句右面加上分號,如果在宏中使用{},代碼編譯展開後宏就相當於這樣寫了:“{...};”,展開後就是這個樣子:

if(a>0)
{
    foo1();foo2();
};

很明顯,這是一個語法錯誤(大括號後多了一個分號)。
現在的編譯器會自動檢測自動忽略分號,不會報錯,但是我們還是希望能跑在老的編譯器上。

在沒有do/while(0)的情況下,在所有可能情況下,期望我們寫的多語句宏總能有正確的表現幾乎是不可能的。
如果我們使用do{...}while(0)來定義宏,即:

#define DOSOMETHING(x) do{foo1(x); foo2(x);} while(0)  //注意這裏沒有分號

對於上面的if語句,將會被擴展爲:

if(a > 0)
    do{foo1(x); foo2(x);} while(0);

這樣,宏被展開後,上面的調用語句纔會保留初始的語義。do能確保大括號裏的邏輯能被執行,而while(0)能確保該邏輯只被執行一次,就像沒有循環語句一樣。

總結:在Linux和其它代碼庫裏的,很多宏實現都使用do/while(0)來包裹他們的邏輯,這樣不管在調用代碼中怎麼使用分號和大括號,而該宏總能確保其行爲是一致的。
cocos2d-x中大量使用了這種宏定義:

#define CC_SAFE_DELETE(p) do { if(p) { delete (p); (p) = 0; } } while(0)

2. 避免使用goto控制程序流(和宏定義無關,屬於代碼優化)

在一些函數中,我們可能需要在return語句之前做一些清理工作,比如釋放在函數開始處由malloc申請的內存空間,使用goto總是一種簡單的方法:

int foo()
{
    somestruct *ptr = malloc(...);

    dosomething...;
    if(error)
        goto END;
    dosomething...;
    if(error)
        goto END;
    dosomething...;
END:
    free(ptr);
    return 0;
}

但由於goto不符合軟件工程的結構化,而且有可能使得代碼難懂,所以很多人都不倡導使用,這個時候我們可以使用do{...}while(0)來做同樣的事情:

int foo()
{
    somestruct *ptr = malloc(...);
    do
    {
        dosomething...;
        if(error)
            break;
        dosomething...;
        if(error)
            break;
        dosomething...;
    }
    while(0);      //注意這裏有;分號

    free(ptr);
    return 0;
}

這裏將函數主體部分使用do{...}while(0)包含起來,使用break來代替goto,後續的清理工作在while之後,現在既能達到同樣的效果,而且代碼的可讀性、可維護性都要比上面的goto代碼好的多了。

我經常使用這個種技能在Lua裏,Lua不支持do{...}while(0)語法,但是Lua有一種類似的語法repeat...until,僞代碼如下:

repeat
  dosomething...
  if error then
    break;
  end
  dosomething...;
  if error then
    break;
  end      
  dosomething...;
until (1);
print("break repeat");

這樣和do{...}while(0)一樣,也保證了只執行一次,可以用break調出循環。

3. 避免由宏引起的警告

內核中由於不同架構的限制,很多時候會用到空宏,。在編譯的時候,這些空宏會給出warning,爲了避免這樣的warning,我們可以使用do{...}while(0)來定義空宏:

#define EMPTYMICRO do{}while(0)

這種情況不太常見,因爲有很多編譯器,已經支持空宏。

4. 定義單一的函數塊來完成複雜的操作

如果你有一個複雜的函數,變量很多,而且你不想要增加新的函數,可以使用do{...}while(0),將你的代碼寫在裏面,裏面可以定義變量而不用考慮變量名會同函數之前或者之後的重複。
但是我不建議這樣做,儘量聲明不同的變量名,以便於後續開發人員閱讀。

int key;
string value;
int func()
{
    int key = GetKey();
    string value = GetValue();
    dosomething for key,value;
    do{
        int key;string value;
        dosomething for this key,value;
    }while(0);    
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章