訂閱
糾錯(cuò)
加入自媒體

如何提高代碼逼格?宏定義-從入門(mén)到放棄

三、宏擴(kuò)展

所謂的宏擴(kuò)展就是代碼替換,這部分內(nèi)容也是我想表達(dá)的主要內(nèi)容。宏擴(kuò)展最大的好處有如下幾點(diǎn):

減少重復(fù)的代碼;完成一些通過(guò) C 語(yǔ)法無(wú)法實(shí)現(xiàn)的功能(字符串拼接);動(dòng)態(tài)定義數(shù)據(jù)類(lèi)型,實(shí)現(xiàn)類(lèi)似 C++ 中模板的功能;程序更容易理解、修改(例如:數(shù)字、字符串常亮);

我們?cè)趯?xiě)代碼的時(shí)候,所有使用宏名稱(chēng)的地方,都可以理解為一個(gè)占位符。在編譯程序的預(yù)處理環(huán)節(jié),這些宏名將會(huì)被替換成宏定義中的那些代碼段,注意:僅僅是單純的文本替換。

1. 最常見(jiàn)的宏

為了方便后面的描述,先來(lái)看幾個(gè)常見(jiàn)的宏定義:

(1) 數(shù)據(jù)類(lèi)型的定義

#ifndef BOOL    typedef char BOOL;#endif
#ifndef TRUE    #define TRUE#endif
#ifndef FALSE    #define FALSE#endif

在數(shù)據(jù)類(lèi)型定義中,需要注意的一點(diǎn)是:如果你的程序需要用不同平臺(tái)下的編譯器來(lái)編譯,那么你要去查一下所使用的編譯器對(duì)這些宏定義控制的數(shù)據(jù)類(lèi)型是否已經(jīng)定義了。例如:在 gcc 中沒(méi)有 BOOL 類(lèi)型,但是在 MSVC 中,把 BOOL 類(lèi)型定義為 int 型。

(2) 獲取最大、最小值

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

(3) 計(jì)算數(shù)組中的元素個(gè)數(shù)

#define ARRAY_SIZE(x)    (sizeof(x) / sizeof((x)[0]))

(4) 位操作

#define BIT_MASK(x)         (1 << (x))#define BIT_GET(x, y)       (((x) >> (y)) & 0x01u)#define BIT_SET(x, y)       ((x) | (1 << (y)))#define BIT_CLR(x, y)       ((x) & (~(1 << (y))))#define BIT_INVERT(x, y)    ((x) ^ (1 << (y)))
2. 與函數(shù)的區(qū)別

從上面這幾個(gè)宏來(lái)看,所有的這些操作都可以通過(guò)函數(shù)來(lái)實(shí)現(xiàn),那么他們各有什么優(yōu)缺點(diǎn)呢?

通過(guò)函數(shù)來(lái)實(shí)現(xiàn):

形參的類(lèi)型需要確定,調(diào)用時(shí)對(duì)參數(shù)進(jìn)行檢查;調(diào)用函數(shù)時(shí)需要額外的開(kāi)銷(xiāo):操作函數(shù)棧中的形參、返回值等;

通過(guò)宏來(lái)實(shí)現(xiàn):

不需要檢查參數(shù),更靈活的傳參;直接對(duì)宏進(jìn)行代碼擴(kuò)展,執(zhí)行時(shí)不需要函數(shù)調(diào)用;如果同一個(gè)宏在多處調(diào)用,會(huì)增加代碼體積;

還是舉一個(gè)例子來(lái)說(shuō)明比較好,就拿上面的比較大小來(lái)說(shuō)吧:

(1) 使用宏來(lái)實(shí)現(xiàn)

#define MAX(a, b)    (((a) > (b)) ? (a) : (b))
int main(){    printf("max: %d ", MAX(1, 2));}

(2) 使用函數(shù)來(lái)實(shí)現(xiàn)

int max(int a, int b){    if (a > b)        return a;    return b;}
int main(){    printf("max: %d ", max(1, 2));}

除了函數(shù)調(diào)用的開(kāi)銷(xiāo),其它看起來(lái)沒(méi)有差別。這里比較的是 2 個(gè)整型數(shù)據(jù),那么如果還需要比較 2 個(gè)浮點(diǎn)型數(shù)據(jù)呢?

使用宏來(lái)調(diào)用:MAX(1.1, 2.2);一切 OK;使用函數(shù)調(diào)用:max(1.1, 2.2); 編譯報(bào)錯(cuò):類(lèi)型不匹配。

此時(shí),使用宏來(lái)實(shí)現(xiàn)的優(yōu)勢(shì)就體現(xiàn)出來(lái)了:因?yàn)楹曛袥](méi)有類(lèi)型的概念,調(diào)用者傳入任何數(shù)據(jù)類(lèi)型都可以,然后在后面的比較操作中,大于或小于操作都是利用了 C 語(yǔ)言本身的語(yǔ)法來(lái)執(zhí)行。

如果使用函數(shù)來(lái)實(shí)現(xiàn),那么就必須再定義一個(gè)用來(lái)操作浮點(diǎn)型的函數(shù),以后還有可能比較:char 型、long 型數(shù)據(jù)等等。

在 C++ 中,這樣的操作可以通過(guò)參數(shù)模板來(lái)實(shí)現(xiàn),所謂的模板也是一種代碼動(dòng)態(tài)生成機(jī)制。當(dāng)定義了一個(gè)函數(shù)模板后,根據(jù)調(diào)用者的實(shí)參,來(lái)動(dòng)態(tài)產(chǎn)生多個(gè)函數(shù)。例如定義下面這個(gè)函數(shù)模板:

template

當(dāng)編譯器看到 max(1, 2) 時(shí),就會(huì)動(dòng)態(tài)生成一個(gè)函數(shù) int max(int a, int b) { ... };

當(dāng)編譯器看到 max(1.1, 2.2) 時(shí),又會(huì)動(dòng)態(tài)生成另一個(gè)函數(shù) float max(float a, float b) { ... }。

所以,從代碼的動(dòng)態(tài)生成角度看,宏定義和 C++ 中的模板參數(shù)有點(diǎn)神似,只不過(guò)宏定義僅僅是代碼擴(kuò)展而已。

下面這個(gè)例子也比較不錯(cuò),利用宏的類(lèi)型無(wú)關(guān),來(lái)動(dòng)態(tài)生成結(jié)構(gòu)體:

#define VEC(T)              struct vector_##T {         T *data;               size_t size;        };
int main(){    VEC(int)   vec_1 = { .data = NULL, .size = 0 };    VEC(float) vec_2 = { .data = NULL, .size = 0 };}

這個(gè)例子中用到了 ##,下面會(huì)解釋這個(gè)知識(shí)點(diǎn)。在前面的例子中,宏的參數(shù)傳遞的都是一些變量,而這里傳遞的宏參數(shù)是數(shù)據(jù)類(lèi)型,通過(guò)宏的類(lèi)型無(wú)關(guān)性,達(dá)到了“動(dòng)態(tài)”創(chuàng)建結(jié)構(gòu)體的目的:

struct vector_int {    int *data;    size_t size;}
struct vector_float {    float *data;    size_t size;}

這里有一個(gè)陷阱需要注意:傳遞的數(shù)據(jù)類(lèi)型中不能有空格,如果這樣使用:VEC(long long),那替換之后得到:

struct vector_long long {  // 語(yǔ)法錯(cuò)誤    long long *data;    size_t size;}

四、符號(hào):# 與 ##

這兩個(gè)符號(hào)在編程中的作用也是非常巧妙,夸張的說(shuō)一句:在任何框架性代碼中,都能見(jiàn)到它們的身影!作用如下:

#:把參數(shù)轉(zhuǎn)換成字符串;##:連接參數(shù)。1. #: 字符串化

直接看最簡(jiǎn)單的例子:

#define STR(x) #xprintf("string of 123: %s ", STR(123));

傳入的是一個(gè)數(shù)字 123,輸出的結(jié)果是字符串 “123”,這就是字符串化。

2. ##:參數(shù)連接

把宏中的參數(shù)按照字符進(jìn)行拼接,從而得到一個(gè)新的標(biāo)識(shí)符,例如:

#define MAKE_VAR(name, no) name##no
int main(void){    int MAKE_VAR(a, 1) = 1;     int MAKE_VAR(b, 2) = 2;
   printf("a1 = %d ", a1);    printf("b2 = %d ", b2);    return 0;}

當(dāng)調(diào)用宏 MAKE_VAR(a, 1) 后,符號(hào) ## 把兩側(cè)的 name 和 no 首先替換為 a 和 1,然后連接得到 a1。然后在調(diào)用語(yǔ)句中前面的 int 數(shù)據(jù)類(lèi)型就說(shuō)明了 a1 是一個(gè)整型數(shù)據(jù),最后初始化為 1。

五、可變參數(shù)的處理 

1. 參數(shù)名的定義和使用

宏定義的參數(shù)個(gè)數(shù)可以是不確定的,就像調(diào)用 printf 打印函數(shù)一樣,在定義的時(shí)候,可以使用三個(gè)點(diǎn)(...)來(lái)表示可變參數(shù),也可以在三個(gè)點(diǎn)的前面加上可變參數(shù)的名稱(chēng)。

如果使用三個(gè)點(diǎn)(...)來(lái)接收可變參數(shù),那么在使用的時(shí)候就需要使用 __VA_ARGS__來(lái)表示可變參數(shù),如下:

#define debug1(...)      printf(__VA_ARGS__)
debug1("this is debug1: %d ", 1);

如果在三個(gè)點(diǎn)(...)的前面加上了一個(gè)參數(shù)名,那么在使用時(shí)就一定要使用這個(gè)參數(shù)名,而不能使用 __VA_ARGS__來(lái)表示可變參數(shù),如下:

#define debug2(args...)  printf(args)
debug1("this is debug2: %d ", 2);
2. 可變參數(shù)個(gè)數(shù)為零的處理

看一下這個(gè)宏:

#define debug3(format, ...)      printf(format, __VA_ARGS__)
debug3("this is debug4: %d ", 4);

編譯、執(zhí)行都沒(méi)有問(wèn)題。但是如果這樣來(lái)使用宏:

debug3("hello ");

編譯的時(shí)候,會(huì)出現(xiàn)錯(cuò)誤: error: expected expression before ‘)’ token。為什么呢?

看一下宏擴(kuò)展之后的代碼(__VA_ARGS__為空):

printf("hello ",);

看出問(wèn)題了吧?在格式化字符串的后面多了一個(gè)逗號(hào)!為了解決問(wèn)題,預(yù)處理器給我們提供了一個(gè)方法:通過(guò) ## 符號(hào)把這個(gè)多余的逗號(hào)給自動(dòng)刪掉。于是宏定義改成下面這樣就沒(méi)有問(wèn)題了。

#define debug3(format, ...)     printf(format, ##__VA_ARGS__)

類(lèi)似的,如果自己定義了可變參數(shù)的名字,也在前面加上 ##,如下:

#define debug4(format, args...)  printf(format, ##args)

六、奇思妙想的宏

宏擴(kuò)展的本質(zhì)就是文本替換,但是一旦加上可變參數(shù)(__VA_ARGS__)和 ## 的連接功能,就能夠變化出無(wú)窮的想象力。

我一直堅(jiān)信,模仿是成為高手的第一步,只有見(jiàn)多識(shí)廣、多看、多學(xué)習(xí)別人是怎么來(lái)使用宏的,然后拿來(lái)為己所用,按照“先僵化-再優(yōu)化-最后固化”這個(gè)步驟來(lái)訓(xùn)練,總有一天你也能成為高手。

這里我們就來(lái)看幾個(gè)利用宏定義的巧妙實(shí)現(xiàn)。

1. 日志功能

在代碼中添加日志功能,幾乎是每個(gè)產(chǎn)品的標(biāo)配了,一般見(jiàn)到最普遍的是下面這樣的用法:

#ifdef DEBUG    #define LOG(...) printf(__VA_ARGS__)#else    #define LOG(...) #endif
int main(){    LOG("name = %s, age = %d ", "zhangsan", 20);    return 0;}

在編譯的時(shí)候,如果需要輸出日志功能就傳入宏定義 DEBUG,這樣就能打印輸出調(diào)試信息,當(dāng)然實(shí)際的產(chǎn)品中需要寫(xiě)入到文件中。如果不需要打印語(yǔ)句,通過(guò)把打印日志信息那條語(yǔ)句定義為空語(yǔ)句來(lái)達(dá)到目的。

換個(gè)思路,我們還可以通過(guò)條件判斷語(yǔ)句來(lái)控制打印信息,如下:

#ifdef DEBUG    #define debug if(1)#else     #define debug if(0)#endif
int main(){    debug {        printf("name = %s, age = %d ", "zhangsan", 20);    }    return 0;}

這樣控制日志信息的看到的不多,但是也能達(dá)到目的,放在這里只是給大家開(kāi)闊一下思路。

2. 利用宏來(lái)迭代每個(gè)參數(shù)#define first(x, ...) #x#define rest(x, ...)  #__VA_ARGS__
#define destructive(...)                                  do {                                                      printf("first is: %s", first(__VA_ARGS__));         printf("rest are: %s", rest(__VA_ARGS__));      } while (0)
int main(void){    destructive(1, 2, 3);    return 0;}

主要的思想就是:每次把可變參數(shù) VA_ARGS 中的第一個(gè)參數(shù)給分離出來(lái),然后把后面的參數(shù)再遞歸處理,這樣就可以分離出每一個(gè)參數(shù)了。我記得侯杰老師在 C++ 的視屏中,利用可變參數(shù)模板這個(gè)語(yǔ)法,也實(shí)現(xiàn)了類(lèi)似的功能。

剛才在有道筆記中居然找到了侯杰老師演示的代碼,熟悉 C++ 的小伙伴可以研究下下面這段代碼:

// 遞歸的最后一次調(diào)用void myprint(){}
template

在這個(gè)例子中,核心在于 TEST 宏定義,通過(guò) ## 拼接功能,構(gòu)造出 case 分支的比較目標(biāo),然后動(dòng)態(tài)拼接得到對(duì)應(yīng)的函數(shù),最后調(diào)用這個(gè)函數(shù)。

4. 動(dòng)態(tài)創(chuàng)建錯(cuò)誤編碼與對(duì)應(yīng)的錯(cuò)誤字符串

這也是一個(gè)非常巧妙的例子,利用了 #(字符串化) 和 ##(拼接) 這 2 個(gè)功能來(lái)動(dòng)態(tài)生成錯(cuò)誤編碼碼和相應(yīng)的錯(cuò)誤字符串:

#define MY_ERRORS         E(TOO_SMALL)          E(TOO_BIG)            E(INVALID_VARS)
#define E(e) Error_## e,typedef enum {    MY_ERRORS} MyEnums;#undef E
#define E(e) #e,const char *ErrorStrings[] = {    MY_ERRORS};#undef E
int main(){    printf("%d - %s ", Error_TOO_SMALL, ErrorStrings[0]);    printf("%d - %s ", Error_TOO_BIG, ErrorStrings[1]);    printf("%d - %s ", Error_INVALID_VARS, ErrorStrings[2]);
   return 0;}

我們把宏展開(kāi)之后,得到一個(gè)枚舉類(lèi)型和一個(gè)字符串常量數(shù)組:

typedef enum {    Error_TOO_SMALL,    Error_TOO_BIG,    Error_INVALID_VARS,} MyEnums;
const char *ErrorStrings[] = {    "TOO_SMALL",    "TOO_BIG",    "INVALID_VARS",};

宏擴(kuò)展之后的代碼是不是很簡(jiǎn)單啊。編譯、執(zhí)行結(jié)果如下:

0 - TOO_SMALL 1 - TOO_BIG 2 - INVALID_VARS

七、總結(jié)

有些人對(duì)宏愛(ài)之要死,多到濫用的程度;而有些人對(duì)宏恨之入骨,甚至用上了邪惡(evil)這個(gè)詞!其實(shí)宏對(duì)于 C 來(lái)說(shuō),就像菜刀對(duì)于廚師和歹徒一樣:用的好,可以讓代碼結(jié)構(gòu)簡(jiǎn)潔、后期維護(hù)特別方便;用的不好,就會(huì)引入晦澀的語(yǔ)法、難以調(diào)試的 Bug。

對(duì)于我們開(kāi)發(fā)人員來(lái)說(shuō),只要在程序的執(zhí)行效率、代碼的可維護(hù)性上做好平衡就可以了。

不吹噓,不炒作,不浮夸,認(rèn)真寫(xiě)好每一篇文章!
歡迎轉(zhuǎn)發(fā)、分享給身邊的技術(shù)朋友,道哥在此表示衷心的感謝!轉(zhuǎn)發(fā)的推薦語(yǔ)已經(jīng)幫您想好了:

道哥總結(jié)的這篇總結(jié)文章,寫(xiě)得很用心,對(duì)我的技術(shù)提升很有幫助。好東西,要分享!

<上一頁(yè)  1  2  
聲明: 本文由入駐維科號(hào)的作者撰寫(xiě),觀點(diǎn)僅代表作者本人,不代表OFweek立場(chǎng)。如有侵權(quán)或其他問(wèn)題,請(qǐng)聯(lián)系舉報(bào)。

發(fā)表評(píng)論

0條評(píng)論,0人參與

請(qǐng)輸入評(píng)論內(nèi)容...

請(qǐng)輸入評(píng)論/評(píng)論長(zhǎng)度6~500個(gè)字

您提交的評(píng)論過(guò)于頻繁,請(qǐng)輸入驗(yàn)證碼繼續(xù)

  • 看不清,點(diǎn)擊換一張  刷新

暫無(wú)評(píng)論

暫無(wú)評(píng)論

    掃碼關(guān)注公眾號(hào)
    OFweek人工智能網(wǎng)
    獲取更多精彩內(nèi)容
    文章糾錯(cuò)
    x
    *文字標(biāo)題:
    *糾錯(cuò)內(nèi)容:
    聯(lián)系郵箱:
    *驗(yàn) 證 碼:

    粵公網(wǎng)安備 44030502002758號(hào)