程序的執(zhí)行流程是由條件判斷、跳轉(zhuǎn)和循環(huán)構(gòu)成的,沒有任何一個(gè)程序會(huì)缺少程序流的控制。那么像if 、for 、while 、switch 等這些程序員無比熟悉的語句也存在隱患嗎? 事實(shí)上,C 語言是很靈活的,這種靈活性給程序員編寫代碼帶來了很多便利,但同時(shí)也帶來了很多容易導(dǎo)致混淆的表達(dá)。這些表達(dá)完全符合C 語言標(biāo)準(zhǔn),但有時(shí)程序員也難以發(fā)現(xiàn)自己犯了錯(cuò)誤,最終的結(jié)果是使程序進(jìn)入錯(cuò)誤的執(zhí)行流程。即使程序員沒有犯錯(cuò)誤,但有些容易混淆的表達(dá)也會(huì)給其他人讀懂程序帶來困擾,使程序的維護(hù)變得困難。除此以外,有少量控制流程的方式還會(huì)產(chǎn)生不確定的運(yùn)行結(jié)果,而這些結(jié)果也不容易被發(fā)覺。
如何使程序的流程控制清晰、準(zhǔn)確,不產(chǎn)生混淆的表達(dá)呢? MISRA C 給出了很多的相關(guān)規(guī)定,使程序流的控制變得規(guī)范,避免產(chǎn)生各種混淆和不確定性,從而最大程度上減少程序流控制中的失誤, 并使程序的維護(hù)更加容易。
下面從幾個(gè)例子出發(fā),講述這些混淆是如何產(chǎn)生的,最后給出MISRA C 關(guān)于程序流控制的相關(guān)規(guī)則,幫助讀者規(guī)范編程的習(xí)慣。
1 容易混淆的表達(dá)方式先來看這樣兩段代碼:
uint8_t x ,y ;
if ( x = = y) {
foo ( ) ;
}
uint8_t x ,y ;
if (x = y) {
foo ( ) ;
}
在C 標(biāo)準(zhǔn)中,條件語句需要的是布爾值,條件語句表達(dá)式的布爾值實(shí)際上是按照整型處理的,所以這兩段代碼在語法和邏輯上都沒有任何問題。第一段代碼判斷x 是否等于y ,如果相等,調(diào)用foo () 函數(shù);第二段代碼首先將y的值賦給x ,然后判斷x 是否為0 ,如果不為0 ,調(diào)用foo ()函數(shù)。這兩段代碼只相差一個(gè)等號(hào),卻使判斷條件大不相同,程序的執(zhí)行流程會(huì)出現(xiàn)很大差別。
相信讀者在寫程序的時(shí)候都碰到過將“= = ”這個(gè)判斷語句誤寫成賦值語句“= ”的情況。那么面對這兩個(gè)語句時(shí),如何能快速準(zhǔn)確地判斷這是正確的還是程序員的失誤呢? 當(dāng)程序比較簡單的時(shí)候,很容易判斷,但當(dāng)程序流程比較復(fù)雜的時(shí)候,可能花費(fèi)大量時(shí)間還難以給出確定的答案,而這些地方極有可能是有錯(cuò)誤的。
這樣的混淆,事實(shí)上是可以輕松避免的,MISRA C提出了如下強(qiáng)制性的規(guī)則。
規(guī)則13. 1 :賦值表達(dá)式不能用在需要布爾值的地方。
按照MISRA C 的標(biāo)準(zhǔn),第二段代碼應(yīng)該寫成:
uint8_t x ,y ;
x = y ;
if (x ! = 0) {
foo ( ) ;
}
這樣,當(dāng)看到需要布爾值的地方出現(xiàn)了賦值表達(dá)式,
就可以立即判斷這是一個(gè)錯(cuò)誤。在這條規(guī)則下,如下的表達(dá)也是不允許的:
if ( (x = y) ! = 0 ) ) {
foo ( ) ;
}
與這條規(guī)則類似,MISRA C 還提出了如下推薦的規(guī)則,來避免整型變量和布爾型的混淆。
規(guī)則13. 2 (推薦) :判斷一個(gè)值是否為0 應(yīng)該是顯式的,除非該操作數(shù)是一個(gè)布爾值。
這條規(guī)則禁止了如下的表達(dá):
uint8_t x ;
if ( x )
{ ?}
再來看一個(gè)例子:
uint8_t x ;
uint8_t a ,b ;
?
switch ( x ) {
case 1 :
a = b ;
case 2 :
a + = 2 ;
break ;
case 3 :
?
}
同樣,這段代碼在語法和邏輯上也沒有任何問題,編譯器也不會(huì)給出任何錯(cuò)誤或者警告。在程序執(zhí)行中,當(dāng)x等于1 的時(shí)候,將b 的值賦給a ,然后將a 加2 ,退出;當(dāng)x等于2 的時(shí)候,直接將a 的值加2 ,接著退出。但這兒很可能是一段錯(cuò)誤的代碼,程序員的本意有可能是x 等于1時(shí),將b 的值賦給a ,當(dāng)x 等于2 時(shí),直接將a 的值加2 。
為了避免這樣的混淆,MISRA C 提出了如下強(qiáng)制性的規(guī)則。
規(guī)則15. 2 :所有非空的switch 子句都應(yīng)該以break 語句結(jié)束。
按照這條規(guī)則,上面的程序應(yīng)該寫成:
switch ( x ) {
case 1 :
a = b ;
break ;
case 2 :
a + = 2 ;
break ;
case 3 :
?
}
或者:
switch ( x ) {
case 1 :
a = b ;
a + = 2 ;
break ;
case 2 :
a + = 2 ;
break ;
case 3 :
?
}
MISRA C 中還有一些防止程序流控制中出現(xiàn)混淆的規(guī)則。
規(guī)則13. 5 :for 語句中的3 個(gè)表達(dá)式只能和循環(huán)控制相關(guān)。第一個(gè)表達(dá)式只能為循環(huán)變量賦初值,第二個(gè)表達(dá)式只能進(jìn)行循環(huán)條件的判斷,第三個(gè)表達(dá)式只能進(jìn)行循環(huán)
變量增(減) 值。
規(guī)則13. 6 :for 循環(huán)中,循環(huán)變量只能在for 語句的第三個(gè)表達(dá)式中修改,不允許在循環(huán)體中修改。
規(guī)則13. 7 :布爾表達(dá)式的值必須是可以改變的。
例如,如下代碼是不允許的:
uint8_t x ;
?
if ( x >= 0 )
?
錯(cuò)誤在于該條件判斷的結(jié)果始終為真。
規(guī)則14. 1 :不能存在無法執(zhí)行到的代碼。
規(guī)則14. 2 : 非空語句必須要么產(chǎn)生副作用( side-effect) (副作用是指表達(dá)式執(zhí)行后對程序運(yùn)行環(huán)境造成的影響。賦值語句、自增操作等都是典型的具有副作用的操作。) ;或者使程序流程改變。
例如,下面的代碼是不允許的:
?
x > = 3 ;
?
錯(cuò)誤在于x 和3 比較的結(jié)果被丟棄了。
規(guī)則14. 3 :一行中如果有空語句,那么該行只能有這條空語句,不能有別的語句,并且在這條空語句前不能有注釋,注釋必須在其后,用空格隔開。
例如,如下的代碼都是不允許的:
while (port & 0x80 = = 0) {
; foo ( ) ; ①
/ * wait for pin * / ; ②
;/ * wait for pin * / ③
}
其中
①的錯(cuò)誤是除了空語句還有一條語句;
②的錯(cuò)誤是在空語句前有注釋;
③的錯(cuò)誤是空語句與注釋沒用空格隔開。
規(guī)則14. 8 : switch 、while 、do. . . while 和for 語句的主體必須是復(fù)合語句(即用大括號(hào)包含) ,即使該主體只包含一條語句。
例如,如下代碼是符合MISRA C 標(biāo)準(zhǔn)的:
for ( i = 0 ;i < N_EL EMEN TS ; + + i )
{
buffer [ i ] = 0 ;
}
規(guī)則14. 9 :if 結(jié)構(gòu)后面必須是一個(gè)復(fù)合語句(即用大括號(hào)包含) ,else 后面必須是一個(gè)復(fù)合語句(即用大括號(hào)包含) 或者另一個(gè)if 語句。
規(guī)則15. 1 : switch 語句的主體必須是復(fù)合語句(即用大括號(hào)包含) 。
規(guī)則15. 2 :所有非空的switch 子句都應(yīng)該用break 語句結(jié)束。
規(guī)則15. 3 : switch 的最后一個(gè)子句必須是default 子句,如果default 中沒有包含任何語句,那么應(yīng)該有注釋來說明為什么沒有進(jìn)行任何操作。
規(guī)則15. 4 : switch 表達(dá)式不能是一個(gè)有效的布爾值。
例如,下面的代碼是不允許的:
switch ( x = = 0 )
{ ?}
規(guī)則15. 5 : switch 語句必須至少包含一個(gè)case 子句。
2 導(dǎo)致混亂的表達(dá)方式
在C 語言中,有一些表達(dá)方式可以使程序的代碼量減少,但卻會(huì)使程序的結(jié)構(gòu)化程度降低,流程控制變得混亂,可讀性大大降低??聪旅嬉欢未a:
if ( a > 0x02 )
{
loop1 : b + = 1 ;
if ( c > 0xA0 ) {
goto loop3 ;
}
loop2 : a = 2 3 b ;
c = a + b ;
if ( c < 0x40) {
goto loop1 ;
}el se {
goto loop2 ;
}
}
?
loop3 : ?
這段代碼讀起來很困難。實(shí)際編程時(shí),程序員實(shí)現(xiàn)這段功能的代碼自然不會(huì)這樣寫,但是當(dāng)程序流程復(fù)雜的時(shí)候,各種看起來能使編程工作變得輕松的表達(dá),例如goto 、continue 等語句,卻會(huì)使程序流程變得混亂,可讀性降低,而隱藏其中的問題,很可能就無法發(fā)現(xiàn)了。
針對這種情況, MISRA C 給出了下面幾條強(qiáng)制規(guī)則。
規(guī)則14. 4 :不允許使用goto 語句。
規(guī)則14. 5 :不允許使用continue 語句。
規(guī)則14. 6 :循環(huán)體中最多只能出現(xiàn)一個(gè)break 語句用于結(jié)束循環(huán)。
規(guī)則14. 7 :函數(shù)只能有一個(gè)出口,這個(gè)出口必須在函數(shù)末尾。
規(guī)則14. 10 : if . . . else if 結(jié)構(gòu)必須由一個(gè)else 子句結(jié)束。
當(dāng)if 語句后面有一個(gè)或者多個(gè)else if 語句時(shí),最后的一個(gè)else if 必須有一個(gè)與之對應(yīng)的else 語句。如果只有一個(gè)if 語句時(shí),else 不是必須的。
3 不確定的執(zhí)行結(jié)果除了導(dǎo)致混淆和混亂的表達(dá)外,還有一些對浮點(diǎn)數(shù)的操作會(huì)導(dǎo)致不確定的結(jié)果。來看如下一段代碼:
float32_t x ,y ;
? / 3 一些運(yùn)算3 /
if ( x = = y )
{ ?}
if 的條件無法肯定什么情況為真。這是因?yàn)楦↑c(diǎn)數(shù)在計(jì)算機(jī)中無法用二進(jìn)制精確表示,其運(yùn)算總會(huì)存在舍入和切斷誤差,很多人看起來相等的結(jié)果,但計(jì)算機(jī)給出的兩個(gè)浮點(diǎn)數(shù)并不相等,所以上面代碼中if 的主體語句什么情況執(zhí)行是不確定的。MISRA C 給出了兩條相關(guān)的規(guī)定來解決這一問題。
規(guī)則13. 3 :不允許對浮點(diǎn)數(shù)進(jìn)行相等或者不相等的比較,即使是非直接的比較也是不允許的。
例如,如下非直接的比較也是不允許的:
float32_t x ,y ;
?
if ( x < = y )
{ ?}
規(guī)則13. 4 :for 循環(huán)的控制表達(dá)式不應(yīng)包含浮點(diǎn)數(shù)類型。
4 小 結(jié)好的代碼,要安全可靠、有很好的可讀性和可維護(hù)性。在C 語言中,一些表達(dá)方式,可能會(huì)稍微減少程序員編程的工作量,但卻會(huì)使程序的流程變得難以判斷,其中的錯(cuò)誤可能就無法發(fā)現(xiàn)。
按照MISRA C 的標(biāo)準(zhǔn)來寫代碼,就可以避免程序流程產(chǎn)生混淆和混亂,排除其中的不確定因素,使程序真正按照程序員設(shè)想的工作,并使代碼更清晰易懂,真正實(shí)現(xiàn)安全可靠,并具有良好的可讀性和可維護(hù)性。
參考文獻(xiàn)
1 MISRA C:2004 ,Guidelines for the use of the C language in critical systems. The Motor Indust ry Software Reliability As2sociation ,2004
2 Harbison III. Samuel P , Steele J r. Guy L. C 語言參考手冊,
邱仲潘,等譯,第5 版. 北京:機(jī)械工業(yè)出版社,2003
3 Les Hatton. The MISRA C Compliance Suite The next step , Oakwood Computing. http :/ / www. misra c2. com/
4 ISO/ IEC 9899 :1999. International Organization of Standardi2zation , 1999
聯(lián)系客服