
為什么大家會那么懼怕宏的使用;
定義宏的時候,為什么遇到哪怕很基本的小問題也根本無從下手;
為什么那么多人聲稱系統提供的諸如 __LINE__ 之類的宏時好時壞;
為什么很多關于宏的正常使用被稱為奇技淫巧……
真是哭笑不得。這些規則是如此簡單,介紹一下根本無需多么復雜的篇幅。接下來,讓我們簡單的學習一下這些本應該寫入教科書中的基本內容。注意,這與你們在其它公眾號里學到的關于某些宏的基本使用方法是兩回事。
【宏不屬于C語言】
C語言的編譯分為三個階段:預編譯階段、編譯階段和鏈接階段。正如上圖所示的那樣,預編譯階段的產物是單個的“.c”文件;編譯階段將這些“.c”文件一個一個彼此獨立的編譯為對應的對象("*.obj")文件;這些對象文件就像樂高積木一樣會在最終的鏈接階段按照事先約定好的圖紙(地址空間布局描述文件,又稱linker script或者scatter script)被linker組裝到一起,最終生成在目標機器上可以運行的鏡像文件。
宏僅在預編譯階段有效,它的本質只是文字替換。在完成預編譯處理以后,進入編譯階段的.c實際上已經不存在任何“宏”、條件編譯、“#include”以及"#pragma"之類的預編譯內容——此時的C源文件是一個純粹且獨立的文本文件。很多編譯器在命令行下都提供一個"-E"的選項,它其實就是告訴編譯器,只進行預編譯操作并停在這里。此時,編譯的結果就是大家所說的“宏展開”后的內容。學會使用"-E"選項,是檢測自己縮寫的宏是否正確的最有效工具。
! 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之前要執行“#!” 后面的命令行,這里的"-E"就是告訴armclang:“我們只進行預編譯”——也就是"#include"以及宏替換之類的工作——所以宏“ADDRESS” 會被替換會 0x20000000,而"include_file_1.h" 中的內容也會被加入到當前的scatter-script文件中來。
正如前面所說的,宏只存在于“預編譯階段”,而活不到“編譯階段”;宏是沒有任何C語法意義的;
枚舉與之相反,只存在于“編譯階段”,是具有嚴格的C語法意義的——它的每一個成員都明確代表一個整形常量值。
其實,從宏和枚舉服務的階段看來,他們是老死不相往來的。那么具體在使用時,這里的區別表現在什么地方呢?我們來看一個例子:
extern uint8_t s_chUSARTBuffer[USART_COUNT];
這里例子意圖很簡單,根據宏USART_COUNT的值來條件編譯。如果我們把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];
在這個例子里,USART_COUNT的值會隨著前面列舉的UARTx_idx的增加而自動增加——作為一個技巧——精確的表示當前實際有效的USART數量,從意義上說嚴格貼合了 USART_COUNT 這個名稱的意義。這個代碼看似沒有問題,但實際上根據前面的知識我們知道:條件編譯是在“預編譯階段”進行的、枚舉是在“編譯階段”才有意義。換句話說,當下面代碼判斷枚舉USART_COUNT的時候,預編譯階段根本不認識它是誰(預編譯階段沒有任何C語言的語法知識)——這時候USART_COUNT作為枚舉還沒出生呢!
extern uint8_t s_chUSARTBuffer[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 int usart0_init(void);
extern int usart1_init(void);
extern int usart2_init(void);
extern int usart3_init(void);
usart
應用中,我們期望配合UARTn_idx與宏USART_INIT一起使用:
...
USART_INIT(USART1_idx);
...
借助宏的膠水運算“##”,我們期望的結果是:
...
usart1_init();
...
由于同樣的原因——在進行宏展開的時候,枚舉還沒有“出生”——實際展開的效果是這樣的:
...
usartUSART1_idx_init();
...
由于函數 usartUSART1_idx_init() 并不存在,所以在鏈接階段linker會報告類似“undefined symbol usartUSART1_idx_init()”——簡單說就是找不到函數。要解決這一問題也很簡單,直接把枚舉用宏來定義就可以了:
extern int usart0_init(void);
extern int usart1_init(void);
extern int usart2_init(void);
extern int usart3_init(void);
枚舉可以被當作類型來使用,并定義枚舉變量——宏做不到;
當使用枚舉作為函數的形參或者是switch檢測的目標時,有些比較“智能”的C編譯器會在編譯階段把枚舉作為參考進行“強類型”檢測——比如檢查函數傳遞過程中你給的值是否是枚舉中實際存在的;又比如在switch中是否所有的枚舉條目都有對應的case(在缺省default的情況下)。
除IAR以外,保存枚舉所需的整型在一個編譯環境中是相對來說較為確定的(不是short就是int)——在這種情況下,枚舉的常量值就具有了類型信息,這是用宏表示常量時所不具備的。
少數IDE只能對枚舉進行語法提示而無法對宏進行語法提示。
【宏的本質和替換規則】
在#ifdef、#ifndef 以及 defined() 表達式中,它可以正確的返回boolean量——確切的表示它沒有被定義過;
在#if 中被直接使用(沒有配合defined()),則很多編譯器會報告warning,指出這是一個不存在的宏,同時默認它的值是boolean量的false——而并不保證是"0";
在除以上情形外的其它地方使用,比如在代碼中使用,則它會被作為代碼的一部分原樣保留到編譯階段——而不會進行任何操作;通常這會在鏈接階段觸發“undefined symbol”錯誤——這是很自然的,因為你以為你在用宏(只不過因為你忘記定義了,或者沒有正確include所需的頭文件),編譯器卻以為你在說函數或者變量——當然找不到了。
舉個例子,宏 __STDC_VERSION__ 可以被用來檢查當前ANSI-C的標準:
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的編譯器中是不會有問題的,因為 __STDC_VERSION__ 一定會由編譯器預先定義過;而同樣的代碼放到僅支持C89/90的環境中就有可能會出問題,因為 __STDC_VERSION__ 并不保證一定會被事先定義好(C89/90并沒有規定要提供這個宏),因此 __STDC_VERSION__ 就有可能成為一個未定義的宏,從而觸發編譯器的warning。為了修正這一問題,我們需要對上述內容進行適當的修改:
if defined(__STD_VERSION__) && __STD_VERSION__ >= 199901L
/* support C99 */
...
else
/* doesn't support C99, assume C89/90 */
...
endif
在#ifdef、#ifndef 以及 defined() 表達式中,它可以正確的返回boolean量——確切的表示它被定義了;
在#if 中被直接使用(沒有配合defined()),編譯器會把它看作“空”;在一些數值表達式中,它會被默認當作“0”,沒有任何警告信息會被產生
在除以上情形外的其它地方使用,比如在代碼中使用,編譯器會把它看作“空字符串”(注意,這里不包含引號)——它不會存活到編譯階段;
第一條:任何使用到膠水運算“##”對形參進行粘合的參數宏,一定需要額外的再套一層
第二條:其余情況下,如果要用到膠水運算,一定要在內部借助參數宏來完成粘合過程
#define SAFE_ATOM_CODE(...) \
{ \
uint32_t wTemp = __disable_irq(); \
__VA_ARGS__; \
__set_PRIMASK(wTemp); \
}
由于這里定義了一個變量wTemp,而如果用戶插入的代碼中也使用了同名的變量,就會產生很多問題:輕則編譯錯誤(重復定義);重則出現局部變量wTemp強行取代了用戶自定義的靜態變量的情況,從而直接導致系統運行出現隨機性的故障(比如隨機性的中斷被關閉后不再恢復,或是原本應該被關閉的全局中斷處于打開狀態等等)。為了避免這一問題,我們往往會想自動給這個變量一個不會重復的名字,比如借助 __LINE__ 宏給這一變量加入一個后綴:
#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);
}
...
從內容上看,SAFE_ATOM_CODE() 要粘合的對象并不是形參,根據結論第二條,需要借助另外一個參數宏來幫忙完成這一過程。為此,我們需要引入一個專門的宏:
##__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) {...}
對于下面的代碼:
CONNECT2(uint32_t wVariable, EXAMPLE);
如果宏是一個變量,那么展開的結果應該是:
uint32_t wVariable123;
然而,我們實際獲得的是:
uint32_t wVariableEXAMPLE_A;
如何理解這一結果呢?
如果宏是一個引用,那么當EXAMPLE_A與123之間的關系被銷毀時,原本EXAMPLE > EXAMPLE_A > 123 的引用關系就只剩下 EXAMPLE > EXAMPLE_A。又由于EXAMPLE_A已經不復存在,因此EXAMPLE_A在展開時就被當作是最終的字符串,與"uint32_t wVariable"連接到了一起。
usart
USART_INIT(USART1_idx);
usart1_init();
USART_INIT(DEBUG_USART);
/* app_cfg.h */
usart(1+2)_init();
/* 獲取個位 */
/* 獲取十位數字 */
/* 獲取百位數字 */
__MFUNC_OUT_DEC_DIGIT_TEMP0)
__MFUNC_OUT_DEC_DIGIT_TEMP1,\
__MFUNC_OUT_DEC_DIGIT_TEMP0)
/* 建立腳本輸入值與 DEBUG_USART 之間的引用關系*/
/* "調用"轉換腳本 */
/* 建立 DEBUG_USART 與腳本輸出值之間的引用 */
USART_INIT(DEBUG_USART);
打完收工。
往期推薦
如果喜歡這篇文章,請點贊、在看,支持一下哦~謝謝!