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

C語言頭文件是做什么的
C語言頭文件組織作用與包含原則詳解
C語言的頭文件組織與包含原則
單片機-頭文件

MCU_頭文件編寫

評論