【為宏正名】for的妙用你想不到
掃描二維碼
隨時(shí)隨地手機(jī)看文章
【說(shuō)在前面的話(huà)】
【被低估的價(jià)值】
for ( init_clause ; cond_expression ; iteration_expression )
loop_statement
for 循環(huán)中的 cond_expression 和 interation_expression 都必須是表達(dá)式,而不能是直接的語(yǔ)句。
for 循環(huán)中第一個(gè)部分 init_clause 一開(kāi)始是用來(lái)放置給變量賦值的表達(dá)式;但從ANSI-C99開(kāi)始,init_clause 可以被用來(lái)建立局部變量;而局部變量的生命周期覆蓋且僅覆蓋整個(gè)for循環(huán)——這一點(diǎn)非常有利用價(jià)值,也是大家容易忽略的地方。
int i = 0;
...
for (i = 0; i < 100; i++) {
...
}
int i = 0, j,k;
...
for (i = 0, j = 100, k = 1; i < 100; i++) {
...
}
for (int i = 0, j = 100, k = 1; i < 100; i++) {
...
}
for (int i = 0, short j = 100; i < 100; i++) {
...
}
for (int i = 0, *p = NULL; i < 100; i++) {
...
}
另外一個(gè)值得注意的是 for 的執(zhí)行順序,它可以用下面的流程圖來(lái)表示:
圖中灰色的部分為原本實(shí)際的執(zhí)行流程,而純黑色的線(xiàn)條以及最下方的虛線(xiàn)箭頭則為等效的運(yùn)行流程。與do {} while(0) 相比,在我們眼中 for 循環(huán)的幾個(gè)關(guān)鍵部分就有了新的意義:
在執(zhí)行用戶(hù)代碼之前(灰色部分),有能力進(jìn)行一定的“準(zhǔn)備工作”(Before部分);
在執(zhí)行用戶(hù)代碼之后,有能力執(zhí)行一定的“收尾工作”(After部分)
在init_clause階段有能力定義一個(gè)“僅僅只覆蓋” for 循環(huán)的,并且只對(duì) User Code可見(jiàn)的局部變量——換句話(huà)說(shuō),這些局部變量是不會(huì)污染 for 循環(huán)以外的地方的。
【構(gòu)造using結(jié)構(gòu)】
using (StreamReader tReader = File.OpenText(m_InputTextFilePath))
{
while (!tReader.EndOfStream)
{
...
}
}
以上述代碼為例進(jìn)行講解:
在 using 圓括號(hào)內(nèi)定義的變量,其生命周期僅覆蓋 using 緊隨其后的花括號(hào)內(nèi)部;
當(dāng)用于代碼離開(kāi) using 結(jié)構(gòu)的時(shí)候,using 會(huì)自動(dòng)執(zhí)行一個(gè)“掃尾工作”,而這個(gè)掃尾工作是對(duì)應(yīng)的類(lèi)事先定義好的。在上述例子中,所謂的掃尾工作就是關(guān)閉 與 類(lèi)StreamReader的實(shí)例tReader 所關(guān)聯(lián)的文件——簡(jiǎn)單說(shuō)就是using會(huì)自動(dòng)把文件關(guān)閉,而不必用戶(hù)親自動(dòng)手。
for (int i = 1; i > 0; i++) {
...
}
以此為起點(diǎn),對(duì)比我們的“藍(lán)圖”,發(fā)現(xiàn)至少有以下幾個(gè)問(wèn)題:
如何實(shí)現(xiàn) before和after的部分?
現(xiàn)在用的變量 i 固定是 int 類(lèi)型的,如何允許用戶(hù)在 init_clause 定義自己的局部變量,并允許使用自己的類(lèi)型?
問(wèn)題一:如何實(shí)現(xiàn) before 和 after 部分
//! 假設(shè)用戶(hù)要插入的內(nèi)容我們都放在叫做 before 和after的函數(shù)里
extern void before(void);
extern void after(void);
for (int i = 1; //!< init_clause
i--?(before(),1):0; //!< cond_expression
after()) //!< iteration_expression
{
...
}
(i--) ? 1 : 0
(i--) ?
(before(), 1) //!< 使用逗哈表達(dá)式進(jìn)行擴(kuò)展
: 0
由于逗號(hào)表達(dá)式只管 最右邊的結(jié)果,忽略所有左邊的返回值,因此,哪怕before()函數(shù)沒(méi)有實(shí)際返回值對(duì)C編譯器來(lái)說(shuō)都是無(wú)所謂的。同理,由于我們?cè)赾ond_expression部分已經(jīng)完成了所有功能,因此 iteration_expression 就任由我們?cè)赘盍恕幾g器原本就對(duì)此處表達(dá)式所產(chǎn)生的數(shù)值并不感興——我們直接放下 after() 函數(shù)即可。
#define using(__declare, __on_enter_expr, __on_leave_expr) \
for (__declare, *_ptr = NULL; \
_ptr++ == NULL ? \
((__on_enter_expr),1) : 0; \
__on_leave_expr \
)
using(int a = 0,printf("========= On Enter =======\r\n"),
printf("========= On Leave =======\r\n"))
{
printf("\t In Body a=%d \r\n", ++a);
}
這是對(duì)應(yīng)的執(zhí)行效果:
我們不妨將上述的宏進(jìn)行展開(kāi),一個(gè)可能的結(jié)果是:
for (int a = 0, *_ptr = NULL;
_ptr++ == NULL ? ((printf("========= On Enter =======\r\n")),1) : 0;
printf("========= On Leave =======\r\n") )
{
printf("\t In Body a=%d \r\n", ++a);
}
從 init_clause 的展開(kāi)結(jié)果來(lái)看,完全符合要求:
int a = 0, *_ptr = NULL;
#define using(__declare, __on_enter_expr, __on_leave_expr) \
for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL; \
CONNECT3(__using_, __LINE__,_ptr)++ == NULL ? \
((__on_enter_expr),1) : 0; \
__on_leave_expr \
)
這里,實(shí)際上是使用了前面文章中介紹的宏 CONNECT3() 將 “__using_”,__LINE__所表示的當(dāng)前行號(hào),以及 "_ptr" 粘連在一起,形成一個(gè)唯一的局部變量名:
CONNECT3(__using_, __LINE__,_ptr)
如果你對(duì) CONNECT() 宏的來(lái)龍去脈感興趣,可以單擊這里。
#define __using1(__declare) \
for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL; \
CONNECT3(__using_, __LINE__,_ptr)++ == NULL; \
)
#define __using2(__declare, __on_leave_expr) \
for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL; \
CONNECT3(__using_, __LINE__,_ptr)++ == NULL; \
__on_leave_expr \
)
#define __using3(__declare, __on_enter_expr, __on_leave_expr) \
for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL; \
CONNECT3(__using_, __LINE__,_ptr)++ == NULL ? \
((__on_enter_expr),1) : 0; \
__on_leave_expr \
)
#define __using4(__dcl1, __dcl2, __on_enter_expr, __on_leave_expr) \
for (__dcl1, __dcl2, *CONNECT3(__using_, __LINE__,_ptr) = NULL; \
CONNECT3(__using_, __LINE__,_ptr)++ == NULL ? \
((__on_enter_expr),1) : 0; \
__on_leave_expr \
)
借助宏的重載技術(shù),我們可以根據(jù)用戶(hù)輸入的參數(shù)數(shù)量自動(dòng)選擇正確的版本:
#define using(...) \
CONNECT2(__using, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)
至此,我們完成了對(duì) for 的改造,并提出了__using1, __using2, __using3 和 __using4 四個(gè)版本變體。那么問(wèn)題來(lái)了,他們分別有什么用處呢?
【提供不阻礙調(diào)試的代碼封裝】
#define SAFE_ATOM_CODE(...) \
{ \
uint32_t CONNECT2(temp, __LINE__) = __disable_irq(); \
__VA_ARGS__ \
__set_PRIMASK((CONNECT2(temp, __LINE__))); \
}
因此可以很容易的通過(guò)如下的代碼來(lái)保護(hù)關(guān)鍵的寄存器操作:
/**
\fn void wr_dat (uint16_t dat)
\brief Write data to the LCD controller
\param[in] dat Data to write
*/
static __inline void wr_dat (uint_fast16_t dat)
{
SAFE_ATOM_CODE (
LCD_CS(0);
GLCD_PORT->DAT = (dat >> 8); /* Write D8..D15 */
GLCD_PORT->DAT = (dat & 0xFF); /* Write D0..D7 */
LCD_CS(1);
)
}
唯一的問(wèn)題是,這樣的寫(xiě)法,在調(diào)試時(shí)完全沒(méi)法在用戶(hù)代碼處添加斷點(diǎn)(編譯器會(huì)認(rèn)為宏內(nèi)所有的內(nèi)容都寫(xiě)在了同一行),這是大多數(shù)人不喜歡使用宏來(lái)封裝代碼結(jié)構(gòu)的最大原因。借助 __using2,我們可以輕松的解決這個(gè)問(wèn)題:
#define SAFE_ATOM_CODE() \
__using2( uint32_t CONNECT2(temp,__LINE__) = __disable_irq(), \
__set_PRIMASK(CONNECT2(temp,__LINE__)))
修改上述的代碼為:
static __inline void wr_dat (uint_fast16_t dat)
{
SAFE_ATOM_CODE() {
LCD_CS(0);
GLCD_PORT->DAT = (dat >> 8); /* Write D8..D15 */
GLCD_PORT->DAT = (dat & 0xFF); /* Write D0..D7 */
LCD_CS(1);
}
}
由于using的本質(zhì)是 for 循環(huán),因?yàn)槲覀兛梢酝ㄟ^(guò)花括號(hào)的形式來(lái)包裹用戶(hù)代碼,因此,可以很方便的在用戶(hù)代碼中添加斷點(diǎn),單步執(zhí)行。至于原子保護(hù)的功能,我們不妨將上述代碼進(jìn)行宏展開(kāi):
static __inline void wr_dat (uint_fast16_t dat)
{
for (uint32_t temp154 = __disable_irq(), *__using_154_ptr = NULL;
__using_154_ptr++ == NULL ? ((temp154 = temp154),1) : 0;
__set_PRIMASK(temp154) )
{
LCD_CS(0);
GLCD_PORT->DAT = (dat >> 8);
GLCD_PORT->DAT = (dat & 0xFF);
LCD_CS(1);
}
}
通過(guò)觀(guān)察,容易發(fā)現(xiàn),這里巧妙使用 init_clause 給 temp154 變量進(jìn)行賦值——在關(guān)閉中斷的同時(shí)保存了此前的狀態(tài);并在原本 after 的位置放置了 恢復(fù)中斷的語(yǔ)句 __set_PRIMASK(temp154)。
在OOPC中自動(dòng)創(chuàng)建類(lèi),并使用 before 部分來(lái)執(zhí)行構(gòu)造函數(shù);在 after 部分完成 類(lèi)的析構(gòu)。
在外設(shè)操作中,在 init_clause 部分定義指向外設(shè)的指針;在 before部分 Enable或者Open外設(shè);在after部分Disable或者Close外設(shè)。
在RTOS中,在 before 部分嘗試進(jìn)入臨界區(qū);在 after 部分釋放臨界區(qū)
在文件操作中,在 init_clause 部分嘗試打開(kāi)文件,并獲得句柄;在 after 部分自動(dòng) close 文件句柄。
在有MPU進(jìn)行內(nèi)存保護(hù)的場(chǎng)合,在 before 部分,重新配置MPU獲取目標(biāo)地址的訪(fǎng)問(wèn)權(quán)限;在 after部分再次配置MPU,關(guān)閉對(duì)目標(biāo)地址范圍的訪(fǎng)問(wèn)權(quán)限。
……
【構(gòu)造with塊】
你鄰居的->朋友的->親戚家的->一個(gè)狗的->保姆的->手機(jī)
WITH 你鄰居的->朋友的->親戚家的->一個(gè)狗的->保姆的->手機(jī)
# 這里可以直接訪(fǎng)問(wèn)手機(jī)的各項(xiàng)屬性,用 “.” 開(kāi)頭就行
. 手機(jī)殼顏色 = xxxxx
. 貼膜 = 玻璃膜
END WITH
不光是Visual Basic,我們使用C語(yǔ)言進(jìn)行大規(guī)模的應(yīng)用開(kāi)發(fā)時(shí),或多或少也會(huì)遇到同樣的情況,比如,配置 STM32 外設(shè)時(shí),填寫(xiě)外設(shè)配置結(jié)構(gòu)體的時(shí)候,每一行都要重新寫(xiě)一遍結(jié)構(gòu)體變量的名字,也是在是很繁瑣:
static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef();
s_UARTHandle.Instance = USART2;
s_UARTHandle.Init.BaudRate = 115200;
s_UARTHandle.Init.WordLength = UART_WORDLENGTH_8B;
s_UARTHandle.Init.StopBits = UART_STOPBITS_1;
s_UARTHandle.Init.Parity = UART_PARITY_NONE;
s_UARTHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
s_UARTHandle.Init.Mode = UART_MODE_TX_RX;
入股有了with塊的幫助,上述代碼可能就會(huì)變得更加清爽,比如:
static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef();
with(s_UARTHandle) {
.Instance = USART2;
.Init.BaudRate = 115200;
.Init.WordLength = UART_WORDLENGTH_8B;
.Init.StopBits = UART_STOPBITS_1;
.Init.Parity = UART_PARITY_NONE;
.Init.HwFlowCtl = UART_HWCONTROL_NONE;
.Init.Mode = UART_MODE_TX_RX;
}
遺憾的是,如果要完全實(shí)現(xiàn)上述的結(jié)構(gòu),在C語(yǔ)言中是不可能的,但借助我們的 using() 結(jié)構(gòu),我們可以做到一定程度的模擬:
#define with(__type, __addr) using(__type *_p=(__addr))
#define _ (*_p)
在這里,我們要至少提供目標(biāo)對(duì)象的類(lèi)型,以及目標(biāo)對(duì)象的地址:
static UART_HandleTypeDef s_UARTHandle = UART_HandleTypeDef();
with(UART_HandleTypeDef &s_UARTHandle) {
_.Instance = USART2;
_.Init.BaudRate = 115200;
_.Init.WordLength = UART_WORDLENGTH_8B;
_.Init.StopBits = UART_STOPBITS_1;
_.Init.Parity = UART_PARITY_NONE;
_.Init.HwFlowCtl = UART_HWCONTROL_NONE;
_.Init.Mode = UART_MODE_TX_RX;
}
注意到,這里“_”實(shí)際上被用來(lái)替代 s_UARTHandle——雖然感覺(jué)有點(diǎn)不夠完美,但考慮到腳本語(yǔ)言 perl 有長(zhǎng)期使用 "_" 表示本地對(duì)象的傳統(tǒng),這樣一看,似乎"_" 就是一個(gè)對(duì) "perl" 的完美致敬了。
【回歸本職 foreach】
typedef struct example_lv0_t {
uint32_t wA;
uint16_t hwB;
uint8_t chC;
uint8_t chID;
} example_lv0_t;
example_lv0_t s_tItem[8] = {
{.chID = 0},
{.chID = 1},
{.chID = 2},
{.chID = 3},
{.chID = 4},
{.chID = 5},
{.chID = 6},
{.chID = 7},
};
我們希望實(shí)現(xiàn)一個(gè)函數(shù),能通過(guò) foreach 自動(dòng)的訪(fǎng)問(wèn)數(shù)組 s_tItem 的所有成員,比如:
foreach(example_lv0_t, s_tItem) {
printf("Processing item with ID = %d\r\n", _.chID);
}
跟With塊一樣,這里我們?nèi)匀弧爸戮础?perl——使用 "_" 表示當(dāng)前循環(huán)下的元素。在這個(gè)例子中,為了使用 foreach,我們需要提供至少兩個(gè)信息:目標(biāo)數(shù)組元素的類(lèi)型(example_lv0_t)和目標(biāo)數(shù)組(s_tItem)。
#define dimof(__array) (sizeof(__array)/sizeof(__array[0]))
#define foreach(__type, __array) \
__using1(__type *_p = __array) \
for ( uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array); \
CONNECT2(count,__LINE__) > 0; \
_p++, CONNECT2(count,__LINE__)-- \
)
上述的宏并不復(fù)雜,大家完全可以自己看懂,唯一需要強(qiáng)調(diào)的是,using() 的本質(zhì)是一個(gè)for,因此__using1() 下方的for 實(shí)際上是位于由 __using1() 所提供的循環(huán)體內(nèi)的,也就是說(shuō),這里的局部變量_p其作用域也覆蓋 下面的for 循環(huán),這就是為什么我們可以借助:
#define _ (*_p)
的巧妙代換,通過(guò) “_” 來(lái)完成對(duì)指針“_p”的使用。為了方便大家理解,我們不妨將前面的例子代碼進(jìn)行宏展開(kāi):
for (example_lv0_t *_p = s_tItem, *__using_177_ptr = NULL;
__using_177_ptr++ == NULL ? ((_p = _p),1) : 0;
)
for ( uint_fast32_t count177 = (sizeof(s_tItem)/sizeof(s_tItem[0]));
count177 > 0;
_p = _p+1, count177-- )
{
printf("Processing item with ID = %d\r\n", (*_p).chID);
}
其執(zhí)行結(jié)果為:
#define foreach2(__type, __array) \
using(__type *_p = __array) \
for ( uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array); \
CONNECT2(count,__LINE__) > 0; \
_p++, CONNECT2(count,__LINE__)-- \
)
#define foreach3(__type, __array, __item) \
using(__type *_p = __array, *__item = _p, _p = _p, ) \
for ( uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array); \
CONNECT2(count,__LINE__) > 0; \
_p++, __item = _p, CONNECT2(count,__LINE__)-- \
)
這里的 foreach3 提供了3個(gè)參數(shù),其中最后一個(gè)參數(shù)就是用來(lái)由用戶(hù)“額外”指定新的指針的;與之相對(duì),老版本的foreach我們稱(chēng)之為 foreach2,因?yàn)樗恍枰獌蓚€(gè)參數(shù),只能使用"_"作為對(duì)象的指代。進(jìn)一步的,我們可以使用宏的重載來(lái)簡(jiǎn)化用戶(hù)的使用:
#define foreach(...) \
CONNECT2(foreach, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)
經(jīng)過(guò)這樣的改造,我們可以用下面的方法來(lái)為我們的循環(huán)指定一個(gè)叫做"ptItem"的指針:
foreach(example_lv0_t, s_tItem, ptItem) {
printf("Processing item with ID = %d\r\n", ptItem->chID);
}
展開(kāi)后的形式如下:
for (example_lv0_t *_p = s_tItem, ptItem = _p, *__using_177_ptr = NULL;
__using_177_ptr++ == NULL ? ((_p = _p),1) : 0;
)
for ( uint_fast32_t count177 = (sizeof(s_tItem)/sizeof(s_tItem[0]));
count177 > 0;
_p = _p+1, ptItem = _p, count177-- )
{
printf("Processing item with ID = %d\r\n", ptItem->chID);
}
代碼已經(jīng)做了適當(dāng)?shù)恼归_(kāi)和縮進(jìn),這里就不作進(jìn)一步的分析了。
【后記】
宏不是奇技淫巧
宏可以封裝出其它高級(jí)語(yǔ)言所提供的“基礎(chǔ)設(shè)施”
設(shè)計(jì)良好的宏可以提升代碼的可讀性,而不是破壞它
設(shè)計(jì)良好的宏并不會(huì)影響調(diào)試
宏可以用來(lái)固化某些模板,避免每次都重新編寫(xiě)復(fù)雜的語(yǔ)法結(jié)構(gòu),在這里,using() 模板的出現(xiàn),避免了我們每次都重復(fù)通過(guò)原始的 for 語(yǔ)句來(lái)構(gòu)造所需的語(yǔ)法結(jié)構(gòu),極大的避免了重復(fù)勞動(dòng),以及由重復(fù)勞動(dòng)所帶來(lái)的出錯(cuò)風(fēng)險(xiǎn)
如果你喜歡我的思維、
如果你覺(jué)得我的文章對(duì)你有所啟發(fā)或是幫助,
還請(qǐng)“點(diǎn)贊、收藏、轉(zhuǎn)發(fā)” 三連!
歡迎訂閱 裸機(jī)思維
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀(guān)點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!