如何提高代碼逼格?宏定義-從入門到放棄
三、宏擴展
所謂的宏擴展就是代碼替換,這部分內(nèi)容也是我想表達的主要內(nèi)容。宏擴展最大的好處有如下幾點:
減少重復(fù)的代碼;完成一些通過 C 語法無法實現(xiàn)的功能(字符串拼接);動態(tài)定義數(shù)據(jù)類型,實現(xiàn)類似 C++ 中模板的功能;程序更容易理解、修改(例如:數(shù)字、字符串常亮);
我們在寫代碼的時候,所有使用宏名稱的地方,都可以理解為一個占位符。在編譯程序的預(yù)處理環(huán)節(jié),這些宏名將會被替換成宏定義中的那些代碼段,注意:僅僅是單純的文本替換。
1. 最常見的宏
為了方便后面的描述,先來看幾個常見的宏定義:
(1) 數(shù)據(jù)類型的定義
#ifndef BOOL typedef char BOOL;#endif
#ifndef TRUE #define TRUE#endif
#ifndef FALSE #define FALSE#endif
在數(shù)據(jù)類型定義中,需要注意的一點是:如果你的程序需要用不同平臺下的編譯器來編譯,那么你要去查一下所使用的編譯器對這些宏定義控制的數(shù)據(jù)類型是否已經(jīng)定義了。例如:在 gcc 中沒有 BOOL 類型,但是在 MSVC 中,把 BOOL 類型定義為 int 型。
(2) 獲取最大、最小值
#define MAX(a, b) (((a) > (b)) ? (a) : (b))#define MIN(a, b) (((a) < (b)) ? (a) : (b))
(3) 計算數(shù)組中的元素個數(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ū)別
從上面這幾個宏來看,所有的這些操作都可以通過函數(shù)來實現(xiàn),那么他們各有什么優(yōu)缺點呢?
通過函數(shù)來實現(xiàn):
形參的類型需要確定,調(diào)用時對參數(shù)進行檢查;調(diào)用函數(shù)時需要額外的開銷:操作函數(shù)棧中的形參、返回值等;
通過宏來實現(xiàn):
不需要檢查參數(shù),更靈活的傳參;直接對宏進行代碼擴展,執(zhí)行時不需要函數(shù)調(diào)用;如果同一個宏在多處調(diào)用,會增加代碼體積;
還是舉一個例子來說明比較好,就拿上面的比較大小來說吧:
(1) 使用宏來實現(xiàn)
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
int main(){ printf("max: %d ", MAX(1, 2));}
(2) 使用函數(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)用的開銷,其它看起來沒有差別。這里比較的是 2 個整型數(shù)據(jù),那么如果還需要比較 2 個浮點型數(shù)據(jù)呢?
使用宏來調(diào)用:MAX(1.1, 2.2);一切 OK;使用函數(shù)調(diào)用:max(1.1, 2.2); 編譯報錯:類型不匹配。
此時,使用宏來實現(xiàn)的優(yōu)勢就體現(xiàn)出來了:因為宏中沒有類型的概念,調(diào)用者傳入任何數(shù)據(jù)類型都可以,然后在后面的比較操作中,大于或小于操作都是利用了 C 語言本身的語法來執(zhí)行。
如果使用函數(shù)來實現(xiàn),那么就必須再定義一個用來操作浮點型的函數(shù),以后還有可能比較:char 型、long 型數(shù)據(jù)等等。
在 C++ 中,這樣的操作可以通過參數(shù)模板來實現(xiàn),所謂的模板也是一種代碼動態(tài)生成機制。當定義了一個函數(shù)模板后,根據(jù)調(diào)用者的實參,來動態(tài)產(chǎn)生多個函數(shù)。例如定義下面這個函數(shù)模板:
template
當編譯器看到 max(1, 2) 時,就會動態(tài)生成一個函數(shù) int max(int a, int b) { ... };
當編譯器看到 max(1.1, 2.2) 時,又會動態(tài)生成另一個函數(shù) float max(float a, float b) { ... }。
所以,從代碼的動態(tài)生成角度看,宏定義和 C++ 中的模板參數(shù)有點神似,只不過宏定義僅僅是代碼擴展而已。
下面這個例子也比較不錯,利用宏的類型無關(guān),來動態(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 };}
這個例子中用到了 ##,下面會解釋這個知識點。在前面的例子中,宏的參數(shù)傳遞的都是一些變量,而這里傳遞的宏參數(shù)是數(shù)據(jù)類型,通過宏的類型無關(guān)性,達到了“動態(tài)”創(chuàng)建結(jié)構(gòu)體的目的:
struct vector_int { int *data; size_t size;}
struct vector_float { float *data; size_t size;}
這里有一個陷阱需要注意:傳遞的數(shù)據(jù)類型中不能有空格,如果這樣使用:VEC(long long),那替換之后得到:
struct vector_long long { // 語法錯誤 long long *data; size_t size;}
四、符號:# 與 ##
這兩個符號在編程中的作用也是非常巧妙,夸張的說一句:在任何框架性代碼中,都能見到它們的身影!作用如下:
#:把參數(shù)轉(zhuǎn)換成字符串;##:連接參數(shù)。1. #: 字符串化
直接看最簡單的例子:
#define STR(x) #xprintf("string of 123: %s ", STR(123));
傳入的是一個數(shù)字 123,輸出的結(jié)果是字符串 “123”,這就是字符串化。
2. ##:參數(shù)連接
把宏中的參數(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;}
當調(diào)用宏 MAKE_VAR(a, 1) 后,符號 ## 把兩側(cè)的 name 和 no 首先替換為 a 和 1,然后連接得到 a1。然后在調(diào)用語句中前面的 int 數(shù)據(jù)類型就說明了 a1 是一個整型數(shù)據(jù),最后初始化為 1。
五、可變參數(shù)的處理
1. 參數(shù)名的定義和使用
宏定義的參數(shù)個數(shù)可以是不確定的,就像調(diào)用 printf 打印函數(shù)一樣,在定義的時候,可以使用三個點(...)來表示可變參數(shù),也可以在三個點的前面加上可變參數(shù)的名稱。
如果使用三個點(...)來接收可變參數(shù),那么在使用的時候就需要使用 __VA_ARGS__來表示可變參數(shù),如下:
#define debug1(...) printf(__VA_ARGS__)
debug1("this is debug1: %d ", 1);
如果在三個點(...)的前面加上了一個參數(shù)名,那么在使用時就一定要使用這個參數(shù)名,而不能使用 __VA_ARGS__來表示可變參數(shù),如下:
#define debug2(args...) printf(args)
debug1("this is debug2: %d ", 2);
2. 可變參數(shù)個數(shù)為零的處理
看一下這個宏:
#define debug3(format, ...) printf(format, __VA_ARGS__)
debug3("this is debug4: %d ", 4);
編譯、執(zhí)行都沒有問題。但是如果這樣來使用宏:
debug3("hello ");
編譯的時候,會出現(xiàn)錯誤: error: expected expression before ‘)’ token。為什么呢?
看一下宏擴展之后的代碼(__VA_ARGS__為空):
printf("hello ",);
看出問題了吧?在格式化字符串的后面多了一個逗號!為了解決問題,預(yù)處理器給我們提供了一個方法:通過 ## 符號把這個多余的逗號給自動刪掉。于是宏定義改成下面這樣就沒有問題了。
#define debug3(format, ...) printf(format, ##__VA_ARGS__)
類似的,如果自己定義了可變參數(shù)的名字,也在前面加上 ##,如下:
#define debug4(format, args...) printf(format, ##args)
六、奇思妙想的宏
宏擴展的本質(zhì)就是文本替換,但是一旦加上可變參數(shù)(__VA_ARGS__)和 ## 的連接功能,就能夠變化出無窮的想象力。
我一直堅信,模仿是成為高手的第一步,只有見多識廣、多看、多學(xué)習(xí)別人是怎么來使用宏的,然后拿來為己所用,按照“先僵化-再優(yōu)化-最后固化”這個步驟來訓(xùn)練,總有一天你也能成為高手。
這里我們就來看幾個利用宏定義的巧妙實現(xiàn)。
1. 日志功能
在代碼中添加日志功能,幾乎是每個產(chǎn)品的標配了,一般見到最普遍的是下面這樣的用法:
#ifdef DEBUG #define LOG(...) printf(__VA_ARGS__)#else #define LOG(...) #endif
int main(){ LOG("name = %s, age = %d ", "zhangsan", 20); return 0;}
在編譯的時候,如果需要輸出日志功能就傳入宏定義 DEBUG,這樣就能打印輸出調(diào)試信息,當然實際的產(chǎn)品中需要寫入到文件中。如果不需要打印語句,通過把打印日志信息那條語句定義為空語句來達到目的。
換個思路,我們還可以通過條件判斷語句來控制打印信息,如下:
#ifdef DEBUG #define debug if(1)#else #define debug if(0)#endif
int main(){ debug { printf("name = %s, age = %d ", "zhangsan", 20); } return 0;}
這樣控制日志信息的看到的不多,但是也能達到目的,放在這里只是給大家開闊一下思路。
2. 利用宏來迭代每個參數(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 中的第一個參數(shù)給分離出來,然后把后面的參數(shù)再遞歸處理,這樣就可以分離出每一個參數(shù)了。我記得侯杰老師在 C++ 的視屏中,利用可變參數(shù)模板這個語法,也實現(xiàn)了類似的功能。
剛才在有道筆記中居然找到了侯杰老師演示的代碼,熟悉 C++ 的小伙伴可以研究下下面這段代碼:
// 遞歸的最后一次調(diào)用void myprint(){}
template
在這個例子中,核心在于 TEST 宏定義,通過 ## 拼接功能,構(gòu)造出 case 分支的比較目標,然后動態(tài)拼接得到對應(yīng)的函數(shù),最后調(diào)用這個函數(shù)。
4. 動態(tài)創(chuàng)建錯誤編碼與對應(yīng)的錯誤字符串
這也是一個非常巧妙的例子,利用了 #(字符串化) 和 ##(拼接) 這 2 個功能來動態(tài)生成錯誤編碼碼和相應(yīng)的錯誤字符串:
#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;}
我們把宏展開之后,得到一個枚舉類型和一個字符串常量數(shù)組:
typedef enum { Error_TOO_SMALL, Error_TOO_BIG, Error_INVALID_VARS,} MyEnums;
const char *ErrorStrings[] = { "TOO_SMALL", "TOO_BIG", "INVALID_VARS",};
宏擴展之后的代碼是不是很簡單啊。編譯、執(zhí)行結(jié)果如下:
0 - TOO_SMALL 1 - TOO_BIG 2 - INVALID_VARS
七、總結(jié)
有些人對宏愛之要死,多到濫用的程度;而有些人對宏恨之入骨,甚至用上了邪惡(evil)這個詞!其實宏對于 C 來說,就像菜刀對于廚師和歹徒一樣:用的好,可以讓代碼結(jié)構(gòu)簡潔、后期維護特別方便;用的不好,就會引入晦澀的語法、難以調(diào)試的 Bug。
對于我們開發(fā)人員來說,只要在程序的執(zhí)行效率、代碼的可維護性上做好平衡就可以了。
不吹噓,不炒作,不浮夸,認真寫好每一篇文章!
歡迎轉(zhuǎn)發(fā)、分享給身邊的技術(shù)朋友,道哥在此表示衷心的感謝!轉(zhuǎn)發(fā)的推薦語已經(jīng)幫您想好了:
道哥總結(jié)的這篇總結(jié)文章,寫得很用心,對我的技術(shù)提升很有幫助。好東西,要分享!
請輸入評論內(nèi)容...
請輸入評論/評論長度6~500個字
最新活動更多
-
11月20日火熱報名中>> 2024 智能家居出海論壇
-
11月28日立即報名>>> 2024工程師系列—工業(yè)電子技術(shù)在線會議
-
12月19日立即報名>> 【線下會議】OFweek 2024(第九屆)物聯(lián)網(wǎng)產(chǎn)業(yè)大會
-
即日-12.26火熱報名中>> OFweek2024中國智造CIO在線峰會
-
即日-2025.8.1立即下載>> 《2024智能制造產(chǎn)業(yè)高端化、智能化、綠色化發(fā)展藍皮書》
-
精彩回顧立即查看>> 【在線會議】多物理場仿真助跑新能源汽車
推薦專題
- 1 腦機接口芯片,華為出了新專利!
- 2 銀行業(yè)AI大模型,從入局到求變
- 3 巨頭搶布局,VC狂撒錢,為了能讓「AI讀心」這些公司卷瘋了
- 4 阿斯麥ASML:“骨折級”洋相,又成AI第一殺手?
- 5 蘋果市值創(chuàng)新高,iPhone 16能否助力突破4萬億美元大關(guān)?
- 6 地平線開啟配售,阿里百度各砸5000萬美金,市值最高超500億
- 7 小馬智行沖刺納斯達克:或成「全球Robotaxi第一股」,兩年半營收約12億元
- 8 云從科技:營收低迷與虧損加劇,2025年盈利目標挑戰(zhàn)重重
- 9 AI奇跡:域名賣爆,無名小島意外賺2億
- 10 逆境求生,泄密風(fēng)波中的高精地圖
- 高級軟件工程師 廣東省/深圳市
- 自動化高級工程師 廣東省/深圳市
- 光器件研發(fā)工程師 福建省/福州市
- 銷售總監(jiān)(光器件) 北京市/海淀區(qū)
- 激光器高級銷售經(jīng)理 上海市/虹口區(qū)
- 光器件物理工程師 北京市/海淀區(qū)
- 激光研發(fā)工程師 北京市/昌平區(qū)
- 技術(shù)專家 廣東省/江門市
- 封裝工程師 北京市/海淀區(qū)
- 結(jié)構(gòu)工程師 廣東省/深圳市