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





