以下文章來源于裸機思維,作者GorgonMeducer 傻孩子
【說在前面的話】
有人的地方就有江湖。我想應該沒人愿意自廢武功吧?
年輕人,你可曾記得,在修習C語言的時候,見過這樣的字句:在創(chuàng)建頭文件的時候,一定要加入保護宏。例如:
/*這是頭文件my_header.h的開頭*/ #ifndef__MY_HEADER_H__ #define __MY_HEADER_H__ /*頭文件的實體內(nèi)容 */ #endif/*endof__MY_HEANDER_H__有好問者打破砂鍋問到底,定有那先來者苦口婆心:這是防止頭文件被有意無意間重復包含的時候出現(xiàn)內(nèi)容重復定義的問題。
此話不虛、亦非假話。
但……它從一開始就隱藏了C語言預處理的一項普普通通的技法,并將其活生生逼成了所謂的武林絕學——并非因為它有怎樣的禁忌,僅僅只是因為自廢武功的人太多——幾近滅絕啊。
【未曾設想的道路】
一般情況下,我們創(chuàng)建的頭文件都可以被歸入“不可重入”的大類,顧名思義,就是如果這個頭文件被同一個 C 源文件直接或間接的包含(include)了多次,那么就會出現(xiàn)“內(nèi)容重復定義”的問題——正因為不可重入,才需要加入保護宏來確保:
頭文件中的內(nèi)容僅在第一次被包含時生效
隨后再次包含該頭文件時,內(nèi)容將被跳過
與“不可重入”的頭文件相對,還有另外一個大類被稱為“可重入的頭文件”——顧名思義,這類頭文件不僅允許出現(xiàn)重復包含,而且每一次包含都會發(fā)揮(一樣或者不一樣的)功能。
其實,在本系列之前的文章《【為宏正名】什么?我忘了去上“數(shù)學必修課”!》就已經(jīng)介紹過一個可重入頭文件mf_u8_dec2str.h 了,它的作用是在每次調(diào)用時“將用戶給定的表達式計算出結(jié)果并轉(zhuǎn)化為十進制字符串”(當然這里的數(shù)值必須小于256),例如:
//! 一個用于表示序號的宏,初值是0 #defineMY_INDEX0
每次使用下面的預編譯代碼,我們就可以實現(xiàn)將 MY_INDEX的值加一的效果:
//!MFUNC_IN_U8_DEC_VALUE=MY_INDEX+ 1;給腳本提供輸入 #defineMFUNC_IN_U8_DEC_VALUE (MY_INDEX+1) //!讓預編譯器執(zhí)行腳本 #include "mf_u8_dec2str.h" #undef MY_INDEX //!MY_INDEX=MFUNC_OUT_DEC_STR;獲得腳本輸出 #define MY_INDEX MFUNC_OUT_DEC_STR
作為一個可重入頭文件,你調(diào)用他多少次都可以——每次都可以發(fā)揮應有的作用。對于這個頭文件的用途和原理感到好奇的小伙伴,不妨單擊這里,重新閱讀一下這篇文章。需要注意的是,最新的源代碼已經(jīng)進行了更新,文章中提及的只是原理,具體實現(xiàn)以最新的源代碼為準:
https://github.com/GorgonMeducer/Generic_MCU_Software_Infrastructure/blob/master/sources/gmsi/utilities/preprocessor/mf_u8_dec2str.h
【重復包含頭文件的意義何在】
我們什么時候回會用到“可重入的頭文件”呢?或者換個問法:“可重入頭文件究竟有何作用”? 從發(fā)揮作用的方式來說,“可重入頭文件”可以被主要分為三大類:
重復提供簡單的預處理服務(比如前面提到過的mf_u8_dec2str.h)
通過遞歸調(diào)用的方式來進行代碼生成(比如在編譯時刻給一個數(shù)組填充0~255的初始值);
為同樣的宏模板提供不同的解釋
第一個大類,我們已經(jīng)在文章【為宏正名】什么?我忘了去上“數(shù)學必修課”!》中詳細介紹過,這里就不再贅述。而借助 mf_u8_dec2str.h 的幫助,我們也可以很輕松的實現(xiàn)第二類功能。
假設,我們要定義一系列數(shù)據(jù),以固定間隔向其中填充指定數(shù)量的初始值,比如:
//2位Alpha 對應 8bit Alpha的備查表 constuint8_tc_chAlphaA4Table[4]={ 0, 85, 170, 255 }; //4位Alpha對應8bitAlpha的備查表 constuint8_tc_chAlphaA4Table[16] = { 0,17,34,51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255 }; // 8位 Alpha 對應 8bit Alpha的備查表 constuint8_tc_chAlphaA8Table[256] = { 0,1,2,3...255, };
另外,別問我為啥有這么傻的代碼,LVGL源代碼中就有,而且非常合理。
https://github.com/lvgl/lvgl/blob/master/src/draw/sw/lv_draw_sw_letter.c
所以,就不要質(zhì)疑這里的合理性——我也只是舉個例子,作為技術(shù)介紹,能簡單的把事情講清楚,用簡單的例子無可厚非,領(lǐng)會精神即可。
理想中,如果有一個可重入的頭文件 mf_u8_fill_dec.h,它接受三個宏作為輸入參數(shù):
MFUNC_IN_START——起始數(shù)字
MFUNC_IN_DELTA——間隔
MFUNC_IN_COUNT——填充的數(shù)量
那么上述代碼完全可以改寫成以下的形式:
//2位Alpha 對應 8bit Alpha的備查表 constuint8_tc_chAlphaA4Table[4]={ #define MFUNC_IN_START 0 #defineMFUNC_IN_COUNT4 #defineMFUNC_IN_DELTA(255/(MFUNC_COUNT-1)) #include “mf_u8_fill_dec.h” }; //4位Alpha對應8bitAlpha的備查表 constuint8_tc_chAlphaA4Table[16] = { #define MFUNC_IN_START 0 #define MFUNC_IN_COUNT 16 #defineMFUNC_IN_DELTA(255/(MFUNC_COUNT-1)) #include “mf_u8_fill_dec.h” }; // 8位 Alpha 對應 8bit Alpha的備查表 constuint8_tc_chAlphaA8Table[256] = { #define MFUNC_IN_START 0 #defineMFUNC_IN_COUNT256 #define MFUNC_IN_DELTA (255 / (MFUNC_COUNT - 1)) #include “mf_u8_fill_dec.h” };
是不是簡單多了?——苦力活讓預編譯器去做,我們只管描述任務本身即可。
那么要如何實現(xiàn)mf_u8_fill_dec.h呢?這就離不開“可重入頭文件”的固定結(jié)構(gòu)了。
【可重復頭文件的固定結(jié)構(gòu)】
可重入頭文件的基本結(jié)構(gòu)一般固定為5個分區(qū),如下圖所示:
文檔區(qū):主要用于放置頭文件使用說明,當然,也包括可選的License和版本信息等;
輸入?yún)?shù)檢查區(qū):對作為輸入?yún)?shù)的宏進行必要的檢測,比如:
如果用戶忘記定義某些可選參數(shù)時提供默認值
如果用戶忘記定義某些必填的參數(shù)時,提供錯誤提示
如果用戶給的輸入?yún)?shù)非法時,提供錯誤提示
#undef 區(qū):對功能區(qū)里會定義的宏首先進行無腦 undef
功能區(qū):實現(xiàn)具體功能的區(qū)域,一般會包含如下的內(nèi)容:
定義一些宏、帶參數(shù)的宏等等
進行條件編譯
包含其它頭文件,或者進行遞歸包含
垃圾清理區(qū):主要用于清理頭文件所產(chǎn)生的宏垃圾,其中包括:
【可選】根據(jù)情況決定是否#undef作為輸入?yún)?shù)的宏
【可選】清除一些在功能區(qū)產(chǎn)生的、不希望暴露給用戶的宏
可重入頭文件的五個區(qū)域,拋開文檔區(qū),也就只剩下4個,看起來似乎并不復雜。下面我們就以mf_u8_fill_dec.h 為例,手把手帶大家建立一個麻雀雖小五臟俱全的可重入頭文件:
第一步:對輸入?yún)?shù)進行檢查(設計輸入?yún)?shù)檢查區(qū))
如前面例子中所介紹的那樣,mf_u8_fill_dec.h 包含了三個參數(shù):
MFUNC_IN_START——起始數(shù)字
MFUNC_IN_DELTA——間隔
MFUNC_IN_COUNT——填充的數(shù)量
由于并不復雜,我們可以簡單的構(gòu)建出如下的代碼:
#ifndef MFUNC_IN_START #defineMFUNC_IN_START0/*默認從0 開始 */ #endif #ifndef MFUNC_IN_DELTA #defineMFUNC_IN_DELTA 1 /* 默認以 1 為間隔 */ #endif #ifndef MFUNC_IN_COUNT /* 連數(shù)量都不提供,這就不能忍了!*/ # error "Please at least define MFUNC_COUNT!!!" #endif
這里,MFUNC是Macro Function(宏函數(shù))的縮寫,IN表示這是輸入?yún)?shù)。
第二步:編寫功能(實現(xiàn)功能區(qū))
由于無法事先知道功能區(qū)會定義哪些宏,因此無法在“#undef區(qū)”進行清理,索性直接跳過,進入功能的實現(xiàn)——完成以后,再回頭編寫“#undef區(qū)”就是水到渠成了。 對mf_u8_fill_dec.h來說,它是一個典型的循環(huán)體結(jié)構(gòu),由于C語言的預編譯器并沒有提供類似 FOR之類的循環(huán)支持,我們的可以通過“用遞歸來模擬迭代”的方式來實現(xiàn)一個循環(huán),基本思路如下:
通過mf_u8_dec2str.h來維護一個計數(shù)器
只要計數(shù)器值不為0,就遞歸調(diào)用頭文件
如果計數(shù)器為0,則退出頭文件
對應代碼如下:
/* 如果計數(shù)器為0就退出 */ #if MFUNC_IN_COUNT /* 實現(xiàn) MFUNC_IN_COUNT-- */ // MFUNC_IN_U8_DEC_VALUE = MFUNC_IN_COUNT - 1; 給腳本提供輸入 #define MFUNC_IN_U8_DEC_VALUE (MFUNC_IN_COUNT - 1) #include "mf_u8_dec2str.h" #undef MFUNC_IN_COUNT //! MFUNC_IN_COUNT = MFUNC_OUT_DEC_STR; 獲得腳本輸出 #define MFUNC_IN_COUNT MFUNC_OUT_DEC_STR #include"mf_u8_fill_dec.h" #endif對一個循環(huán)來說,我們一定有一個循環(huán)體。這里的技巧是,將循環(huán)體放置在遞歸調(diào)用的后面,換句話說:我們的做法是先一口氣積攢足夠的遞歸深度,然后在逐層返回的過程中執(zhí)行循環(huán)體。這樣做的好處是不用擔心循環(huán)的終止條件了——因此次數(shù)就是遞歸深度,這已經(jīng)固定了。 在這個例子中,循環(huán)體要做的事情就是以固定間隔填充數(shù)值,因此,當我們從遞歸的最深處逐層返回時,我們要做的就是維護填充數(shù)值,實現(xiàn)類似:
FUNC_IN_START += FUNC_IN_DELTA這樣的功能。具體代碼為:
/* 如果計數(shù)器為0就退出 */ #if MFUNC_IN_COUNT /* 實現(xiàn) MFUNC_IN_COUNT-- */ // MFUNC_IN_U8_DEC_VALUE = MFUNC_IN_COUNT - 1; 給腳本提供輸入 #define MFUNC_IN_U8_DEC_VALUE (MFUNC_IN_COUNT - 1) #include "mf_u8_dec2str.h" #undef MFUNC_IN_COUNT //! MFUNC_IN_COUNT = MFUNC_OUT_DEC_STR; 獲得腳本輸出 #define MFUNC_IN_COUNT MFUNC_OUT_DEC_STR #include "mf_u8_fill_dec.h" /*Loop body begin ------------------------------- */ MFUNC_IN_START, /* 實現(xiàn) FUNC_IN_START += FUNC_IN_DELTA */ #define MFUNC_IN_U8_DEC_VALUE (MFUNC_IN_START + MFUNC_IN_DELTA) #include "mf_u8_dec2str.h" #undef MFUNC_IN_START #define MFUNC_IN_START MFUNC_OUT_DEC_STR /* Loop Body End --------------------------------- */ #endif
第三步:更新 #undef區(qū)
通過觀察,發(fā)現(xiàn)功能區(qū)并沒有定義什么新的宏,因此略過此步驟。 第四步:清理垃圾(更新垃圾清理區(qū)) 在這個例子中,由于我們是通過遞歸返回的方法來實現(xiàn)功能,因此不能在尾部 #undef 關(guān)鍵的兩個參數(shù)MFUNC_IN_START和 MFUNC_IN_DELTA,但我們卻可以清理輸入?yún)?shù) MFUNC_IN_COUNT:
#undef MFUNC_IN_COUNT
第五步:添加使用說明(更新文檔區(qū))
注意到 三個輸入?yún)?shù)中的兩個需要用戶在使用前自行#undef,因此應該將這一條關(guān)鍵信息寫入文檔區(qū)——并最好提供一個范例代碼。
至此,我們就獲得了一個可以進行數(shù)據(jù)填充的可重入宏,其完整代碼如下:
/* How To Use 1. Please #undef macros MFUNC_IN_START and MFUNC_IN_DELTA before using 2. [optional]Define macro MFUNC_IN_START to specify the starting value 3. [optional]Define macro MFUNC_IN_DELTA to specify the increasing step 4. Define macro MFUNC_IN_COUNT to specify the number of items. NOTE: the MFUNC_IN_COUNT should not larger than 200 // 4位 Alpha 對應 8bit Alpha的備查表 const uint8_t c_chAlphaA4Table[16] = { #undef MFUNC_IN_START #undef MFUNC_IN_DELTA #define MFUNC_IN_START 0 #define MFUNC_IN_COUNT 16 #define MFUNC_IN_DELTA 17 #include "mf_u8_fill_dec.h" }; */ #ifndef MFUNC_IN_START # define MFUNC_IN_START 0 /* 默認從 0 開始 */ #endif #ifndef MFUNC_IN_DELTA # define MFUNC_IN_DELTA 1 /* 默認以 1 為間隔 */ #endif #ifndef MFUNC_IN_COUNT /* 連數(shù)量都不提供,這就不能忍了!*/ # error "Please at least define MFUNC_COUNT!!!" #endif /* 如果計數(shù)器為0就退出 */ #if MFUNC_IN_COUNT /* 實現(xiàn) MFUNC_IN_COUNT-- */ // MFUNC_IN_U8_DEC_VALUE = MFUNC_IN_COUNT - 1; 給腳本提供輸入 #define MFUNC_IN_U8_DEC_VALUE (MFUNC_IN_COUNT - 1) #include "mf_u8_dec2str.h" #undef MFUNC_IN_COUNT //! MFUNC_IN_COUNT = MFUNC_OUT_DEC_STR; 獲得腳本輸出 #define MFUNC_IN_COUNT MFUNC_OUT_DEC_STR #include "mf_u8_fill_dec.h" /* Loop body begin ------------------------------- */ MFUNC_IN_START, /* 實現(xiàn) FUNC_IN_START += FUNC_IN_DELTA */ #define MFUNC_IN_U8_DEC_VALUE (MFUNC_IN_START + MFUNC_IN_DELTA) #include "mf_u8_dec2str.h" #undef MFUNC_IN_START #define MFUNC_IN_START MFUNC_OUT_DEC_STR /* Loop body End --------------------------------- */ #endif #undef MFUNC_IN_COUNT
別忘記根據(jù)使用說明,對例子代碼進行適當?shù)男薷模?/p>
// 2位 Alpha 對應 8bit Alpha的備查表 const uint8_t c_chAlphaA4Table[4] = { #undefMFUNC_IN_START #undef MFUNC_IN_DELTA #define MFUNC_IN_START 0 #define MFUNC_IN_COUNT 4 #define MFUNC_IN_DELTA 85 #include "mf_u8_fill_dec.h" }; // 4位 Alpha 對應 8bit Alpha的備查表 const uint8_t c_chAlphaA4Table[16] = { #undefMFUNC_IN_START #undef MFUNC_IN_DELTA #define MFUNC_IN_START 0 #define MFUNC_IN_COUNT 16 #define MFUNC_IN_DELTA 17 #include "mf_u8_fill_dec.h" }; // 8位 Alpha 對應 8bit Alpha的備查表 const uint8_t c_chAlphaA8Table[256] = { #undef MFUNC_IN_START #undef MFUNC_IN_DELTA #define MFUNC_IN_START 0 #define MFUNC_IN_COUNT 128 #include "mf_u8_fill_dec.h" #undef MFUNC_IN_START #undef MFUNC_IN_DELTA #define MFUNC_IN_START 128 #define MFUNC_IN_COUNT 128 #include "mf_u8_fill_dec.h" };
大功告成!
【說在后面的話】
受到篇幅限制,本文只介紹了“可重入頭文件”的兩種常見形式,并著重介紹了以“遞歸”方式來批量進行代碼生成的例子。
雖然填充數(shù)組看起來用處并不很大,但它充分展示了通過可重入頭文件進行指定次數(shù)遞歸的方法。相信只要打開了思路,我對大家舉一反三的能力從不懷疑。
需要強調(diào)一下:可重入頭文件只是一類非常基本的方法,并不是所謂的旁門左道,其構(gòu)建方式有固定的方法,且有章可循,人人都能掌握。
-
C語言
+關(guān)注
關(guān)注
180文章
7575瀏覽量
134233 -
代碼
+關(guān)注
關(guān)注
30文章
4672瀏覽量
67781 -
頭文件
+關(guān)注
關(guān)注
0文章
24瀏覽量
9817
原文標題:【為宏正名】99%的人從第一天學習C語言就自廢的武功
文章出處:【微信號:TopSemic,微信公眾號:TopSemic嵌入式】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論