宏的妙用

[TOC]

變長(zhǎng)數(shù)組

? 嚴(yán)格說(shuō)來(lái),變長(zhǎng)數(shù)組的實(shí)現(xiàn)在c++中并不是一件麻煩的事情。Stl中的vector本身就是一個(gè)變長(zhǎng)數(shù)組,并且有自動(dòng)管理內(nèi)存的能力。但是在c中,實(shí)現(xiàn)變長(zhǎng)數(shù)組就稍顯麻煩。用C實(shí)現(xiàn),必然需要一個(gè)結(jié)構(gòu),結(jié)構(gòu)當(dāng)中應(yīng)當(dāng)有一個(gè)指針,指針?lè)峙湟欢蝺?nèi)存空間,空間大小根據(jù)需要而定,而且必須有另外一個(gè)字段記錄究竟開(kāi)辟了多大多長(zhǎng)的空間。

大致描述如下:

struct MutableLenArray
{
  Int count;
  Char *p;
}
P = new Char[Count];

沒(méi)什么問(wèn)題,但是C語(yǔ)言的使用者有個(gè)最大的自豪就在于對(duì)于效率、空間使用的掌控。他們會(huì)有這樣的疑問(wèn),如果count=0,那么p就沒(méi)必要了,白白占了4(64位系統(tǒng)為8)個(gè)字節(jié)的空間,簡(jiǎn)直浪費(fèi)。 那有沒(méi)有更好的方式能實(shí)現(xiàn)上面的需求,又保證空間合理呢?答案是有的,用0長(zhǎng)度。如下:

Struct MutableLenArray 
{ 
    Int count; 
    Char p[0]; 
};

和上面的結(jié)構(gòu)使用方法一致,但是我們可以用sizeof嘗試讀取其大小,發(fā)現(xiàn)竟然只有count字段的長(zhǎng)度4字節(jié),p沒(méi)有被分配空間。完美!

宏的妙用

1. do{...}while(0)

在很多的C程序中,你可能會(huì)看到許多看起來(lái)不是那么直接的較特殊的宏定義。下面就是一個(gè)例子:

#define __set_task_state(tsk,state_value)    \
    do{(tsk)->state = (state_value); }while(0)

在Linux內(nèi)核和其它一些著名的C庫(kù)中有許多使用do{...}while(0)的宏定義。這種宏的用途是什么?有什么好處?Google的Robert Love(先前從事Linux內(nèi)核開(kāi)發(fā))給我們解答如下:

do{...}while(0)是C中唯一的構(gòu)造程序,讓你定義的宏總是以相同的方式工作,不管怎樣使用宏(尤其是沒(méi)有使用大括號(hào)包圍調(diào)用宏的語(yǔ)句),宏后面的分號(hào)也是相同的效果。

這句話聽(tīng)起來(lái)可能有些拗口,其實(shí)用一句話概括就是:使用do{...}while(0)構(gòu)造后的宏定義不會(huì)受到大括號(hào)、分號(hào)等的影響,總是會(huì)按你期望的方式調(diào)用運(yùn)行。同時(shí)因?yàn)榻^大多數(shù)的編譯器能夠識(shí)別do{...}while(0) 這種無(wú)用的循環(huán)并進(jìn)行優(yōu)化,所以使用這個(gè)方法并不會(huì)導(dǎo)致程序的性能降低。

例如:

#define foo(x) bar(x); baz(x)

然后你可能這樣調(diào)用:

foo(wolf);

這將被宏擴(kuò)展為:

bar(wolf); baz(wolf);

這的確是我們期望的正確輸出。下面看看如果我們這樣調(diào)用:

if (!feral)
    foo(wolf);

那么擴(kuò)展后可能就不是你所期望的結(jié)果。上面語(yǔ)句將擴(kuò)展為:

if (!feral)
    bar(wolf);
baz(wolf);

顯而易見(jiàn),這是錯(cuò)誤的,也是大家經(jīng)常易犯的錯(cuò)誤之一。

使用do{...}while(0)消除goto語(yǔ)句

? 通常,如果在一個(gè)函數(shù)中開(kāi)始要分配一些資源,然后在中途執(zhí)行過(guò)程中如果遇到錯(cuò)誤則退出函數(shù),當(dāng)然,退出前先釋放資源,我們的代碼可能是這樣:

bool Execute()
{
   // 分配資源
   int *p = new int;
   bool bOk(true);

   // 執(zhí)行并進(jìn)行錯(cuò)誤處理
   bOk = func1();
   if(!bOk) 
   {
      delete p;   
      p = NULL;
      return false;
   }

   bOk = func2();
   if(!bOk) 
   {
      delete p;   
      p = NULL;
      return false;
   }

   bOk = func3();
   if(!bOk) 
   {
      delete p;   
      p = NULL;
      return false;
   }
   // ..........
   // 執(zhí)行成功,釋放資源并返回
    delete p;   
    p = NULL;
    return true;
}

這里一個(gè)最大的問(wèn)題就是代碼的冗余,而且我每增加一個(gè)操作,就需要做相應(yīng)的錯(cuò)誤處理,非常不靈活。于是我們想到了goto:

bool Execute()
{
   // 分配資源
   int *p = new int;
   bool bOk(true);

   // 執(zhí)行并進(jìn)行錯(cuò)誤處理
   bOk = func1();
   if(!bOk) goto errorhandle;

   bOk = func2();
   if(!bOk) goto errorhandle;

   bOk = func3();
   if(!bOk) goto errorhandle;
   // ..........
   // 執(zhí)行成功,釋放資源并返回
    delete p;   
    p = NULL;
    return true;

errorhandle:
    delete p;   
    p = NULL;
    return false;
}

代碼冗余是消除了,但是我們引入了C++中身份比較微妙的goto語(yǔ)句,雖然正確的使用goto可以大大提高程序的靈活性與簡(jiǎn)潔性,但太靈活的東西往往是很危險(xiǎn)的,它會(huì)讓我們的程序捉摸不定,那么怎么才能避免使用goto語(yǔ)句,又能消除代碼冗余呢,請(qǐng)看do...while(0)循環(huán):

bool Execute()
{
   // 分配資源
   int *p = new int;
   bool bOk(true);
   do
   {
      // 執(zhí)行并進(jìn)行錯(cuò)誤處理
      bOk = func1();
      if(!bOk) break;

      bOk = func2();
      if(!bOk) break;

      bOk = func3();
      if(!bOk) break;
      // ..........
   }while(0);

    // 釋放資源
    delete p;   
    p = NULL;
    return bOk;
   
}

2. “#”符號(hào)

"#"符號(hào)用來(lái)將一個(gè)符號(hào)直接轉(zhuǎn)換為為字符串,例如

#define TO_STRING(x) #x
const char * str=TO_STRING(test);

str指向的內(nèi)容就是"test",也就是說(shuō)宏定義中#會(huì)把其后的符號(hào)直接加上雙引號(hào)。這個(gè)特性為C++的反射的實(shí)現(xiàn)提供了極大的方便。

3. "##"符號(hào)

用來(lái)連接兩個(gè)符號(hào),從而產(chǎn)出新的詞法(詞法層次),例如:

#define SIGN(x) INT_##x
int SIGN(1);

宏被展開(kāi)后將會(huì)成為:int INT_1;
可以把##看成是連字符,連字符為新符號(hào)的產(chǎn)生提供了方便。google的Gtest框架就巧妙地運(yùn)用了連字符來(lái)生成新的測(cè)試案例。

4. 變參宏

#define LOG(format,...) printf(format,__VA_ARGS__)
LOG("%s %d",str,count);
#define LogE(format, ...) usb_logWrite('e', __FILE__, __LINE__, format, ##__VA_ARGS__)
//此處使用__VA_ARGS__或者是 ##__VA_ARGS__均可
#define LogD(format, ...) usb_logWrite('d', __FILE__, __LINE__, format, ##__VA_ARGS__)

#define LogI(format, ...) usb_logWrite('i', __FILE__, __LINE__, format, ##__VA_ARGS__)

#define LogW(format, ...) usb_logWrite('w', __FILE__, __LINE__, format, ##__VA_ARGS__)

#define LogO(format, ...) usb_logWrite('o', __FILE__, __LINE__, format, ##__VA_ARGS__)

extern int usb_logWrite(char level,char *fileName, int lineNum, const char * format, ...);
int usb_logWrite(char level,char *fileName, int lineNum, const char * format, ...)
{
    int n, m;
    char logWriteBuf[BUF_SIZE];

    _init_fd();

    if(fileName != NULL) {
        char *rf = strrchr(fileName, '/');
        if(rf != NULL) fileName = rf+1;
    }
    n = snprintf(logWriteBuf,BUF_SIZE,"%%%%<L=%c;PN=%d;P=%s;F=%s;N=%d;>",\
                                    level,Id,name,fileName,lineNum);

    va_list argList;
    va_start(argList,format);
    m = vsnprintf(logWriteBuf+n,BUF_SIZE-n,format,argList);
    va_end(argList);

    if(m <= 0) {
        m = snprintf(logWriteBuf+n,BUF_SIZE-n, "format error\n");
    }
    n += m;
    if(n > BUF_SIZE-2) n = BUF_SIZE-2;
    logWriteBuf[n++] = '#';
    logWriteBuf[n++] = '#';

    write(log_fd, logWriteBuf, n);
    return n;
}

_VA_ARGS_是系統(tǒng)預(yù)定義的宏,被自動(dòng)的替換為參數(shù)列表,經(jīng)常需要用到格式化輸出,重定義時(shí),可以使用以上技巧。

C中一些特殊的宏還有__FUNCTION__、__DATE__、__FILE__、__LINE__、__STDC__、__TIME__、__TIMESTAMP__

5. 宏參數(shù)的prescan

? prescan的定義為:當(dāng)一個(gè)宏參數(shù)被放進(jìn)宏體當(dāng)中時(shí),這個(gè)宏參數(shù)會(huì)首先被全部展開(kāi),當(dāng)展開(kāi)以后的宏參數(shù)放入宏體時(shí),預(yù)處理器對(duì)新展開(kāi)的宏體進(jìn)行二次掃描,并繼續(xù)展開(kāi)。

#define PARAM(x) x
#define ADDPARAM(x) INT_##x
PARAM(ADDPARAM(1))

因?yàn)锳DDPARAM(1)是作為PARAM的宏參數(shù),所以先將ADDPARAGM(1)展開(kāi)為INT_1,然后再將INT_1放進(jìn)PARAM,例外的情況是如果PARAM宏里面對(duì)宏參數(shù)使用了"#"或者是"##",那么宏參數(shù)不會(huì)被展開(kāi)。

#define PARAM( x ) #x 
#define ADDPARAM( x ) INT_##x

PARAM( ADDPARAM( 1 ) ); 將被展開(kāi)為ADDPARAM( 1 )。 所以此時(shí)要得到"INT_1"的結(jié)果,必須要加入一個(gè)中間宏:

#define PARAM(x) PARAM1(x)
#define PARAM1( x ) #x

PARAM( ADDPARAM( 1 ) );此時(shí)的結(jié)果將會(huì)是“INT_1”。根據(jù)prescan原則,當(dāng)ADDPARAM(1)傳入,會(huì)展開(kāi)得到INT_1,然后將INT_1帶入PARAM1宏,最終得到“INT_1”的結(jié)果。

6. C++中的接口宏

C++的目標(biāo)之一就是把類的聲明和定義分離開(kāi)來(lái),這對(duì)于項(xiàng)目的開(kāi)發(fā)極其有利——這可以使開(kāi)發(fā)人員不用看到類的實(shí)現(xiàn)就能知曉類的功能。但是,C++實(shí)現(xiàn)類的聲明與類定義的分離的方法會(huì)導(dǎo)致一些額外的工作——每個(gè)非內(nèi)聯(lián)函數(shù)的表示都需要寫兩次,一次在類聲明中,一次在類定義中。 代碼如下:

// .h File 
class Element 
{ 
void Tick (); 
};
 
// .cpp File 
void Element ::Tick () 
{ 
// todo 
}

由于Tick的標(biāo)識(shí)在兩個(gè)地方都出現(xiàn)了,因此如果我們需要改變這個(gè)方法的參數(shù)的時(shí)候(改變函數(shù)名、返回類型或者加const),我們需要改變兩個(gè)地方。
當(dāng)然通常這沒(méi)有什么工作量,但是有些情況下這個(gè)特性會(huì)帶來(lái)不少麻煩。
舉個(gè)例子,如果我們有一個(gè)叫做BaseClass的基類,有三個(gè)從BaseClass繼承而來(lái)的子類——D1、D2和D3.其中BaseClass聲明了一個(gè)虛函數(shù)Foo()并且有一個(gè)缺省實(shí)現(xiàn),并且D1、D2、D3中重載了Foo()函數(shù)。
現(xiàn)在,如果說(shuō)我們給BaseClass::Foo()添加一個(gè)參數(shù),但是忘了給D3中做相應(yīng)的修改。麻煩來(lái)了——編譯可以通過(guò),編譯器會(huì)把BaseClass::Foo(…)和D3::Foo()當(dāng)成兩個(gè)完全不同的函數(shù)。當(dāng)我們想通過(guò)虛函數(shù)機(jī)制來(lái)調(diào)用D3的Foo的時(shí)候,這就容易出一些問(wèn)題。
UE4中光繼承自AActor類的類就有上千個(gè),如果需要對(duì)AActor類做一個(gè)修改,那么如果使用傳統(tǒng)方法,我們還要針對(duì)上千個(gè)派生類進(jìn)行修改,而且萬(wàn)一有一個(gè)派生類沒(méi)有修改,編譯器也不會(huì)報(bào)錯(cuò)!
這么看來(lái),理想的情況是我們希望一個(gè)函數(shù)的表示只在一個(gè)地方存在,如果說(shuō)只聲明BaseClass::Foo()一次,然后再它的派生類中不用再額外聲明Foo就好了。
而且在效率方面來(lái)說(shuō),在C++中使用繼承的時(shí)候我們經(jīng)常會(huì)使用很多淺層次的類繼承關(guān)系,一個(gè)父類往往有一堆子類。很多時(shí)候我們只需要把很多互不相關(guān)的功能集成到一個(gè)單獨(dú)的類繼承家族里面。
對(duì)于淺繼承來(lái)說(shuō),我們只是把開(kāi)始的父類聲明為一個(gè)接口——也就是說(shuō)它聲明了一些虛函數(shù)(大部分是純虛函數(shù))。在大多數(shù)情況下,我們會(huì)在這個(gè)類家族里面有一個(gè)基類以及其余的派生類。
如果說(shuō)我們的基類有10個(gè)函數(shù),我們從這個(gè)基類派生了20個(gè)類,那么我們就需要額外做200個(gè)函數(shù)聲明。但是這些聲明的目的往往只是為了Implement基類中的那些方法而已,這就或多或少的容易使得頭文件不好維護(hù)。
傳統(tǒng)方法的實(shí)現(xiàn)
如果說(shuō)我們有一個(gè)Animal的類,這個(gè)類被視為基類,我們希望從這個(gè)基類派生出不同的子類。在Animal中有3個(gè)純需函數(shù),如下所示:

class Animal 
{ 
public: 
virtual std :: string GetName () const = 0 ; 
virtual Vector3f GetPosition () const = 0; 
virtual Vector3f GetVelocity () const = 0; 
};

同時(shí),這個(gè)基類擁有三個(gè)派生類——Monkey,Tiger,Lion。
那么我們?nèi)齻€(gè)方法的每一個(gè)都會(huì)在7個(gè)地方存在:Animal中一次,Monkey、Lion、Tiget的聲明和定義各一次。
然后假設(shè)我們做一個(gè)小改動(dòng)——我們想將GetPosition和GetVelocity的返回類型改為Vector4f以適應(yīng)Transform變換,那么我們就要在7個(gè)地方進(jìn)行修改:Animal的.h文件,Lion、Tiger和Monkey的.h文件和.cpp文件。
使用宏的實(shí)現(xiàn)
有一種很妙的處理方法就是將這些方法進(jìn)行包裝,改成所謂接口宏的形式。我們可以試試看:

#define INTERFACE_ANIMAL(terminal)             \
public:                           \
  virtual std::string GetName() const ##terminal     \
  virtual IntVector GetPosition() const ##terminal    \
  virtual IntVector GetVelocity() const ##terminal    
 
#define BASE_ANIMAL   INTERFACE_ANIMAL(=0;)
#define DERIVED_ANIMAL INTERFACE_ANIMAL(;)

值得一提的是,##符號(hào)代表的是連接,\符號(hào)代表的是把下一行的連起來(lái)。
通過(guò)這些宏,我們就可以大大簡(jiǎn)化Animal的聲明,還有所有從它派生的類的聲明了:

// Animal.h
class Animal
{
  BASE_ANIMAL ;
};
 
 
 
// Monkey.h
class Monkey : public Animal
{
  DERIVED_ANIMAL ;
};
 
 
// Lion.h
class Lion : public Animal
{
  DERIVED_ANIMAL ;
};
 
 
// Tiger.h
class Tiger : public Animal
{
  DERIVED_ANIMAL ;
};
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容