對于斷言,相信大家都不陌生,大多數(shù)編程語言也都有斷言這一特性。簡單地講,斷言就是對某種假設(shè)條件進(jìn)行檢查。
在 C 語言中,斷言被定義為宏的形式(assert(expression)
),而不是函數(shù),其原型定義在
文件中。
其中,assert 將通過檢查表達(dá)式 expression 的值來決定是否需要終止執(zhí)行程序。也就是說,如果表達(dá)式 expression 的值為假(即為 0),那么它將首先向標(biāo)準(zhǔn)錯(cuò)誤流 stderr 打印一條出錯(cuò)信息,然后再通過調(diào)用 abort 函數(shù)終止程序運(yùn)行;否則,assert 無任何作用。
原型定義:
#include
voidassert(intexpression);
默認(rèn)情況下,assert宏只有在 Debug 版本(內(nèi)部調(diào)試版本)中才能夠起作用,而在 Release 版本(發(fā)行版本)中將被忽略。當(dāng)然,也可以通過定義宏或設(shè)置編譯器參數(shù)等形式來在任何時(shí)候啟用或者禁用斷言檢查(不建議這么做)。同樣,在程序投入運(yùn)行后,最終用戶在遇到問題時(shí)也可以重新起用斷言。
這樣可以快速發(fā)現(xiàn)并定位軟件問題,同時(shí)對系統(tǒng)錯(cuò)誤進(jìn)行自動(dòng)報(bào)警。對于在系統(tǒng)中隱藏很深,用其他手段極難發(fā)現(xiàn)的問題也可以通過斷言進(jìn)行定位,從而縮短軟件問題定位時(shí)間,提高系統(tǒng)的可測性。
盡量利用斷言來提高代碼的可測試性
在討論如何使用斷言之前,先來看下面一段示例代碼:
void*Memcpy(void*dest,constvoid*src,size_tlen)
{
char*tmp_dest=(char*)dest;
char*tmp_src=(char*)src;
while(len--)
*tmp_dest++=*tmp_src++;
returndest;
}
對于上面的 Memcpy 函數(shù),毋庸置疑,它能夠通過編譯程序的檢查成功編譯。從表面上看,該函數(shù)并不存在其他任何問題,并且代碼也非常干凈。
但遺憾的是,在調(diào)用該函數(shù)時(shí),如果不小心為 dest 與 src 參數(shù)錯(cuò)誤地傳入了 NULL 指針,那么問題就嚴(yán)重了。輕者在交付之前這個(gè)潛在的錯(cuò)誤導(dǎo)致程序癱瘓,從而暴露出來。否則,如果將該程序打包發(fā)布出去,那么所造成的后果是無法估計(jì)的。
由此可見,不能夠簡單地認(rèn)為“只要通過編譯程序成功編譯的就都是安全的程序”。當(dāng)然,編譯程序也很難檢查出類似的潛在錯(cuò)誤(如所傳遞的參數(shù)是否有效、潛在的算法錯(cuò)誤等)。
面對這類問題,一般首先想到的應(yīng)該是使用最簡單的if語句進(jìn)行判斷檢查,如下面的示例代碼所示:
void*Memcpy(void*dest,constvoid*src,size_tlen)
{
if(dest==NULL)
{
fprintf(stderr,"destisNULL
");
abort();
}
if(src==NULL)
{
fprintf(stderr,"srcisNULL
");
abort();
}
char*tmp_dest=(char*)dest;
char*tmp_src=(char*)src;
while(len--)
*tmp_dest++=*tmp_src++;
returndest;
}
現(xiàn)在,通過“if(dest == NULL)與if(src == NULL)
”判斷語句,只要在調(diào)用該函數(shù)的時(shí)候?yàn)?dest 與 src 參數(shù)錯(cuò)誤地傳入了NULL指針,這個(gè)函數(shù)就會(huì)檢查出來并做出相應(yīng)的處理,即先向標(biāo)準(zhǔn)錯(cuò)誤流 stderr 打印一條出錯(cuò)信息,然后再調(diào)用 abort 函數(shù)終止程序運(yùn)行。
從表面看來,上面的解決方案應(yīng)該堪稱完美。但是,隨著函數(shù)參數(shù)或需要檢查的表達(dá)式不斷增多,這種檢查測試代碼將占據(jù)整個(gè)函數(shù)的大部分(這一點(diǎn)從上面的 Memcpy 函數(shù)中就不難看出)。這樣代碼看起來非常不簡潔,甚至可以說很“糟糕”,而且也降低了函數(shù)的執(zhí)行效率。
面對上面的問題,或許可以利用 C 的預(yù)處理程序有條件地包含或不包含相應(yīng)的檢查部分進(jìn)行解決,如下面的代碼所示:
void*MemCopy(void*dest,constvoid*src,size_tlen)
{
#ifdefDEBUG
if(dest==NULL)
{
fprintf(stderr,"destisNULL
");
abort();
}
if(src==NULL)
{
fprintf(stderr,"srcisNULL
");
abort();
}
#endif
char*tmp_dest=(char*)dest;
char*tmp_src=(char*)src;
while(len--)
*tmp_dest++=*tmp_src++;
returndest;
}
這樣,通過條件編譯“#ifdef DEBUG
”來同時(shí)維護(hù)同一程序的兩個(gè)版本(內(nèi)部調(diào)試版本與發(fā)行版本),即在程序編寫過程中,編譯其內(nèi)部調(diào)試版本,利用其提供的測試檢查代碼為程序自動(dòng)查錯(cuò)。而在程序編完之后,再編譯成發(fā)行版本。
上面的解決方案盡管通過條件編譯“#ifdef DEBUG”能產(chǎn)生很好的結(jié)果,也完全符合我們的程序設(shè)計(jì)要求,但是仔細(xì)觀察會(huì)發(fā)現(xiàn),這樣的測試檢查代碼顯得并不那么友好,當(dāng)一個(gè)函數(shù)里這種條件編譯語句很多時(shí),代碼會(huì)顯得有些浮腫,甚至有些糟糕。
因此,對于上面的這種情況,多數(shù)程序員都會(huì)選擇將所有的調(diào)試代碼隱藏在斷言 assert 宏中。其實(shí),assert 宏也只不過是使用條件編譯“#ifdef”對部分代碼進(jìn)行替換,利用 assert 宏,將會(huì)使代碼變得更加簡潔,如下面的示例代碼所示:
void*MemCopy(void*dest,constvoid*src,size_tlen)
{
assert(dest!=NULL&&src!=NULL);
char*tmp_dest=(char*)dest;
char*tmp_src=(char*)src;
while(len--)
*tmp_dest++=*tmp_src++;
returndest;
}
現(xiàn)在,通過“assert(dest !=NULL && src !=NULL)
”語句既完成程序的測試檢查功能(即只要在調(diào)用該函數(shù)的時(shí)候?yàn)?dest 與 src 參數(shù)錯(cuò)誤傳入 NULL 指針時(shí)都會(huì)引發(fā) assert),與此同時(shí),對 MemCopy 函數(shù)的代碼量也進(jìn)行了大幅度瘦身,不得不說這是一個(gè)兩全其美的好辦法。
實(shí)際上,在編程中我們經(jīng)常會(huì)出于某種目的(如把 assert 宏定義成當(dāng)發(fā)生錯(cuò)誤時(shí)不是中止調(diào)用程序的執(zhí)行,而是在發(fā)生錯(cuò)誤的位置轉(zhuǎn)入調(diào)試程序,又或者是允許用戶選擇讓程序繼續(xù)運(yùn)行等)需要對 assert 宏進(jìn)行重新定義
。
但值得注意的是,不管斷言宏最終是用什么樣的方式進(jìn)行定義,其所定義宏的主要目的都是要使用它來對傳遞給相應(yīng)函數(shù)的參數(shù)進(jìn)行確認(rèn)檢查。
如果違背了這條宏定義原則,那么所定義的宏將會(huì)偏離方向,失去宏定義本身的意義。與此同時(shí),為不影響標(biāo)準(zhǔn) assert 宏的使用,最好使用其他的名字。例如,下面的示例代碼就展示了用戶如何重定義自己的宏 ASSERT:
/*使用斷言測試*/
#ifdefDEBUG
/*處理函數(shù)原型*/
voidAssert(char*filename,unsignedintlineno);
#defineASSERT(condition)
if(condition)
NULL;
else
Assert(__FILE__,__LINE__)
/*不使用斷言測試*/
#else
#defineASSERT(condition)NULL
#endif
voidAssert(char*filename,unsignedintlineno)
{
fflush(stdout);
fprintf(stderr,"
Assertfailed:%s,line%u
",filename,lineno);
fflush(stderr);
abort();
}
如果定義了 DEBUG,ASSERT 將被擴(kuò)展為一個(gè)if語句,否則執(zhí)行“#define ASSERT(condition) NULL
”替換成 NULL。
這里需要注意的是,因?yàn)樵诰帉?C 語言代碼時(shí),在每個(gè)語句后面加一個(gè)分號(hào)“;
”已經(jīng)成為一種約定俗成的習(xí)慣,因此很有可能會(huì)在“Assert(__FILE__,__LINE__)
”調(diào)用語句之后習(xí)慣性地加上一個(gè)分號(hào)。
實(shí)際上并不需要這個(gè)分號(hào),因?yàn)橛脩粼谡{(diào)用 ASSERT 宏時(shí),已經(jīng)給出了一個(gè)分號(hào)。面對這種問題,我們可以使用“do{}while(0)”結(jié)構(gòu)進(jìn)行處理,如下面的代碼所示:
#defineASSERT(condition)
do{
if(condition)
NULL;
else
Assert(__FILE__,__LINE__);
}while(0)
現(xiàn)在,將不再為分號(hào)“;”而擔(dān)心了,調(diào)用示例如下:
voidTest(unsignedchar*str)
{
ASSERT(str!=NULL);
/*函數(shù)處理代碼*/
}
intmain(void)
{
Test(NULL);
return0;
}
很顯然,因?yàn)檎{(diào)用語句“Test(NULL)
”為參數(shù) str 錯(cuò)誤傳入一個(gè) NULL 指針的原因,所以ASSERT
宏會(huì)自動(dòng)檢測到這個(gè)錯(cuò)誤,同時(shí)根據(jù)宏__FILE__
和__LINE__
所提供的文件名和行號(hào)參數(shù)在標(biāo)準(zhǔn)錯(cuò)誤輸出設(shè)備 stderr 上打印一條錯(cuò)誤消息,然后調(diào)用 abort 函數(shù)中止程序的執(zhí)行。運(yùn)行結(jié)果如圖 1 所示。
圖 1 調(diào)用自定義 ASSERT 宏的運(yùn)行結(jié)果
如果這時(shí)候?qū)⒆远x ASSERT 宏替換成標(biāo)準(zhǔn) assert 宏結(jié)果會(huì)是怎樣的呢?如下面的示例代碼所示:
voidTest(unsignedchar*str)
{
assert(str!=NULL);
/*函數(shù)處理代碼*/
}
毋庸置疑,標(biāo)準(zhǔn) assert 宏同樣會(huì)自動(dòng)檢測到這個(gè) NULL 指針錯(cuò)誤。與此同時(shí),標(biāo)準(zhǔn) assert 宏除給出以上信息之外,還能夠顯示出已經(jīng)失敗的測試條件。運(yùn)行結(jié)果如圖 2 所示。
在這里插入圖片描述圖 2 調(diào)用標(biāo)準(zhǔn) assert 宏的運(yùn)行結(jié)果
從上面的示例中不難發(fā)現(xiàn),對標(biāo)準(zhǔn)的 assert 宏來說,自定義的 ASSERT 宏將具有更大的靈活性,可以根據(jù)自己的需要打印輸出不同的信息,同時(shí)也可以對不同類型的錯(cuò)誤或者警告信息使用不同的斷言,這也是在工程代碼中經(jīng)常使用的做法。當(dāng)然,如果沒有什么特殊需求,還是建議使用標(biāo)準(zhǔn) assert 宏。
盡量在函數(shù)中使用斷言來檢查參數(shù)的合法性
在函數(shù)中使用斷言來檢查參數(shù)的合法性是斷言最主要的應(yīng)用場景之一,它主要體現(xiàn)在如下 3 個(gè)方面:
-
1.在代碼執(zhí)行之前或者在函數(shù)的入口處,使用斷言來檢查參數(shù)的合法性,這稱為前置條件斷言。
-
2.在代碼執(zhí)行之后或者在函數(shù)的出口處,使用斷言來檢查參數(shù)是否被正確地執(zhí)行,這稱為后置條件斷言。
-
3.在代碼執(zhí)行前后或者在函數(shù)的入出口處,使用斷言來檢查參數(shù)是否發(fā)生了變化,這稱為前后不變斷言。
例如,在上面的 Memcpy 函數(shù)中,除了可以通過“assert(dest !=NULL && src!=NULL);
”語句在函數(shù)的入口處檢查 dest 與 src 參數(shù)是否傳入 NULL 指針之外,還可以通過“assert(tmp_dest>=tmp_src+len||tmp_src>=tmp_dest+len);
”語句檢查兩個(gè)內(nèi)存塊是否發(fā)生重疊。如下面的示例代碼所示:
void*Memcpy(void*dest,constvoid*src,size_tlen)
{
assert(dest!=NULL&&src!=NULL);
char*tmp_dest=(char*)dest;
char*tmp_src=(char*)src;
/*檢查內(nèi)存塊是否重疊*/
assert(tmp_dest>=tmp_src+len||tmp_src>=tmp_dest+len);
while(len--)
*tmp_dest++=*tmp_src++;
returndest;
}
除此之外,建議每一個(gè) assert 宏只檢驗(yàn)一個(gè)條件,這樣做的好處就是當(dāng)斷言失敗時(shí),便于程序排錯(cuò)。
試想一下,如果在一個(gè)斷言中同時(shí)檢驗(yàn)多個(gè)條件,當(dāng)斷言失敗時(shí),我們將很難直觀地判斷哪個(gè)條件失敗。因此,下面的斷言代碼應(yīng)該更好一些,盡管這樣顯得有些多此一舉:
assert(dest!=NULL);
assert(src!=NULL);
最后,建議 assert 宏后面的語句應(yīng)該空一行,以形成邏輯和視覺上的一致感,讓代碼有一種視覺上的美感。同時(shí)為復(fù)雜的斷言添加必要的注釋,可澄清斷言含義并減少不必要的誤用。
避免在斷言表達(dá)式中使用改變環(huán)境的語句
默認(rèn)情況下,因?yàn)?assert 宏只有在 Debug 版本中才能起作用,而在 Release 版本中將被忽略。因此,在程序設(shè)計(jì)中應(yīng)該避免在斷言表達(dá)式中使用改變環(huán)境的語句。如下面的示例代碼所示:
intTest(inti)
{
assert(i++);
returni;
}
intmain(void)
{
inti=1;
printf("%d
",Test(i));
return0;
}
對于上面的示例代碼,由于“assert(i++)
”語句的原因,將導(dǎo)致不同的編譯版本產(chǎn)生不同的結(jié)果。如果是在 Debug 版本中,因?yàn)檫@里向變量 i 所賦的初始值為 1,所以在執(zhí)行“assert(i++)”語句的時(shí)候?qū)⑼ㄟ^條件檢查,進(jìn)而繼續(xù)執(zhí)行“i++”,最后輸出的結(jié)果值為 2;如果是在 Release 版本中,函數(shù)中的斷言語句“assert(i++)”將被忽略掉,這樣表達(dá)式“i++”將得不到執(zhí)行,從而導(dǎo)致輸出的結(jié)果值還是 1。
因此,應(yīng)該避免在斷言表達(dá)式中使用類似“i++
”這樣改變環(huán)境的語句,使用如下代碼進(jìn)行替換:
intTest(inti)
{
assert(i);
i++;
returni;
}
現(xiàn)在,無論是 Debug 版本,還是 Release 版本的輸出結(jié)果都將為 2。
避免使用斷言去檢查程序錯(cuò)誤
在對斷言的使用中,一定要遵循這樣一條規(guī)定:對來自系統(tǒng)內(nèi)部的可靠的數(shù)據(jù)使用斷言,對于外部不可靠數(shù)據(jù)不能夠使用斷言,而應(yīng)該使用錯(cuò)誤處理代碼。換句話說,斷言是用來處理不應(yīng)該發(fā)生的非法情況,而對于可能會(huì)發(fā)生且必須處理的情況應(yīng)該使用錯(cuò)誤處理代碼,而不是斷言。
在通常情況下,系統(tǒng)外部的數(shù)據(jù)(如不合法的用戶輸入)都是不可靠的,需要做嚴(yán)格的檢查(如某模塊在收到其他模塊或鏈路上的消息后,要對消息的合理性進(jìn)行檢查,此過程為正常的錯(cuò)誤檢查,不能用斷言來實(shí)現(xiàn))才能放行到系統(tǒng)內(nèi)部,這相當(dāng)于一個(gè)守衛(wèi)。
而對于系統(tǒng)內(nèi)部的交互(如子程序調(diào)用),如果每次都去處理輸入的數(shù)據(jù),也就相當(dāng)于系統(tǒng)沒有可信的邊界,這樣會(huì)讓代碼變得臃腫復(fù)雜。
事實(shí)上,在系統(tǒng)內(nèi)部,傳遞給子程序預(yù)期的恰當(dāng)數(shù)據(jù)應(yīng)該是調(diào)用者的責(zé)任,系統(tǒng)內(nèi)的調(diào)用者應(yīng)該確保傳遞給子程序的數(shù)據(jù)是恰當(dāng)且可以正常工作的。這樣一來,就隔離了不可靠的外部環(huán)境和可靠的系統(tǒng)內(nèi)部環(huán)境,降低復(fù)雜度。
但是在代碼編寫與測試階段,代碼很可能包含一些意想不到的缺陷,也許是處理外部數(shù)據(jù)的程序考慮得不夠周全,也許是調(diào)用系統(tǒng)內(nèi)部子程序的代碼存在錯(cuò)誤,造成子程序調(diào)用失敗。
這個(gè)時(shí)候,斷言就可以發(fā)揮作用,用來確診到底是哪部分出現(xiàn)了問題而導(dǎo)致子程序調(diào)用失敗。在清理所有缺陷之后,就建立了內(nèi)外有別的信用體系。等到發(fā)行版的時(shí)候,這些斷言就沒有存在的必要了。因此,不能用斷言來檢查最終產(chǎn)品肯定會(huì)出現(xiàn)且必須處理的錯(cuò)誤情況。
看下面一段示例代碼:
char*Strdup(constchar*source)
{
assert(source!=NULL);
char*result=NULL;
size_tlen=strlen(source)+1;
result=(char*)malloc(len);
assert(result!=NULL);
strcpy(result,source);
returnresult;
}
對于上面的 Strdup 函數(shù),相信大家都不陌生。其中,第一個(gè)斷言語句“assert(source!=NULL)”用來檢查該程序正常工作時(shí)絕對不應(yīng)該發(fā)生的非法情況。
換句話說,在調(diào)用代碼正確的情況下傳遞給 source 參數(shù)的值必然不為 NULL,如果斷言失敗,說明調(diào)用代碼中有錯(cuò)誤,必須修改。因此,它屬于斷言的正常使用情況。
而第二個(gè)斷言語句“assert(result!=NULL)”的用法則不同,它測試的是錯(cuò)誤情況,是在其最終產(chǎn)品中肯定會(huì)出現(xiàn)且必須對其進(jìn)行處理的錯(cuò)誤情況。
即對 malloc 函數(shù)而言,當(dāng)內(nèi)存不足導(dǎo)致內(nèi)存分配失敗時(shí)就會(huì)返回 NULL,因此這里不應(yīng)該使用 assert 宏進(jìn)行處理,而應(yīng)該使用錯(cuò)誤處理代碼。如下面問題將使用 if 判斷語句進(jìn)行處理:
char*Strdup(constchar*source)
{
assert(source!=NULL);
char*result=NULL;
size_tlen=strlen(source)+1;
result=(char*)malloc(len);
if(result!=NULL)
{
strcpy(result,source);
}
returnresult;
}
總之記住一句話:斷言是用來檢查非法情況的,而不是測試和處理錯(cuò)誤的。因此,不要混淆非法情況與錯(cuò)誤情況之間的區(qū)別,后者是必然存在且一定要處理的。
盡量在防錯(cuò)性程序設(shè)計(jì)中使用斷言來進(jìn)行錯(cuò)誤報(bào)警
對于防錯(cuò)性程序設(shè)計(jì),相信有經(jīng)驗(yàn)的程序員并不陌生,大多數(shù)教科書也都鼓勵(lì)程序員進(jìn)行防錯(cuò)性程序設(shè)計(jì)。在程序設(shè)計(jì)過程中,總會(huì)或多或少產(chǎn)生一些錯(cuò)誤,這些錯(cuò)誤有些屬于設(shè)計(jì)階段隱藏下來的,有些則是在編碼中產(chǎn)生的。
為了避免和糾正這些錯(cuò)誤,可在編碼過程中有意識(shí)地在程序中加進(jìn)一些錯(cuò)誤檢查的措施,這就是防錯(cuò)性程序設(shè)計(jì)的基本思想。其中,它又可以分為主動(dòng)式防錯(cuò)程序設(shè)計(jì)和被動(dòng)式防錯(cuò)程序設(shè)計(jì)兩種。
主動(dòng)式防錯(cuò)程序設(shè)計(jì)是指周期性地對整個(gè)程序或數(shù)據(jù)庫進(jìn)行搜查或在空閑時(shí)搜查異常情況。它既可以在處理輸入信息期間使用,也可以在系統(tǒng)空閑時(shí)間或等待下一個(gè)輸入時(shí)使用。如下面所列出的檢查均適合主動(dòng)式防錯(cuò)程序設(shè)計(jì)。
-
?內(nèi)存檢查:如果在內(nèi)存的某些塊中存放了一些具有某種類型和范圍的數(shù)據(jù),則可對它們做經(jīng)常性檢查。
-
?標(biāo)志檢查:如果系統(tǒng)的狀態(tài)是由某些標(biāo)志指示的,可對這些標(biāo)志做單獨(dú)檢查。
-
?反向檢查:對于一些從一種代碼翻譯成另一種代碼或從一種系統(tǒng)翻譯成另一種系統(tǒng)的數(shù)據(jù)或變量值,可以采用反向檢查,即利用反向翻譯來檢查原始值的翻譯是否正確。
-
?狀態(tài)檢查:對于某些具有多個(gè)操作狀態(tài)的復(fù)雜系統(tǒng),若用某些特定的存儲(chǔ)值來表示這些狀態(tài),則可通過單獨(dú)檢查存儲(chǔ)值來驗(yàn)證系統(tǒng)的操作狀態(tài)。
-
?連接檢查:當(dāng)使用鏈表結(jié)構(gòu)時(shí),可檢查鏈表的連接情況。
-
?時(shí)間檢查:如果已知道完成某項(xiàng)計(jì)算所需的最長時(shí)間,則可用定時(shí)器來監(jiān)視這個(gè)時(shí)間。
-
?其他檢查:程序設(shè)計(jì)人員可經(jīng)常仔細(xì)地對所使用的數(shù)據(jù)結(jié)構(gòu)、操作序列和定時(shí)以及程序的功能加以考慮,從中得到要進(jìn)行哪些檢查的啟發(fā)。
被動(dòng)式防錯(cuò)程序設(shè)計(jì)則是指必須等到某個(gè)輸入之后才能進(jìn)行檢查,也就是達(dá)到檢查點(diǎn)時(shí)才能對程序的某些部分進(jìn)行檢查。一般所要進(jìn)行的檢查項(xiàng)目如下:
-
?來自外部設(shè)備的輸入數(shù)據(jù),包括范圍、屬性是否正確。
-
?由其他程序所提供的數(shù)據(jù)是否正確。
-
?數(shù)據(jù)庫中的數(shù)據(jù),包括數(shù)組、文件、結(jié)構(gòu)、記錄是否正確。
-
?操作員的輸入,包括輸入的性質(zhì)、順序是否正確。
-
?棧的深度是否正確。
-
?數(shù)組界限是否正確。
-
?表達(dá)式中是否出現(xiàn)零分母情況。
-
?正在運(yùn)行的程序版本是否是所期望的(包括最后系統(tǒng)重新組合的日期)。
-
?通過其他程序或外部設(shè)備的輸出數(shù)據(jù)是否正確。
雖然防錯(cuò)性程序設(shè)計(jì)被譽(yù)為有較好的編碼風(fēng)格,一直被業(yè)界強(qiáng)烈推薦。但防錯(cuò)性程序設(shè)計(jì)也是一把雙刃劍,從調(diào)試錯(cuò)誤的角度來看,它把原來簡單的、顯而易見的缺陷轉(zhuǎn)變成晦澀的、難以檢測的缺陷,而且診斷起來非常困難。從某種意義上講,防錯(cuò)性程序設(shè)計(jì)隱瞞了程序的潛在錯(cuò)誤。
當(dāng)然,對于軟件產(chǎn)品,希望它越健壯越好。但是調(diào)試脆弱的程序更容易幫助我們發(fā)現(xiàn)其問題,因?yàn)楫?dāng)缺陷出現(xiàn)的時(shí)候它就會(huì)立即表現(xiàn)出來。
因此,在進(jìn)行防錯(cuò)性程序設(shè)計(jì)時(shí),如果“不可能發(fā)生”的事情的確發(fā)生了,則需要使用斷言進(jìn)行報(bào)警,這樣,才便于程序員在內(nèi)部調(diào)試階段及時(shí)對程序問題進(jìn)行處理,從而保證發(fā)布的軟件產(chǎn)品具有良好的健壯性。
一個(gè)很常見的例子就是無處不在的 for 循環(huán),如下面的示例代碼所示:
for(i=0;i/*處理代碼*/
}
在幾乎所有的 for 循環(huán)示例中,其行為都是迭代從 0 開始到“count-1”,因此,大家也都自然而然地編寫成了上面這種防錯(cuò)性版本。但存在的問題是:如果 for 循環(huán)中的索引 i 值確實(shí)大于 count,那么極有可能意味著代碼中存在著潛在的缺陷問題。
由于上面的 for 循環(huán)示例采用了防錯(cuò)性程序設(shè)計(jì)方式,因此,就算是在內(nèi)部測試階段中出現(xiàn)了這種缺陷也很難發(fā)現(xiàn)其問題的所在,更加不可能出現(xiàn)系統(tǒng)報(bào)警提示。同時(shí),因?yàn)檫@個(gè)潛在的程序缺陷,極有可能會(huì)在以后讓我們吃盡苦頭,而且非常難以診斷。
那么,不采用防錯(cuò)性程序設(shè)計(jì)會(huì)是什么樣子呢?如下面的示例代碼所示:
for(i=0;i!=count;i++)
{
/*處理代碼*/
}
很顯然,這種寫法肯定是不行的,當(dāng) for 循環(huán)中的索引 i 值確實(shí)大于 count 時(shí),它還是不會(huì)停止循環(huán)。
對于上面的問題,斷言為我們提供了一個(gè)非常簡單的解決方法,如下面的示例代碼所示:
for(i=0;i/*處理代碼*/
}
assert(i==count);
不難發(fā)現(xiàn),通過斷言真正實(shí)現(xiàn)了一舉兩得的目的:健壯的產(chǎn)品軟件和脆弱的開發(fā)調(diào)試程序,即在該程序的交付版本中,相應(yīng)的程序防錯(cuò)代碼可以保證當(dāng)程序的缺陷問題出現(xiàn)的時(shí)候,用戶可以不受損失;而在該程序的內(nèi)部調(diào)試版本中,潛在的錯(cuò)誤仍然可以通過斷言預(yù)警報(bào)告。
因此,“無論你在哪里編寫防錯(cuò)性代碼,都應(yīng)該盡量確保使用斷言來保護(hù)這段代碼”。當(dāng)然,也不必過分拘泥于此。例如,如果每次執(zhí)行 for 循環(huán)時(shí)索引 i 的值只是簡單地增 1,那么要使索引i的值超過 count 從而引起問題幾乎是不可能的。在這種情況下,相應(yīng)的斷言也就沒有任何存在的意義,應(yīng)該從程序中刪除。
但是,如果索引 i 的值有其他處理情況,則必須使用斷言進(jìn)行預(yù)警。由此可見,在防錯(cuò)性程序設(shè)計(jì)中是否需要使用斷言進(jìn)行錯(cuò)誤報(bào)警要視具體情況而定,在編碼之前都要問自己:“在進(jìn)行防錯(cuò)性程序設(shè)計(jì)時(shí),程序中隱瞞錯(cuò)誤了嗎?”如果答案是肯定的,就必須在程序中加上相應(yīng)的斷言,以此來對這些錯(cuò)誤進(jìn)行報(bào)警。否則,就不要多此一舉了。
用斷言保證沒有定義的特性或功能不被使用
在日常軟件設(shè)計(jì)中,如果原先規(guī)定的一部分功能尚未實(shí)現(xiàn),則應(yīng)該使用斷言來保證這些沒有被定義的特性或功能不被使用。例如,某通信模塊在設(shè)計(jì)時(shí),準(zhǔn)備提供“無連接”和“連接”這兩種業(yè)務(wù)。但當(dāng)前的版本中僅實(shí)現(xiàn)了“無連接”業(yè)務(wù),且在此版本的正式發(fā)行版中,用戶(上層模塊)不應(yīng)產(chǎn)生“連接”業(yè)務(wù)的請求,那么在測試時(shí)可用斷言來檢查用戶是否使用了“連接”業(yè)務(wù)。如下面的示例代碼所示:
/*無連接業(yè)務(wù)*/
#defineCONNECTIONLESS0
/*連接業(yè)務(wù)*/
#defineCONNECTION1
intMessageProcess(MESSAGE*msg)
{
assert(msg!=NULL);
unsignedcharservice;
service=GetMessageService(msg);
/*使用斷言來檢查用戶是否使用了“連接”業(yè)務(wù)*/
assert(service!=CONNECTION);
/*處理代碼*/
}
謹(jǐn)慎使用斷言對程序開發(fā)環(huán)境中的假設(shè)進(jìn)行檢查
在程序設(shè)計(jì)中,不能夠使用斷言來檢查程序運(yùn)行時(shí)所需的軟硬件環(huán)境及配置要求
,它們需要由專門的處理代碼進(jìn)行檢查處理。而斷言僅可對程序開發(fā)環(huán)境(OS/Compiler/Hardware)中的假設(shè)及所配置的某版本軟硬件是否具有某種功能的假設(shè)進(jìn)行檢查
。例如,某網(wǎng)卡是否在系統(tǒng)運(yùn)行環(huán)境中配置了,應(yīng)由程序中正式代碼來檢查;而此網(wǎng)卡是否具有某設(shè)想的功能,則可以由斷言來檢查。
除此之外,對編譯器提供的功能及特性的假設(shè)也可以使用斷言進(jìn)行檢查,如下面的示例代碼所示:
/*int類型占用的內(nèi)存空間是否為2*/
assert(sizeof(int)==2);
/*long類型占用的內(nèi)存空間是否為4*/
assert(sizeof(long)==4);
/*byte的寬度是否為8*/
assert(CHAR_BIT==8);
之所以可以這樣使用斷言,那是因?yàn)檐浖罱K發(fā)行的 Release 版本與編譯器已沒有任何直接關(guān)系。
最后,必須保證軟件的 Debug 與 Release 兩個(gè)版本在實(shí)現(xiàn)功能上的一致性,同時(shí)可以使用調(diào)測開關(guān)來切換這兩個(gè)不同的版本,以便統(tǒng)一維護(hù),切記不要同時(shí)存在 Debug 版本與 Release 版本兩個(gè)不同的源文件。
當(dāng)然,因?yàn)轭l繁調(diào)用 assert 會(huì)極大影響程序的性能,增加額外的開銷。因此,應(yīng)該在正式軟件產(chǎn)品(即 Release 版本)中將斷言及其他調(diào)測代碼關(guān)掉(尤其是針對自定義的斷言宏)。在調(diào)試結(jié)束后,可以通過在包含#include
的語句之前插入#define NDEBUG
來禁用assert調(diào)用,示例代碼如下:
#include
#defineNDEBUG
#include
審核編輯 :李倩
-
C語言
+關(guān)注
關(guān)注
180文章
7595瀏覽量
135878 -
編程語言
+關(guān)注
關(guān)注
10文章
1930瀏覽量
34542 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4284瀏覽量
62325
原文標(biāo)題:C語言斷言函數(shù)assert()的應(yīng)用,清晰明了!
文章出處:【微信號(hào):玩轉(zhuǎn)嵌入式,微信公眾號(hào):玩轉(zhuǎn)嵌入式】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論