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