【說在前面的話】javascript
經過本系列前面兩篇文章的學習,咱們掌握了宏的基本語法和使用規則,諷刺的是這些所謂的「基本語法和規則」卻偏偏是正規C語言教育中所缺失的。本文的內容將創建在前面構築的基礎之上,以for功能的挖掘和封裝爲契機,手把手的教會你如何正確使用宏來簡化平常開發,加強C語言的可讀性、下降應用開發的難度、同時還儘量避免宏對平常代碼調試帶來的負面影響。
php
在開始本文的內容以前,若是你尚未閱讀過前面兩篇文章,能夠單擊下面的連接:
java
基礎必修1:【爲宏正名】本應寫入教科書的「世界設定」nginx
基礎必修2:【爲宏正名】什麼?我忘了去上「數學必修課」!sql
應用範例1:【爲宏正名】99%人都不知道的"##"裏用法express
【被低估的價值】編程
想必你們對C語言中的 for 循環結構並不陌生。根據C/C++語法網站cppreference.com 的介紹,for 的語法結構以下:swift
for ( init_clause ; cond_expression ; iteration_expression ) loop_statement
這裏,我並不想假設你們對 for 結構一無所知,並介紹一堆教科書上已有的內容。然而,在 for 的語法結構中有幾個你們容易忽視的地方,而它們偏偏是本文後續各類「展開」的基礎:
c#
for 循環中的 cond_expression 和 interation_expression 都必須是表達式,而不能是直接的語句。
數組for 循環中第一個部分 init_clause 一開始是用來放置給變量賦值的表達式;但從ANSI-C99開始,init_clause 能夠被用來創建局部變量;而局部變量的生命週期覆蓋且僅覆蓋整個for循環——這一點很是有利用價值,也是你們容易忽略的地方。
爲了說明這一點,咱們不妨舉幾個例子。首先在C99標準以前,若是你要在 for 循環中使用一個循環變量,你只能在進入 for 以前將其定義好:
int i = 0; ... for (i = 0; i < 100; i++) { ... }
如你所見,雖然咱們能夠在 init_clause 的位置對變量賦值,但它並非必須的——多少一點雞肋是否是?也許更雞肋的是,你能夠在 init_clause 這裏完成更多的賦值操做,好比:
int i = 0, j,k; ... for (i = 0, j = 100, k = 1; i < 100; i++) { ... }
實際上,明眼人均可以看出,init_clause 中所做的事情徹底能夠放置到 for 循環以前去完成,還能夠避免「使用逗號進行分隔」 這樣讓人不那麼習慣的使用方式。也許是意識到這一點,C99容許在 init_clause 裏定義局部變量,而正是這一點,徹底改變了 for 的命運(關於這一點,咱們將在隨後的內容中詳細介紹)。如今,上述代碼能夠等效的改寫爲:
for (int i = 0, j = 100, k = 1; i < 100; i++) { ... }
須要強調的是,這裏仍然有一個小小的限制,即:init_clause 裏雖然能夠定義局部變量,但這些變量只能是同一類型的,或者是指向這一類型的指針。所以下面的寫法是非法的:
for (int i = 0, short j = 100; i < 100; i++) { ... }
而這樣的寫法是合法的:
for (int i = 0, *p = NULL; i < 100; i++) { ... }
請你們務必留意這裏的語法細節,咱們將在後面的封裝中大規模使用。
另一個值得注意的是 for 的執行順序,它能夠用下面的流程圖來表示:
容易發現,通過必要的「構造」,咱們能夠剛好實現一個如同 do { } while(0) 同樣的效果:
圖中灰色的部分爲本來實際的執行流程,而純黑色的線條以及最下方的虛線箭頭則爲等效的運行流程。與do {} while(0) 相比,在咱們眼中 for 循環的幾個關鍵部分就有了新的意義:
在執行用戶代碼以前(灰色部分),有能力進行必定的「準備工做」(Before部分);
在執行用戶代碼以後,有能力執行必定的「收尾工做」(After部分)
在init_clause階段有能力定義一個「僅僅只覆蓋」 for 循環的,而且只對 User Code可見的局部變量——換句話說,這些局部變量是不會污染 for 循環之外的地方的。
【構造using結構】
上面所提到的結構,在C#中有一個相似的語法,叫作 using(),其典型的用法以下:
using (StreamReader tReader = File.OpenText(m_InputTextFilePath)) { while (!tReader.EndOfStream) { ... } }
以上述代碼爲例進行講解:
在 using 圓括號內定義的變量,其生命週期僅覆蓋 using 緊隨其後的花括號內部;
當用於代碼離開 using 結構的時候,using 會自動執行一個「掃尾工做」,而這個掃尾工做是對應的類事先定義好的。在上述例子中,所謂的掃尾工做就是關閉 與 類StreamReader的實例tReader 所關聯的文件——簡單說就是using會自動把文件關閉,而沒必要用戶親自動手。
是否是聞到了熟悉的味道?不要搞錯因果關係——咱們正是對C#中的using結構「甚是眼饞」才決定本身動手,用 for 來創造一個——現有C#的using結構纔有咱們後面的嘗試。下圖是using所等校流程圖,能夠看到他比咱們此前的結構還少了一個「Before」部分:
要實現相似using的結構,首先要考慮如何構造一個"至執行一次"的for循環結構。要作到這一點,毫無難度:
for (int i = 1; i > 0; i++) { ... }
以此爲起點,對比咱們的「藍圖」,發現至少有如下幾個問題:
如何實現 before和after的部分?
如今用的變量 i 固定是 int 類型的,如何容許用戶在 init_clause 定義本身的局部變量,並容許使用本身的類型?
問題一:如何實現 before 和 after 部分
對比前面的圖例,咱們知道 before 和 after 的部分實際上分別對應 for 循環的 cond_expression 和 iteration_expression;同時,這兩個部分都必須是表達式——因爲表達式的限制,能插入在 before 和 after 部分的內容實際上就只能是「普通表達式」或者是「函數」。
因爲咱們還必須至少藉助 cond_expression 來實現 「只運行一次」 的功能,如何見縫插針的實現 before 的功能呢?不繞彎子,看代碼:
//! 假設用戶要插入的內容咱們都放在叫作 before 和after的函數裏 extern void before(void); extern void after(void); for (int i = 1; //!< init_clause i--?(before(),1):0; //!< cond_expression after()) //!< iteration_expression { ... }
咱們知道,cond_expression 只在意用戶表達式的返回值是0仍是非0,所以,這裏其實真正起做用的本體是 "i--"——第一次判斷的時候返回值是1,因爲自減操做,第二次判斷的時候就是0了——這就完成了讓 for 運行且只運行一次的功能。
接下來,咱們藉助一個問好表達式,嘗試給 i-- 的結果作一個等效「解釋」,即:
(i--) ? 1 : 0
用人話說就是,若是 (i--)值是非0的,咱們就返回1,反之返回0。這麼作的意義是爲了進一步經過逗號表達式對 "1" 所在的部分進行擴展:
(i--) ? (before(), 1) //!< 使用逗哈表達式進行擴展 : 0
因爲逗號表達式只管 最右邊的結果,忽略全部左邊的返回值,所以,哪怕before()函數沒有實際返回值對C編譯器來講都是無所謂的。同理,因爲咱們在cond_expression部分已經完成了全部功能,所以 iteration_expression 就職由咱們宰割了——編譯器本來就對此處表達式所產生的數值並不感興——咱們直接放下 after() 函數便可。
至此,插入 before() 和 after() 的問題圓滿解決。
問題二:如何容許用戶定義本身的局部變量,而且擁有本身的類型
要解決這個問題,首先必須打破定勢思惟,即:for循環只能用整型變量。實際並不是如此,對for來講真正起做用的只有 cond_expression 的返回值,而它只關心用戶的表達式返回的 布爾量 是什麼——換句話說,有無數種方法來產生 cond_expression,而使用普通的整形計數器,並對其進行判斷只是衆多方法中的一種。
打破了這必定勢思惟後,咱們就從問題自己出發考慮:容許用戶用本身的類型定義本身的變量——雖然看似咱們並不能知道用戶會用什麼類型來定義變量,於是就沒法寫出通用的 cond_expression 來實現「讓for執行且執行一次」的功能,然而,大家也許忘記了 init_clause 的一個特色:它還能夠定義指針——換句話說,不管用戶定義了什麼類型,咱們均可以在最後定義一個指向該類型的指針:
#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); }
這是對應的執行效果:
咱們不妨將上述的宏進行展開,一個可能的結果是:
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 的展開結果來看,徹底符合要求:
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 \ )
這裏,其實是使用了前面文章中介紹的宏 CONNECT3() 將 「__using_」,__LINE__所表示的當前行號,以及 "_ptr" 粘連在一塊兒,造成一個惟一的局部變量名:
CONNECT3(__using_, __LINE__,_ptr)
若是你對 CONNECT() 宏的前因後果感興趣,能夠單擊這裏。
更進一步,若是用戶有不一樣的需求:好比想定義兩個以上的局部變量,或是想省確 __on_enter_expr 或者是 __on_leave_expr ——咱們徹底能夠定義多個不一樣版本的 using:
#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 \ )
藉助宏的重載技術,咱們能夠根據用戶輸入的參數數量自動選擇正確的版本:
#define using(...) \ CONNECT2(__using, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)
至此,咱們完成了對 for 的改造,並提出了__using1, __using2, __using3 和 __using4 四個版本變體。那麼問題來了,他們分別有什麼用處呢?
【提供不阻礙調試的代碼封裝】
前面的文章中,咱們曾有意無心的提供過一個實現原子操做的封裝:即在代碼的開始階段關閉全局中斷並記錄此前的中斷狀態;執行用戶代碼後,恢復關閉中斷前的狀態。其代碼以下:
#define SAFE_ATOM_CODE(...) \ { \ uint32_t CONNECT2(temp, __LINE__) = __disable_irq(); \ __VA_ARGS__ \ __set_PRIMASK((CONNECT2(temp, __LINE__))); \ }
所以能夠很容易的經過以下的代碼來保護關鍵的寄存器操做:
/** \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); ) }
惟一的問題是,這樣的寫法,在調試時徹底無法在用戶代碼處添加斷點(編譯器會認爲宏內全部的內容都寫在了同一行),這是大多數人不喜歡使用宏來封裝代碼結構的最大緣由。藉助 __using2,咱們能夠輕鬆的解決這個問題:
#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的本質是 for 循環,由於咱們能夠經過花括號的形式來包裹用戶代碼,所以,能夠很方便的在用戶代碼中添加斷點,單步執行。至於原子保護的功能,咱們不妨將上述代碼進行宏展開:
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); } }
經過觀察,容易發現,這裏巧妙使用 init_clause 給 temp154 變量進行賦值——在關閉中斷的同時保存了此前的狀態;並在本來 after 的位置放置了 恢復中斷的語句 __set_PRIMASK(temp154)。
觸類旁通,此類方法除了用來開關中斷之外,還能夠用在如下的場合:
在OOPC中自動建立類,並使用 before 部分來執行構造函數;在 after 部分完成 類的析構。
在外設操做中,在 init_clause 部分定義指向外設的指針;在 before部分 Enable或者Open外設;在after部分Disable或者Close外設。
在RTOS中,在 before 部分嘗試進入臨界區;在 after 部分釋放臨界區
在文件操做中,在 init_clause 部分嘗試打開文件,並得到句柄;在 after 部分自動 close 文件句柄。
在有MPU進行內存保護的場合,在 before 部分,從新配置MPU獲取目標地址的訪問權限;在 after部分再次配置MPU,關閉對目標地址範圍的訪問權限。
……
【構造with塊】
不知道大家在實際應用中有沒有遇到一連串指針訪問的情形——提及來就比如是:
你鄰居的->朋友的->親戚家的->一個狗的->保姆的->手機
若是咱們要操做這裏的「手機」,實在是不想每次都寫這麼一長串「噁心」的東西,爲了應對這一問題,Visual Basic(其實最先是Quick Basic)引入了一個叫作 WITH 塊的概念,它的用法以下:
WITH 你鄰居的->朋友的->親戚家的->一個狗的->保姆的->手機 # 這裏能夠直接訪問手機的各項屬性,用 「.」 開頭就行 . 手機殼顏色 = xxxxx . 貼膜 = 玻璃膜 END WITH
不光是Visual Basic,咱們使用C語言進行大規模的應用開發時,或多或少也會遇到一樣的狀況,好比,配置 STM32 外設時,填寫外設配置結構體的時候,每一行都要從新寫一遍結構體變量的名字,也是在是很繁瑣:
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塊的幫助,上述代碼可能就會變得更加清爽,好比:
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; }
遺憾的是,若是要徹底實現上述的結構,在C語言中是不可能的,但藉助咱們的 using() 結構,咱們能夠作到必定程度的模擬:
#define with(__type, __addr) using(__type *_p=(__addr)) #define _ (*_p)
在這裏,咱們要至少提供目標對象的類型,以及目標對象的地址:
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; }
注意到,這裏「_」實際上被用來替代 s_UARTHandle——雖然感受有點不夠完美,但考慮到腳本語言 perl 有長期使用 "_" 表示本地對象的傳統,這樣一看,彷佛"_" 就是一個對 "perl" 的完美致敬了。
【迴歸本職 foreach】
不少高級語言都有專門的 foreach 語句,用來實現對數組(或是鏈表)中的元素進行逐一訪問。原生態C語言並無這種奢侈,即使如此,Linux也定義了一個「野生」的 foreach 來實現相似的功能。爲了演示如何使用 using 結構來構造 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}, };
咱們但願實現一個函數,能經過 foreach 自動的訪問數組 s_tItem 的全部成員,好比:
foreach(example_lv0_t, s_tItem) { printf("Processing item with ID = %d\r\n", _.chID); }
跟With塊同樣,這裏咱們仍然「致敬」 perl——使用 "_" 表示當前循環下的元素。在這個例子中,爲了使用 foreach,咱們須要提供至少兩個信息:目標數組元素的類型(example_lv0_t)和目標數組(s_tItem)。
這裏的難點在於,如何定義一個局部的指針,而且它的做用範圍僅僅只覆蓋 foreach 的循環體。此時,坐在角落裏的 __with1() 按耐不住了,高高的舉起了雙手——是的,它僅有的功能就是容許用戶定義一個局部變量,並覆蓋由第三方所編寫的、由 {} 包裹的區域:
#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__)-- \ )
上述的宏並不複雜,你們徹底能夠本身看懂,惟一須要強調的是,using() 的本質是一個for,所以__using1() 下方的for 其實是位於由 __using1() 所提供的循環體內的,也就是說,這裏的局部變量_p其做用域也覆蓋 下面的for 循環,這就是爲何咱們能夠藉助:
#define _ (*_p)
的巧妙代換,經過 「_」 來完成對指針「_p」的使用。爲了方便你們理解,咱們不妨將前面的例子代碼進行宏展開:
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); }
其執行結果爲:
foreach目前的用法看起來「歲月靜好」,彷佛沒有什麼問題,惋惜的是,一旦進行實際的代碼編寫,咱們會發現,假如咱們要在 foreach 結構中再用一個foreach,或是在foreach中使用 with 塊,就會出現 「_」 被覆蓋的問題——也就是在裏層的 foreach或是 with 沒法經過 「_」 來訪問外層"_" 所表明的對象。爲了應對這一問題,咱們能夠對 foreach 進行一個小小的改造——容許用戶再指定一個專門的局部變量,用於替代"_" 表示當前循環下的對象:
#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個參數,其中最後一個參數就是用來由用戶「額外」指定新的指針的;與之相對,老版本的foreach咱們稱之爲 foreach2,由於它只須要兩個參數,只能使用"_"做爲對象的指代。進一步的,咱們可使用宏的重載來簡化用戶的使用:
#define foreach(...) \ CONNECT2(foreach, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)
通過這樣的改造,咱們能夠用下面的方法來爲咱們的循環指定一個叫作"ptItem"的指針:
foreach(example_lv0_t, s_tItem, ptItem) { printf("Processing item with ID = %d\r\n", ptItem->chID); }
展開後的形式以下:
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); }
代碼已經作了適當的展開和縮進,這裏就不做進一步的分析了。
【後記】
本文的目的,算是對【爲宏正名】系列所介紹的知識進行一次示範——告訴你們如何正確的使用宏,配合已有的老的語法結構來「固化」一個新的模板,並以這個模板爲起點,理解它的語法意義和用戶,簡化咱們的平常開發。在這篇文章中,老的語法結構就是 for,它是由C語言原生支持的,藉助宏,咱們封裝了一個新的語法結構 using(), 藉助它的4種不一樣形式、理解它們各自的特色,咱們又分別封裝了很是實用的SAFE_ATOM_CODE(),With塊和foreach語法結構——他們的存在至少證實瞭如下幾點:
宏不是奇技淫巧
宏能夠封裝出其它高級語言所提供的「基礎設施」
設計良好的宏能夠提高代碼的可讀性,而不是破壞它
設計良好的宏並不會影響調試
宏能夠用來固化某些模板,避免每次都從新編寫複雜的語法結構,在這裏,using() 模板的出現,避免了咱們每次都重複經過原始的 for 語句來構造所需的語法結構,極大的避免了重複勞動,以及由重複勞動所帶來的出錯風險
免責聲明:本文系網絡轉載,版權歸原做者全部。如涉及做品版權問題,請與咱們聯繫,咱們將根據您提供的版權證明材料確認版權並支付稿酬或者刪除內容。