【說在前面的話】
//! 非閏年的情況下,一年中有多少秒
static uint32_t s_wTotalSecInAYear = SEC_IN_A_YEAR;
例子雖然簡單,但立馬引出了一個有趣的問題:宏展開后,make時編譯器看到的究竟是上述常量表達式的計算結果:
static uint32_t s_wTotalSecInAYear = 31536000ul;
還是原樣的字符串替換呢?
static uint32_t s_wTotalSecInAYear = (60ul * 60ul * 24ul * 365ul);
感興趣的讀者可以通過“-E”來研究一下:
SET PATH=C:\Keil_v5\ARM\ARMCLANG\Bin;
armclang -xc -std=gnu11 --target=arm-arm-none-eabi -mcpu=cortex-m4 -E -o "preprocessed_main.c" "main.c"
這里,命令行使用 armclang(Arm Compiler 6)對 “main.c”進行預編譯("-E"的結果),并將結果輸出到一個名為“preprocessed_main.c” 的文件中——而這一文件就是我們在后面文章中要經常觀察的,比如,針對前面的例子,一個可能的輸出結果是:
static uint32_t s_wTotalSecInAYear = (60ul * 60ul * 24ul * 365ul);
【數位拼接律】
借助上一篇文章中引入的膠水宏 CONNECT3():
我們可以把這三個宏粘貼在一起:
我們當然知道,最終宏替換的結果肯定是字符串“255”,但這個拼接出來的字符串“255”,和十進制數字255是等效的么?換句話說,預編譯器懂得這個字符串“255”的含義么?為了驗證這一問題,我們不妨使用下面的代碼,去直接問問預編譯器本人的看法:
在 “main.c” 中加入上述全部宏定義以后,進行預編譯,我們會得到如下的結果:
驚呆了!拼接出來的字符串不僅被正確的當作十進制數字256使用,還可以與十六進制數字進行正確的比較!!
這是不是意味著:無論是十進制、十六進制,我們只要想辦法得到對應的“數位”,就可以通過拼接的方法還原出所需進制的“常熟字符串”,而且與編譯器還懂得這一字符串的數學意義!——沒錯,這就是上一篇文章的最后,我們能夠1)把任意通過宏編寫的常量表達式計算出結果,并2)將數值轉換成十進制字符串的原理——恍然大悟的同學可以“單擊這里去重溫一下”,這里就不再贅述了。
【序號自增】
預編譯器能夠理解“數字字符串”的數值意義;
宏的本質是一個對目標字符串的引用;
目標字符串是個常量,修改常量是不可能的;
推論:
假設一個宏表示一個序號
我們可以根據當前宏的值,計算出下一個序號的值,并借助“數位拼接律”生成一個新的字符串
修改宏的引用關系,讓它指向新生成的字符串
根據上篇文章中引入的腳本頭文件"mf_u8_dec2str.h",我們可以實現上述效果:
//! 一個用于表示序號的宏,初值是0
每次使用下面的預編譯代碼,我們就可以實現將 MY_INDEX的值加一的效果:
//! MFUNC_IN_U8_DEC_VALUE = MY_INDEX + 1; 給腳本提供輸入
//! 讓預編譯器執行腳本
//! MY_INDEX = MFUNC_OUT_DEC_STR; 獲得腳本輸出
可以看到,雖然原理上可行,如果真用這種方法寫代碼,別說可讀性差到爹媽都不認識,就算大家都能看懂,使用起來實在特別麻煩!否決!
typedef struct node_item_t node_item_t
struct node_item_t {
node_item_t *ptNext; //!< 指下一個元素
//! 鏈表節點的其它成員
uint8_t chID; //!< 假設有一個元素是序號
...
};
實際使用的時候,無論運行時刻鏈表的內容和結構是否會發生變化,但在編譯時刻,我們會給他一些指定數量的初始的節點(比如16個),用數組來存儲:
static node_item_t s_tItemPool[16];
static node_item_t *s_ptListRoot = NULL;
一般來說,我們需要編寫一個初始化函數——在運行時刻將 s_tItemPool 中的元素一個一個手工加入到鏈表中(添加到 s_ptListRoot 指向的鏈表中)——這里的代價是雙份的:
初始化函數所占用的代碼空間
和
添加節點的運行時間。
借助__COUNTER__我們可以直接在編譯時刻,以數組初始值的形式完成鏈表的初始化:
{ \
.ptNext = &((__LIST_ADDR)[(__COUNTER__ + 1]), \
__VA_ARGS__ \
}
{ \
.ptNext = NULL, \
__VA_ARGS__ \
}
借助這個宏,我們可以實現對鏈表的靜態初始化:
static node_item_t s_tItemPool[] = {
ADD_ITEM_TO(s_tItemPool), //!< 添加節點0
ADD_ITEM_TO(s_tItemPool), //!< 添加節點1
...
ADD_ITEM_TO(s_tItemPool), //!< 添加節點n-1
ADD_FINAL_ITEM(s_tItemPool), //!< 添加最后一個節點
};
static node_item_t *s_ptListRoot = s_tItemPool;
注意到節點內還有一個節點的序號“chID”,我們其實也可以一并將其自動初始化了——當然要記住,每次使用__COUNTER__它的值都會增加1——修改宏如下:
#define ADD_ITEM_TO(__LIST_ADDR, ...) \
\
.ptNext = &((__LIST_ADDR)[(__COUNTER__/2 + 1]), \
.chID = (__COUNTER__ / 2), \
__VA_ARGS__ \
}
#define ADD_FINAL_ITEM(__LIST_ADDR, ...) \
\
.ptNext = NULL, \
.chID = (__COUNTER__ / 2), \
__VA_ARGS__ \
}
修改后,實際展開效果如下:
static node_item_t s_tItemPool[] = {
{ .ptNext = &((s_tItemPool)[(0/2 + 1]), .chID = (1 / 2), },
{ .ptNext = &((s_tItemPool)[(2/2 + 1]), .chID = (3 / 2), },
...
{ .ptNext = &((s_tItemPool)[(4/2 + 1]), .chID = (5 / 2), },
{ .ptNext = NULL, .chID = (6 / 2), },
};
static node_item_t *s_ptListRoot = s_tItemPool;
上述效果雖然看似令人滿意,但存在一個巨大的隱患,而這一隱患同樣來自于__COUNTER__宏的基本特性:每次使用__COUNTER__它的值都會增加1——換句話說,在你使用 ADD_ITEM_TO() 的時候,如何才能確保 __COUNTER__是從0開始編號的呢?——別的宏可能已經使用過它了。
無論 __COUNTER__ 是什么值,我們都可以將其傳遞給一個枚舉——作為初始值;
使用 __COUNTER__ 時,我們首先通過枚舉將初始值扣除,從而獲得“從0開始的計數”
enum { \
/* 這里 "+1" 是把本次使用__COUNTER__也算進去 */ \
list_
}; \
static node_item_t s_tList
}; \
static node_item_t *LIST_ROOT(__NAME) = \
s_tList
__IMP_LIST(__NAME, __VA_ARGS__)
{ \
.ptNext = &(s_tList
(__COUNTER__ - list_
.chID = ((__COUNTER__ - list_
__VA_ARGS__ \
}
__ADD_ITEM_TO(__NAME, __VA_ARGS__)
{ \
.ptNext = NULL, \
.chID = ((__COUNTER__ - list_
__VA_ARGS__ \
}
__ADD_FINAL_ITEM(__NAME, __VA_ARGS__)
為了方便隱藏定義枚舉的“小動作”,我們追加了一對宏 IMP_LIST() 和 END_IMP_LIST(),就是"implement list"的縮寫,它實現了以下功能:
以指定的名字定義了一個枚舉;
以指定的名字定義了鏈表的節點池;
以指定的名字定義了指向鏈表的根指針,用戶可以通過宏LIST_ROOT()來獲取這一指針;
修改應用代碼,實現一個叫做 MyList 的鏈表:
//! 實現一個list,名字叫 MyList
IMP_LIST(MyList)
ADD_ITEM_TO(MyList), //!< 添加節點0
ADD_ITEM_TO(MyList), //!< 添加節點1
...
ADD_ITEM_TO(MyList), //!< 添加節點n-1
ADD_FINAL_ITEM(MyList), //!< 添加最后一個節點
END_IMP_LIST(MyList)
是不是看起來很“優雅”?實際展開效果如下:
enum { list_MyList_start = 0 + 1, };
static node_item_t s_tListMyListPool[] = {
{ .ptNext = &(s_tListMyListPool[ (1 - list_MyList_start)/2 + 1]), .chID = ((2 - list_MyList_start) / 2), },
{ .ptNext = &(s_tListMyListPool[ (3 - list_MyList_start)/2 + 1]), .chID = ((4 - list_MyList_start) / 2), },
...
{ .ptNext = &(s_tListMyListPool[ (5 - list_MyList_start)/2 + 1]), .chID = ((6 - list_MyList_start) / 2), },
{ .ptNext = NULL, .chID = ((7 - list_MyList_start) / 2), },
};
static node_item_t *s_ptListMyListRoot = s_tListMyListPool;
【參數宏也支持重載?】
什么是參數宏的重載?——要回答這個問題,哪怕你連“重載(overload)”是什么都不知道也不要緊,我們來看一個最實際的例子:在前面的文章中,我們不止一次使用過一個膠水宏 CONNECT3,它的作用是將三個字符串粘連在一起變成一個完整的字符串。如果我們要粘連的字符串數量不同,比如,2個、4個、5個……n個,我們就要編寫對應的版本:
...
__0
__0
//! 安全“套”
...
__CONNECT8(__0, __1, __2, __3, __4, __5, __6, __7)
__CONNECT9(__0, __1, __2, __3, __4, __5, __6, __7, __8)
這里定義了最大連接9個的CONNECT版本,看似麻煩,實際上復制粘貼、一勞永逸——還是挺劃算的——當然,如果你比較“耿直”,還可以做得更多,比如16個。所謂宏的重載是說:我們不必親自去數要粘貼的字符串的數量而“手工選取正確的版本”,而直接讓編譯器自己替我們挑選。
比如,我們舉一個組裝16進制數字的例子:
CONNECT3(0x, __B1, __B0)
CONNECT5(0x, __B3, __B2, __B1, __B0)
CONNECT9(0x, __B7, __B6, __B4, __B4, __B3, __B2, __B1, __B0)
在支持重載的情況下,我們希望這樣使用:
CONNECT(0x, __B1, __B0)
CONNECT(0x, __B3, __B2, __B1, __B0)
CONNECT(0x, __B7, __B6, __B4, __B4, __B3, __B2, __B1, __B0)
如你所見,無論實際給出的參數是多少個,我們都可以使用同一個參數宏CONNECT(),而CONNCT() 會自動計算用戶給出參數的個數,從而正確的替換為CONNETn()版本。假設這一切都是可能做到的,那么實際上我們還可以對上述宏定義進行簡化:
HEX_VALUE(__B1, __B0)
HEX_VALUE(__B3, __B2, __B1, __B0)
HEX_VALUE(__B7, __B6, __B4, __B4, __B3, __B2, __B1, __B0)
是的,一個 HEX_VALUE() 就足夠了,你隨便添幾個參數都行(只要小于等于你實現的CONNECTn的數量)。
CONNECT2(CONNECT, VA_NUM_ARGS(__VA_ARGS__)) /*part1*/\
(__VA_ARGS__) /*part2*/
uint16_t hwValue = HEX_VALUE(D, E, A, D); //! 0xDEAD
uint16_t hwValue = CONNECT(0x, D, E, A, D);
uint16_t hwValue =
CONNECT2(CONNECT, VA_NUM_ARGS(0x, D, E, A, D))
(0x, D, E, A, D);
uint16_t hwValue =
CONNECT5
(0x, D, E, A, D);
VA_NUM_ARGS_IMPL(__VA_ARGS__,9,8,7,6,5,4,3,2,1)
在涉及"..."之前,它要用用戶至少傳遞10個參數;
這個宏的返回值就是第十個參數的內容;
多出來的部分會被"..."吸收掉,不會產生任何后果
VA_NUM_ARGS() 的巧妙在于,它把__VA_ARGS__放在了參數列表的最前面,并隨后傳遞了 "9,8,7,6,5,4,3,2,1" 這樣的序號:
當__VA_ARGS__里有1個參數時,“1”對應第十個參數__N,所以返回值是1
當__VA_ARGS__里有2個參數時,“2”對應第十個參數__N,所以返回值是2
...
當__VA_ARGS__里有9個參數時,"9"對應第十個參數__N,所以返回值是9
如果覺得上述過程似懂非懂,我們不妨對前面的例子做一個展開:
VA_NUM_ARGS(0x, D, E, A, D)
VA_NUM_ARGS_IMPL(0x, D, E, A, D,9,8,7,6,5,4,3,2,1)
宏的重載非常有用,可以極大的簡化用戶"選擇困難",你甚至可以將VA_NUM_ARGS() 與 函數名結合在一起,從而實現簡單的函數重載(即,函數參數不同的時候,可以通過這種方法在編譯階段有預編譯器根據用戶輸入參數的數量自動選擇對應的函數),比如:
extern device_write1(const char *pchString);
extern device_write2(uint8_t *pchStream, uint_fast16_t hwLength);
extern device_write3(uint_fast32_t wAddress, uint8_t *pchStream, uint_fast16_t hwLength);
CONNECT2(device_write, VA_NUM_ARGS(__VA_ARGS__)) \
(__VA_ARGS__)
使用時:
device_write("hello world"); //!< 發送字符串
extern uint8_t chBuffer[32];
device_write(chBuffer, 32); //!< 發送緩沖
//! 向指定偏移量寫數據
#define LCD_DISP_MEM_START 0x4000xxxx
extern uint16_t hwDisplayBuffer[320*240];
device_write(
LCD_DISP_MEM_START,
(uint8_t *)hwDisplayBuffer,
sizeof(hwDisplayBuffer)
);
往期推薦
如果喜歡這篇文章,請點贊、在看,支持一下哦~謝謝!