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