前言
在項目中,我們經常會需要針對不同的需求進行不同的配置。
在windows/Linux等大平臺下,可能會用到配置文件 ini、xml等。而在嵌入式平臺下,可能連文件系統都沒有。而且很多時候我們只需要硬編碼這些配置進代碼里就好,不需要在運行時更改。
比如每臺設備的設備信息等,在整個生命周期中是不會變的。所以并不需要用那么靈活的配置文件。
下面我就帶大家游覽一下C語言的宏配置相關技術,其可以實現靈活的代碼裁剪定制。基于自己目前的積累,可能有錯誤或者遺漏,敬請指出。
故事
假設我們在開發一個設備的項目,簡單起見,我們只寫出其中一小部分。主函數就長這樣就好了:
? ? main.c:
#include "device.h" int main() { Device_printfMsg(); return 0; }
?
????
設備的方法簡單起見就一個函數,打印自身信息:
????
device.h:
?
?
#ifndef _DEVICE_H #define _DEVICE_H void Device_printfMsg(void); #endif
?
????device.c:
?
#include "device.h" #include#include static const char *devType = "ABS"; static uint32_t devID = 34; void Device_printfMsg(void) { printf("Device: %s " , devType); printf("DevID: %u " , devID); printf("DomainName: %s_%u.local " , devType, devID); }
?
?
???這樣一個簡單的設備就完成了:
?
???
?但這樣實在偶合太嚴重了。要是現在我多了一臺設備,需要多維護一個設備,那最樸實的人肯定就屁顛屁顛的一個個去修改值了。要是偶爾修改一下,而且就幾個參數還好,但實際中經常會有多個參數,而且會經常要修改,那直接人工修改就很不靠譜了。
???
?而我第一反應可能會這么搞。
???
?device.c:
?
#include "device.h" #include#include #if 0 static const char *devType = "ABS"; static uint32_t devID = 34; #else static const char *devType = "CBA"; static uint32_t devID = 33435; #endif void Device_printfMsg(void) { printf("Device: %s " , devType); printf("DevID: %u " , devID); printf("DomainName: %s_%u.local " , devType, devID); }
?
????這是快速切換技術,這樣我只要修改#if后面為1或0就能快速切換不同配置:
???
?觀察代碼發現,冗余的代碼有點多,而且比如那個DomainName。很可能代碼其他地方還會經常用到,這樣把它的格式放在printf的格式字符串里就很不合適了,我們需要單獨為它分配個字符串。
????
于是整理之后就變成了這樣。
?
#include "device.h" #include#include #if 0 #define DEV_NAME ABS #define DEV_ID 34 #else #define DEV_NAME CBA #define DEV_ID 33435 #endif #define _STR(s) #s #define MollocDefineToStr(mal) _STR(mal) static const char devType[] = MollocDefineToStr(DEV_NAME); static uint32_t devID = DEV_ID; static const char devDName[] = MollocDefineToStr(DEV_NAME) "_" MollocDefineToStr(DEV_ID) ".local"; void Device_printfMsg(void) { printf("Device: %s " , devType); printf("DevID: %u " , devID); printf("DomainName: %s " , devDName); }
?
???
?不用看了,運行結果和上面那個一模一樣。
??
??#define 就是宏定義,都在看宏配置技巧了應該其實是不需要解釋宏在干什么了。
????
但是要強調的是,宏的作用是文本替換,注意是文本,預處理器并不認得變量不變量的,它只知道見到之前定義過的宏,就直接替換文本。
????
所以:
?
static uint32_t devID = DEV_ID;
?
?
???這句其實經過預處理后就是:
?
static uint32_t devID = 33435;
?
??
??我們看到其中MollocDefineToStr這個宏很有意思,這對宏是用于把宏展開后的值作為字符串的。
??
??預處理后,
?
static?const?char?devDName[]?=?MollocDefineToStr(DEV_NAME)?"_"?MollocDefineToStr(DEV_ID)?".local";
?
???
?這句就會變成:
?
static?const?char?devDName[]?=?"CBA"???"_"???"33435"???".local";
?
???
?然后由于C語言里連續的字符串不分割的話會自動合并,上面這就相當于
?
static const char devDName[] = "CBA_33435.local";
?
???
?接下來又來了一臺設備。我忍,擴充下快速切換,弄成多路分支的那種。
?
#include "device.h" #include#include #define DEV_ABS 1 #define DEV_CBA 2 #define DEV_LOL 3 // 選擇當前的設備 #define DEV_SELECT DEV_LOL #if (DEV_SELECT == DEV_ABS) #define DEV_NAME ABS #define DEV_ID 34 #elif(DEV_SELECT == DEV_CBA) #define DEV_NAME CBA #define DEV_ID 33435 #elif(DEV_SELECT == DEV_LOL) #define DEV_NAME LOL #define DEV_ID 1234 #else #error "please select current device by DEV_SELECT" #endif #define _STR(s) #s #define MollocDefineToStr(mal) _STR(mal) static const char devType[] = MollocDefineToStr(DEV_NAME); static uint32_t devID = DEV_ID; static const char devDName[] = MollocDefineToStr(DEV_NAME) "_" MollocDefineToStr(DEV_ID) ".local"; void Device_printfMsg(void) { printf("Device: %s " , devType); printf("DevID: %u " , devID); printf("DomainName: %s " , devDName); }
?
????這樣每次這樣在 #define DEV_SELECT 那修改一下對應的設備就好了,其實可讀性還不錯。
?
???那句#error確保了你不會遺忘去配置它,因為如果你配置了個錯誤的值,預處理器會直接報錯。
????
這時候,一般來說我會把配置相關的移到頭文件中,就變成了這樣:
????
device.h:
?
#ifndef _DEVICE_H #define _DEVICE_H #define DEV_ABS 1 #define DEV_CBA 2 #define DEV_LOL 3 #ifndef DEV_SELECT #define DEV_SELECT DEV_ABS #endif #if (DEV_SELECT == DEV_ABS) #define DEV_NAME ABS #define DEV_ID 34 #elif(DEV_SELECT == DEV_CBA) #define DEV_NAME CBA #define DEV_ID 33435 #elif(DEV_SELECT == DEV_LOL) #define DEV_NAME LOL #define DEV_ID 1234 #else #error "please select current device by DEV_SELECT" #endif void Device_printfMsg(void); #endif
?
????device.c:
?
#include "device.h" #include#include #define _STR(s) #s #define MollocDefineToStr(mal) _STR(mal) static const char devType[] = MollocDefineToStr(DEV_NAME); static uint32_t devID = DEV_ID; static const char devDName[] = MollocDefineToStr(DEV_NAME) "_" MollocDefineToStr(DEV_ID) ".local"; void Device_printfMsg(void) { printf("Device: %s " , devType); printf("DevID: %u " , devID); printf("DomainName: %s " , devDName); }
?
????這樣,這些配置參數就對其他 include 了這個頭文件的文件是可見的了。
?
????至于那句:
?
#ifndef DEV_SELECT #define DEV_SELECT DEV_ABS #endif
?
???
?這句可有個大好處,所有你想要擁有默認參數,且想要在不同工程中都可以定制的地方都可以這么寫。這樣,在編譯器選項中定義宏,就可以用同一套源碼為不同項目生成項目定制代碼。
??
??比如在VS中可以在解決方案資源管理器中的項目條目上右鍵->屬性,打開項目的屬性頁,在 C/C++ ->預處理器->預處理器定義 中定義宏。
????CodeWarrior中則是在Edit->Standard Settings里:
???
?當然,有一點點問題就是這樣搞沒法使用像前面類枚舉那種方法來給宏賦值宏,得直接賦值數字、字符串等。
???
?接下來。what!?還要加設備,這樣下去不行!一堆 #if #else 會搞死人的。要是我幾十W個設備,難道一個.h文件就幾十萬行么?我得把配置信息獨立出來!
???
?建立一個隨便什么名字,甚至隨便什么擴展名的文件,扔進工程文件夾,就隨便起個名字叫DEVINFO.txt得了。
???
?DEVINFO.txt:
?
// 設備配置信息模板,根據具體設備配置 // 設備名,字符串 #define DEV_NAME DEFAULT // 設備ID,U32 #define DEV_ID 0
?
????然后修改device模塊:
????device.h:
?
#ifndef _DEVICE_H #define _DEVICE_H #ifndef DEVINFO_FILENAME #define DEVINFO_FILENAME DEVINFO.txt #endif void Device_printfMsg(void); #endif
?
????device.c:
?
#include "device.h" #include#include #define _STR(s) #s #define MollocDefineToStr(mal) _STR(mal) #include MollocDefineToStr(DEVINFO_FILENAME) static const char devType[] = MollocDefineToStr(DEV_NAME); static uint32_t devID = DEV_ID; static const char devDName[] = MollocDefineToStr(DEV_NAME) "_" MollocDefineToStr(DEV_ID) ".local"; void Device_printfMsg(void) { printf("Device: %s " , devType); printf("DevID: %u " , devID); printf("DomainName: %s " , devDName); }
?
????完美,設備相關信息全部都從外面的txt文件中讀出來了,而且這個文件的文件名還是由剛剛才提到的可工程定制的宏配置的方式給出的。我們可以把其他幾個設備的配置信息文件都補上。
?
// 設備名,字符串 #define DEV_NAME ABS // 設備ID,U32 #define DEV_ID 34 // 設備名,字符串 #define DEV_NAME CBA // 設備ID,U32 #define DEV_ID 33435 // 設備名,字符串 #define DEV_NAME LOL // 設備ID,U32 #define DEV_ID 1234
?
????好了,這樣我們只要為所有設備各建立一個TXT的信息表,然后當需要切換不同的設備時就用前述方法改一下宏配置切換不同的文件名就好了。
???
?要明白這個方法為什么能起作用,關鍵是要理解這一句:
?
#include MollocDefineToStr(DEVINFO_FILENAME)
?
????我們知道,經過預處理器后,這一句就會變為:
?
#include "DEVINFO.txt"
?
??
??也許你會想:這是什么鬼,還可以include txt文件?我之前見得怎么都是include .h文件呀。這是一個大大的誤區。其實include從來沒規定說一定要.h文件,其實可以是任何名字的,這個預處理器指令干的事情就是把include的文件不斷遞歸的文本展開而已。
?
???所以其實上面這句在經過預處理器后會被直接文本替換為對應的文件的內容,一字不差的那種。可能前后會加點注釋信息。
??
??所以這種成組綁定、十分固定的配置信息就很適合用這種方式解耦到不同的配置文件中去,按需導入即可。更進一步的,應該要專門為這些配置文件建一個文件夾進行管理。
??
??而對于那種經常會獨立更改的配置呢?
???
?一兩個的話可以通過之前說的預處理器宏定義的方式來搞定,但是一個稍微有點規模的項目總會涉及到好多好多的配置參數,這個時候就不適合都寫在編譯器選項里了。
???
?這個時候我會專門建一個工程配置文件,比如就叫app_cfg.h,然后把整個工程中可能用到的宏配置都匯總在這里方便修改,這時之前那種可工程定制的宏寫法就特別管用了:
?
???app_cfg.h:
?
#define DEVINFO_FILENAME DEVINFO_CBA.txt // 其他宏配置選項 ...
?
????然后,就需要用到強制包含文件這個技巧了,相當于在所有的.c文件前面都直接加一行。
?
#include "app_cfg.h"
?
?
???這是VS2012中的:
????這是CodeWarrior中的:
????然后就可以很愉快的在一個文件中操控整個工程了!
???
?那我現在又來需求了,ID是有限制的,不能超過5000。那我就這么改。在
?
#include MollocDefineToStr(DEVINFO_FILENAME)
?
????下面加一句:
?
#if(DEV_ID > 5000) #error "device ID shouldn't bigger than 5000" #endif
?
????那這樣,當我們選取CBA時就沒法通過編譯了:
????還可以通過。
?
#ifndef DEV_ID #error "DEV_ID lost" #endif
?
????檢查DEV_ID是否正確進行了宏定義,或如果想要組合的條件:
?
#if !defined(DEV_NAME) || !defined(DEV_ID) #error "DEV_NAME or DEV_ID malloc define lost" #endif
?
????然后比如某個設備需要進行代碼定制處理,一種方法是在代碼中直接寫語句進行判斷當前設備的名字之類的然后執行對應特定語句。
???
?但為了節約編碼出來的代碼量,同時也是為了體現宏的威力,我們同樣可以用預處理指令,遺憾的是,我們沒法在預處理器指令中判斷字符串,但是可以判斷數字,正好我們有ID可以用,所以比如我們要讓設備ABS多輸出一行hahaha,那代碼就被改成了這樣:
?
void Device_printfMsg(void) { printf("Device: %s " , devType); printf("DevID: %u " , devID); printf("DomainName: %s " , devDName); #if(DEV_ID == 34) printf("hahaha "); #endif }
?
????
記住,這些預處理指令的本質都是在替換文本,所以,只有ABS設備時才有這一行代碼,對其他設備來說壓根沒有見到這行代碼。
???
?當然,你可以嘗試用之前那個include的方法以及其他宏方法來進一步組合定制代碼,這是一項創造性工作。
???
?最后突然又想起來一個妙招。也是我最近代碼里一直在用的。
???
?我專門搞了一個DebugMsg.h,大概長這樣:
?
#ifndef _DEBUG_MSG_H #define _DEBUG_MSG_H #include#ifdef _DEBUG #define _dbg_printf0(format) ((void)printf(format)) #define _dbg_printf1(format,p1) ((void)printf(format,p1)) …… #else #define _dbg_printf0(format) #define _dbg_printf1(format,p1) …… #endif #endif
?
????這樣,所有各個模塊中只要引用了這個文件就可以用統一的接口輸出調試信息,只要我在主配置文件中定義_DEBUG,所有調試printf就會變成真實的printf,否則就是空語句,無調試信息:
?
#include "DebugMsg.h" void Device_printfMsg(void) { _dbg_printf0("Device_printfMsg called. "); printf("Device: %s " , devType); printf("DevID: %u " , devID); printf("DomainName: %s " , devDName); }
?
???
?那我想要使用這個接口,卻又想要為我的device模塊單獨設一個開關怎么辦呢?
???
?整個邏輯簡單來說就是,_DEBUG是主開關,其關了所有模塊的調試信息都關了,然后各個模塊再有各自的開關,必須和_DEBUG一起都被定義才會使這個模塊有調試信息。
?
???那我這個模塊就改成了這樣。
?
???device.h:
?
#ifndef _DEVICE_H #define _DEVICE_H // malloc define _DEVICE_DEBUG to enable debug message // #define _DEVICE_DEBUG #ifndef DEVINFO_FILENAME #define DEVINFO_FILENAME DEVINFO.txt #endif void Device_printfMsg(void); #endif
?
????device.c:
?
…… #ifndef _DEVICE_DEBUG #undef _DEBUG #endif #include "DebugMsg.h" void Device_printfMsg(void) { _dbg_printf0("Device_printfMsg called. "); printf("Device: %s " , devType); printf("DevID: %u " , devID); printf("DomainName: %s " , devDName); }
?
????
這樣,我只有同時宏定義_ DEVICE_DEBUG和_ DEBUG時_dbg_printf0才會被宏定義為printf,否則會被宏定義為空語句,也就沒有調試信息了。
???
?這是怎么回事呢?當預處理器讀到#ifndef _ DEVICE_DEBUG這句發現未宏定義_ DEVICE_DEBUG時,它會在下一句取消_ DEBUG的宏定義,這樣不管我實際有沒宏定義_ DEBUG,當到了#include "DebugMsg.h"并展開后,預處理器都會認為未定義_ DEBUG,所以就會把_dbg_printf0宏定義為空語句,然后就實現了這個串聯的邏輯。
后記
???
?好啦,已經講夠多的了,相信你看得也很過癮。想要再深一步,可以專門看看C語言宏的一些高階用法。
????
下次看見哪個庫里頭到處亂飛的宏配置,不會那么一臉懵逼了吧(# ^ . ^ #)
審核編輯:湯梓紅
評論
查看更多