0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

C語言斷言函數(shù)assert()的應(yīng)用,清晰明了!

玩轉(zhuǎn)嵌入式 ? 來源:玩轉(zhuǎn)嵌入式 ? 2023-04-12 10:02 ? 次閱讀

對于斷言,相信大家都不陌生,大多數(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 所示。

90658a5a-d8c1-11ed-bfe3-dac502259ad0.png在這里插入圖片描述

圖 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 所示。

906e5e00-d8c1-11ed-bfe3-dac502259ad0.png在這里插入圖片描述

圖 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. 1.在代碼執(zhí)行之前或者在函數(shù)的入口處,使用斷言來檢查參數(shù)的合法性,這稱為前置條件斷言。

  2. 2.在代碼執(zhí)行之后或者在函數(shù)的出口處,使用斷言來檢查參數(shù)是否被正確地執(zhí)行,這稱為后置條件斷言。

  3. 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

審核編輯 :李倩


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • 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)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    C語言assert的使用

    assert意思是斷言,常用在程序的DEBUG版本中。
    發(fā)表于 07-21 14:51 ?790次閱讀

    什么是斷言?C語言中斷言的語法和用法

    在軟件開發(fā)過程中,我們經(jīng)常需要處理各種錯(cuò)誤和異常情況。為了提高代碼的健壯性和可靠性,我們需要使用一些工具和技術(shù)來檢測和處理這些問題。本篇博客將深入探討C語言中斷言的使用,幫助讀者更好地理解和應(yīng)用斷言,提高代碼的質(zhì)量和可維護(hù)性。
    發(fā)表于 08-03 10:34 ?2581次閱讀

    解析C語言斷言函數(shù)的使用

    對于斷言,相信大家都不陌生,大多數(shù)編程語言也都有斷言這一特性。簡單地講,斷言就是對某種假設(shè)條件進(jìn)行檢查。 在 C
    發(fā)表于 08-08 09:51 ?437次閱讀
    解析<b class='flag-5'>C</b><b class='flag-5'>語言</b><b class='flag-5'>斷言</b><b class='flag-5'>函數(shù)</b>的使用

    C語言assert(斷言)簡介

    assert的功能,條件為真,程序繼續(xù)執(zhí)行;如果斷言為假(false),則程序終止。
    的頭像 發(fā)表于 11-17 16:33 ?1097次閱讀
    <b class='flag-5'>C</b><b class='flag-5'>語言</b><b class='flag-5'>assert</b>(<b class='flag-5'>斷言</b>)簡介

    請問HAL函數(shù)對Handle有效性的檢查為什么不是用assert_param斷言

    )); ...... } 以HAL_SPI_Init為例,hspi參數(shù)的檢查并沒有使用assert_param斷言宏,如果是我實(shí)現(xiàn)的話,我會(huì)用assert_param(hspi != NULL)實(shí)現(xiàn)。一般
    發(fā)表于 05-08 07:00

    斷言ASSERT)的用法

    STM32中經(jīng)常出現(xiàn)assert函數(shù),網(wǎng)上看了篇博客分享下:我一直以為assert僅僅是個(gè)報(bào)錯(cuò)函數(shù),事實(shí)上,它居然是個(gè)宏,并且作用并非“報(bào)錯(cuò)”?! ≡诮?jīng)過對其進(jìn)行一定了解之后,對其作用
    發(fā)表于 08-23 09:33

    C語言中斷言如何去使用

    文章目錄1 C語言中斷言的使用1.1 處理方式1.2 原型定義1.3 示例代碼1 C語言中斷言的使用1.1 處理方式如果斷言的條件返回錯(cuò)誤,
    發(fā)表于 07-14 08:15

    C語言中斷言是怎樣使用的?

    C語言中斷言是怎樣使用的?
    發(fā)表于 10-14 07:18

    何為斷言?斷言該怎么使用呢

    存在錯(cuò)誤。因此,斷言是提高程序可靠性的有效手段。也是開發(fā)階段快速定位問題的一種很好防御式編程方法。在C語言中,斷言是一些條件判斷的宏。比如C
    發(fā)表于 09-21 14:59

    C語言標(biāo)準(zhǔn)庫函數(shù)

    C語言標(biāo)準(zhǔn)庫函數(shù),使用C語言編程時(shí),常用到的函數(shù)
    發(fā)表于 05-11 16:41 ?0次下載

    怎么理解Assert中的斷言語句?

    為什么項(xiàng)目中的代碼需要有Assert斷言語句?
    的頭像 發(fā)表于 03-03 14:12 ?2704次閱讀

    如何得當(dāng)使用C語言的特殊的用法

    C語言有很多特殊的用法,如果這些特殊用法使用得當(dāng),會(huì)是你的代碼變得更加有健壯,更加容易維護(hù)。 比如我們在使用STM32庫的斷言assert),你會(huì)發(fā)現(xiàn)官方提供了包含__FILE__
    的頭像 發(fā)表于 09-27 10:41 ?1897次閱讀
    如何得當(dāng)使用<b class='flag-5'>C</b><b class='flag-5'>語言</b>的特殊的用法

    STM32函數(shù)Assert斷言機(jī)制

    編寫代碼時(shí),我們總是會(huì)做出一些假設(shè),斷言就是用于在代碼中捕捉這些假設(shè),可以將斷言看作是異常處理的一種高級形式。斷言表示為一些布爾表達(dá)式,程序員相信在程序中的某個(gè)特定點(diǎn)該表達(dá)式值為真??梢栽谌?/div>
    發(fā)表于 02-08 15:29 ?2次下載
    STM32<b class='flag-5'>函數(shù)</b>庫<b class='flag-5'>Assert</b><b class='flag-5'>斷言</b>機(jī)制

    C語言進(jìn)階】利用assert高效排查你的C程序

    C語言進(jìn)階】利用assert高效排查你的C程序
    的頭像 發(fā)表于 08-31 13:27 ?2065次閱讀

    防御式編程之斷言assert的使用

    防御式編程的重點(diǎn)就是需要防御一些程序未曾預(yù)料的錯(cuò)誤,這是一種提高軟件質(zhì)量的輔助性方法,斷言assert就用于防御式編程,編寫代碼時(shí),我們總是會(huì)做出一些假設(shè),斷言就是用于在代碼中捕捉這些假設(shè)。使用
    的頭像 發(fā)表于 04-19 11:35 ?632次閱讀