【為宏正名】本應(yīng)寫入教科書的“世界設(shè)定”
掃描二維碼
隨時(shí)隨地手機(jī)看文章
為什么大家會(huì)那么懼怕宏的使用;
定義宏的時(shí)候,為什么遇到哪怕很基本的小問(wèn)題也根本無(wú)從下手;
為什么那么多人聲稱系統(tǒng)提供的諸如 __LINE__ 之類的宏時(shí)好時(shí)壞;
為什么很多關(guān)于宏的正常使用被稱為奇技淫巧……
真是哭笑不得。這些規(guī)則是如此簡(jiǎn)單,介紹一下根本無(wú)需多么復(fù)雜的篇幅。接下來(lái),讓我們簡(jiǎn)單的學(xué)習(xí)一下這些本應(yīng)該寫入教科書中的基本內(nèi)容。注意,這與你們?cè)谄渌娞?hào)里學(xué)到的關(guān)于某些宏的基本使用方法是兩回事。
【宏不屬于C語(yǔ)言】
C語(yǔ)言的編譯分為三個(gè)階段:預(yù)編譯階段、編譯階段和鏈接階段。正如上圖所示的那樣,預(yù)編譯階段的產(chǎn)物是單個(gè)的“.c”文件;編譯階段將這些“.c”文件一個(gè)一個(gè)彼此獨(dú)立的編譯為對(duì)應(yīng)的對(duì)象("*.obj")文件;這些對(duì)象文件就像樂(lè)高積木一樣會(huì)在最終的鏈接階段按照事先約定好的圖紙(地址空間布局描述文件,又稱linker script或者scatter script)被linker組裝到一起,最終生成在目標(biāo)機(jī)器上可以運(yùn)行的鏡像文件。
宏僅在預(yù)編譯階段有效,它的本質(zhì)只是文字替換。在完成預(yù)編譯處理以后,進(jìn)入編譯階段的.c實(shí)際上已經(jīng)不存在任何“宏”、條件編譯、“#include”以及"#pragma"之類的預(yù)編譯內(nèi)容——此時(shí)的C源文件是一個(gè)純粹且獨(dú)立的文本文件。很多編譯器在命令行下都提供一個(gè)"-E"的選項(xiàng),它其實(shí)就是告訴編譯器,只進(jìn)行預(yù)編譯操作并停在這里。此時(shí),編譯的結(jié)果就是大家所說(shuō)的“宏展開”后的內(nèi)容。學(xué)會(huì)使用"-E"選項(xiàng),是檢測(cè)自己縮寫的宏是否正確的最有效工具。
! armclang --target=arm-arm-none-eabi -march=armv6-m -E -x c
define ADDRESS 0x20000000
"include_file_1.h" include
LR1 ADDRESS
{
…
}
這里,第一行的命令行:
#! armclang --target=arm-arm-none-eabi -march=armv6-m -E -x c
就是告訴linker,在處理scatter-script之前要執(zhí)行“#!” 后面的命令行,這里的"-E"就是告訴armclang:“我們只進(jìn)行預(yù)編譯”——也就是"#include"以及宏替換之類的工作——所以宏“ADDRESS” 會(huì)被替換會(huì) 0x20000000,而"include_file_1.h" 中的內(nèi)容也會(huì)被加入到當(dāng)前的scatter-script文件中來(lái)。
正如前面所說(shuō)的,宏只存在于“預(yù)編譯階段”,而活不到“編譯階段”;宏是沒(méi)有任何C語(yǔ)法意義的;
枚舉與之相反,只存在于“編譯階段”,是具有嚴(yán)格的C語(yǔ)法意義的——它的每一個(gè)成員都明確代表一個(gè)整形常量值。
其實(shí),從宏和枚舉服務(wù)的階段看來(lái),他們是老死不相往來(lái)的。那么具體在使用時(shí),這里的區(qū)別表現(xiàn)在什么地方呢?我們來(lái)看一個(gè)例子:
extern uint8_t s_chUSARTBuffer[USART_COUNT];
這里例子意圖很簡(jiǎn)單,根據(jù)宏USART_COUNT的值來(lái)?xiàng)l件編譯。如果我們把USART_COUNT換成枚舉就不行了:
typedef enum {
/* list all the available USART here */
USART0_idx = 0,
USART1_idx,
USART2_idx,
USART3_idx,
/* number of USARTs*/
USART_COUNT,
}usart_idx_t;
extern uint8_t s_chUSARTBuffer[USART_COUNT];
在這個(gè)例子里,USART_COUNT的值會(huì)隨著前面列舉的UARTx_idx的增加而自動(dòng)增加——作為一個(gè)技巧——精確的表示當(dāng)前實(shí)際有效的USART數(shù)量,從意義上說(shuō)嚴(yán)格貼合了 USART_COUNT 這個(gè)名稱的意義。這個(gè)代碼看似沒(méi)有問(wèn)題,但實(shí)際上根據(jù)前面的知識(shí)我們知道:條件編譯是在“預(yù)編譯階段”進(jìn)行的、枚舉是在“編譯階段”才有意義。換句話說(shuō),當(dāng)下面代碼判斷枚舉USART_COUNT的時(shí)候,預(yù)編譯階段根本不認(rèn)識(shí)它是誰(shuí)(預(yù)編譯階段沒(méi)有任何C語(yǔ)言的語(yǔ)法知識(shí))——這時(shí)候USART_COUNT作為枚舉還沒(méi)出生呢!
extern uint8_t s_chUSARTBuffer[USART_COUNT];
同樣道理,如果你想借助下面的宏來(lái)生成代碼,得到的結(jié)果會(huì)出人意料:
typedef enum {
/* list all the available USART here */
USART0_idx = 0,
USART1_idx,
USART2_idx,
USART3_idx,
/* number of USARTs*/
USART_COUNT,
}usart_idx_t;
extern int usart0_init(void);
extern int usart1_init(void);
extern int usart2_init(void);
extern int usart3_init(void);
usart
應(yīng)用中,我們期望配合UARTn_idx與宏USART_INIT一起使用:
...
USART_INIT(USART1_idx);
...
借助宏的膠水運(yùn)算“##”,我們期望的結(jié)果是:
...
usart1_init();
...
由于同樣的原因——在進(jìn)行宏展開的時(shí)候,枚舉還沒(méi)有“出生”——實(shí)際展開的效果是這樣的:
...
usartUSART1_idx_init();
...
由于函數(shù) usartUSART1_idx_init() 并不存在,所以在鏈接階段linker會(huì)報(bào)告類似“undefined symbol usartUSART1_idx_init()”——簡(jiǎn)單說(shuō)就是找不到函數(shù)。要解決這一問(wèn)題也很簡(jiǎn)單,直接把枚舉用宏來(lái)定義就可以了:
extern int usart0_init(void);
extern int usart1_init(void);
extern int usart2_init(void);
extern int usart3_init(void);
枚舉可以被當(dāng)作類型來(lái)使用,并定義枚舉變量——宏做不到;
當(dāng)使用枚舉作為函數(shù)的形參或者是switch檢測(cè)的目標(biāo)時(shí),有些比較“智能”的C編譯器會(huì)在編譯階段把枚舉作為參考進(jìn)行“強(qiáng)類型”檢測(cè)——比如檢查函數(shù)傳遞過(guò)程中你給的值是否是枚舉中實(shí)際存在的;又比如在switch中是否所有的枚舉條目都有對(duì)應(yīng)的case(在省缺default的情況下)。
除IAR以外,保存枚舉所需的整型在一個(gè)編譯環(huán)境中是相對(duì)來(lái)說(shuō)較為確定的(不是short就是int)——在這種情況下,枚舉的常量值就具有了類型信息,這是用宏表示常量時(shí)所不具備的。
少數(shù)IDE只能對(duì)枚舉進(jìn)行語(yǔ)法提示而無(wú)法對(duì)宏進(jìn)行語(yǔ)法提示。
【宏的本質(zhì)和替換規(guī)則】
在#ifdef、#ifndef 以及 defined() 表達(dá)式中,它可以正確的返回boolean量——確切的表示它沒(méi)有被定義過(guò);
在#if 中被直接使用(沒(méi)有配合defined()),則很多編譯器會(huì)報(bào)告warning,指出這是一個(gè)不存在的宏,同時(shí)默認(rèn)它的值是boolean量的false——而并不保證是"0";
在除以上情形外的其它地方使用,比如在代碼中使用,則它會(huì)被作為代碼的一部分原樣保留到編譯階段——而不會(huì)進(jìn)行任何操作;通常這會(huì)在鏈接階段觸發(fā)“undefined symbol”錯(cuò)誤——這是很自然的,因?yàn)槟阋詾槟阍谟煤辏ㄖ徊贿^(guò)因?yàn)槟阃浂x了,或者沒(méi)有正確include所需的頭文件),編譯器卻以為你在說(shuō)函數(shù)或者變量——當(dāng)然找不到了。
舉個(gè)例子,宏 __STDC_VERSION__ 可以被用來(lái)檢查當(dāng)前ANSI-C的標(biāo)準(zhǔn):
if __STD_VERSION__ >= 199901L
/* support C99 */
define SAFE_ATOM_CODE(...) \
{ \
uint32_t wTemp = __disable_irq(); \
__VA_ARGS__; \
__set_PRIMASK(wTemp); \
}
else
/* doesn't support C99, assume C89/90 */
define SAFE_ATOM_CODE(__CODE) \
{ \
uint32_t wTemp = __disable_irq(); \
__CODE; \
__set_PRIMASK(wTemp); \
}
endif
上述寫法在支持C99的編譯器中是不會(huì)有問(wèn)題的,因?yàn)?nbsp;__STDC_VERSION__ 一定會(huì)由編譯器預(yù)先定義過(guò);而同樣的代碼放到僅支持C89/90的環(huán)境中就有可能會(huì)出問(wèn)題,因?yàn)?nbsp;__STDC_VERSION__ 并不保證一定會(huì)被事先定義好(C89/90并沒(méi)有規(guī)定要提供這個(gè)宏),因此 __STDC_VERSION__ 就有可能成為一個(gè)未定義的宏,從而觸發(fā)編譯器的warning。為了修正這一問(wèn)題,我們需要對(duì)上述內(nèi)容進(jìn)行適當(dāng)?shù)男薷模?br>
if defined(__STD_VERSION__) && __STD_VERSION__ >= 199901L
/* support C99 */
...
else
/* doesn't support C99, assume C89/90 */
...
endif
在#ifdef、#ifndef 以及 defined() 表達(dá)式中,它可以正確的返回boolean量——確切的表示它被定義了;
在#if 中被直接使用(沒(méi)有配合defined()),編譯器會(huì)把它看作“空”;在一些數(shù)值表達(dá)式中,它會(huì)被默認(rèn)當(dāng)作“0”,沒(méi)有任何警告信息會(huì)被產(chǎn)生
在除以上情形外的其它地方使用,比如在代碼中使用,編譯器會(huì)把它看作“空字符串”(注意,這里不包含引號(hào))——它不會(huì)存活到編譯階段;
第一條:任何使用到膠水運(yùn)算“##”對(duì)形參進(jìn)行粘合的參數(shù)宏,一定需要額外的再套一層
第二條:其余情況下,如果要用到膠水運(yùn)算,一定要在內(nèi)部借助參數(shù)宏來(lái)完成粘合過(guò)程
#define SAFE_ATOM_CODE(...) \
{ \
uint32_t wTemp = __disable_irq(); \
__VA_ARGS__; \
__set_PRIMASK(wTemp); \
}
由于這里定義了一個(gè)變量wTemp,而如果用戶插入的代碼中也使用了同名的變量,就會(huì)產(chǎn)生很多問(wèn)題:輕則編譯錯(cuò)誤(重復(fù)定義);重則出現(xiàn)局部變量wTemp強(qiáng)行取代了用戶自定義的靜態(tài)變量的情況,從而直接導(dǎo)致系統(tǒng)運(yùn)行出現(xiàn)隨機(jī)性的故障(比如隨機(jī)性的中斷被關(guān)閉后不再恢復(fù),或是原本應(yīng)該被關(guān)閉的全局中斷處于打開狀態(tài)等等)。為了避免這一問(wèn)題,我們往往會(huì)想自動(dòng)給這個(gè)變量一個(gè)不會(huì)重復(fù)的名字,比如借助 __LINE__ 宏給這一變量加入一個(gè)后綴:
#define SAFE_ATOM_CODE(...) \
{ \
uint32_t wTemp##__LINE__ = __disable_irq(); \
__VA_ARGS__; \
__set_PRIMASK(wTemp); \
}
...
SAFE_ATOM_CODE(
/* do something here */
...
)
...
...
{
uint32_t wTemp123 = __disable_irq();
__VA_ARGS__;
__set_PRIMASK(wTemp);
}
...
...
{
uint32_t wTemp__LINE__ = __disable_irq();
__VA_ARGS__;
__set_PRIMASK(wTemp);
}
...
從內(nèi)容上看,SAFE_ATOM_CODE() 要粘合的對(duì)象并不是形參,根據(jù)結(jié)論第二條,需要借助另外一個(gè)參數(shù)宏來(lái)幫忙完成這一過(guò)程。為此,我們需要引入一個(gè)專門的宏:
##__B define __CONNECT2(__A, __B) __A
define CONNECT2(__A, __B) __CONNECT2(__A, __B)
#define __CONNECT3(__A, __B, __C) __A##__B##__C
define CONNECT2(__A, __B, __C) __CONNECT3(__A, __B, __C)
#define SAFE_ATOM_CODE(...) \
{ \
uint32_t CONNECT2(wTemp,__LINE__) = \
__disable_irq(); \
__VA_ARGS__; \
__set_PRIMASK(wTemp); \
}
if (true == xxxxx) {...}
if (1 == xxxxx) {...}
對(duì)于下面的代碼:
CONNECT2(uint32_t wVariable, EXAMPLE);
如果宏是一個(gè)變量,那么展開的結(jié)果應(yīng)該是:
uint32_t wVariable123;
然而,我們實(shí)際獲得的是:
uint32_t wVariableEXAMPLE_A;
如何理解這一結(jié)果呢?
如果宏是一個(gè)引用,那么當(dāng)EXAMPLE_A與123之間的關(guān)系被銷毀時(shí),原本EXAMPLE > EXAMPLE_A > 123 的引用關(guān)系就只剩下 EXAMPLE > EXAMPLE_A。又由于EXAMPLE_A已經(jīng)不復(fù)存在,因此EXAMPLE_A在展開時(shí)就被當(dāng)作是最終的字符串,與"uint32_t wVariable"連接到了一起。
usart
USART_INIT(USART1_idx);
usart1_init();
USART_INIT(DEBUG_USART);
/* app_cfg.h */
usart(1+2)_init();
/* 獲取個(gè)位 */
/* 獲取十位數(shù)字 */
/* 獲取百位數(shù)字 */
__MFUNC_OUT_DEC_DIGIT_TEMP0)
__MFUNC_OUT_DEC_DIGIT_TEMP1,\
__MFUNC_OUT_DEC_DIGIT_TEMP0)
/* 建立腳本輸入值與 DEBUG_USART 之間的引用關(guān)系*/
/* "調(diào)用"轉(zhuǎn)換腳本 */
/* 建立 DEBUG_USART 與腳本輸出值之間的引用 */
USART_INIT(DEBUG_USART);
打完收工。
干貨不易,如果你覺(jué)得這篇文章對(duì)你有所幫助或是有所啟發(fā),點(diǎn)贊、轉(zhuǎn)發(fā)、收藏三聯(lián)!
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!