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