?
語言是編程的基石,C語言詭異且有種種陷阱和缺陷,需要程序員多年歷練才能達到較為完善的地步。
雖然有眾多書籍、雜志、專題討論過C語言的陷阱和缺陷,但這并不影響本文再次討論它。
總是有大批的初學(xué)者,前仆后繼的倒在這些陷阱和缺陷上,民用設(shè)備、工業(yè)設(shè)備甚至是航天設(shè)備都不例外。
本文將結(jié)合具體例子再次審視它們,希望引起足夠重視。深入理解C語言特性,是編寫優(yōu)質(zhì)嵌入式C程序的基礎(chǔ)。
1-處處都是陷阱
1.1 無心之過
1) “=”和”==”
將比較運算符”==”誤寫成賦值運算符”=”,可能是絕大多數(shù)人都遇到過的,比如下面代碼:
?
1.?if(x=5) 2.?{ 3.?????//其它代碼??? 4.?}
?
代碼的本意是比較變量x是否等于常量5,但是誤將”==”寫成了”=”,if語句恒為真。
如果在邏輯判斷表達式中出現(xiàn)賦值運算符,現(xiàn)在的大多數(shù)編譯器會給出警告信息。
比如keil MDK會給出警告提示:“warning: #187-D: use of "=" where"==" may have been intended”,但并非所有程序員都會注意到這類警告,因此有經(jīng)驗的程序員使用下面的代碼來避免此類錯誤:
?
1.?if(5==x) 2.?{ 3.?????//其它代碼??? 4.?}
?
將常量放在變量x的左邊,即使程序員誤將’==’寫成了’=’,編譯器會產(chǎn)生一個任誰也不能無視的語法錯誤信息:不可給常量賦值!
2) 復(fù)合賦值運算符
復(fù)合賦值運算符(+=、*=等等)雖然可以使表達式更加簡潔并有可能產(chǎn)生更高效的機器代碼,但某些復(fù)合賦值運算符也會給程序帶來隱含Bug,比如”+=”容易誤寫成”=+”,代碼如下:
?
1.?tmp=+1;
?
代碼本意是想表達tmp=tmp+1,但是將復(fù)合賦值運算符”+=”誤寫成”=+”:將正整數(shù)常量1賦值給變量tmp。
編譯器會欣然接受這類代碼,連警告都不會產(chǎn)生。
如果你能在調(diào)試階段就發(fā)現(xiàn)這個Bug,真應(yīng)該慶祝一下,否則這很可能會成為一個重大隱含Bug,且不易被察覺。
復(fù)合賦值運算符”-=”也有類似問題存在。
3) 其它容易誤寫
使用了中文標點
頭文件聲明語句最后忘記結(jié)束分號
邏輯與&&和位與&、邏輯或||和位或|、邏輯非!和位取反~
字母l和數(shù)字1、字母O和數(shù)字0
這些誤寫其實容易被編譯器檢測出,只需要關(guān)注編譯器對此的提示信息,就能很快解決。
很多的軟件Bug源自于輸入錯誤。
在Google上搜索的時候,有些結(jié)果列表項中帶有一條警告,表明Google認為它帶有惡意代碼。
如果你在2009年1月31日一大早使用Google搜索的話,你就會看到,在那天早晨55分鐘的時間內(nèi),Google的搜索結(jié)果標明每個站點對你的PC都是有害的。
這涉及到整個Internet上的所有站點,包括Google自己的所有站點和服務(wù)。
Google的惡意軟件檢測功能通過在一個已知攻擊者的列表上查找站點,從而識別出危險站點。
在1月31日早晨,對這個列表的更新意外地包含了一條斜杠(“/”)。
所有的URL都包含一條斜杠,并且,反惡意軟件功能把這條斜杠理解為所有的URL都是可疑的,
因此,它愉快地對搜索結(jié)果中的每個站點都添加一條警告。
很少見到如此簡單的一個輸入錯誤帶來的結(jié)果如此奇怪且影響如此廣泛,但程序就是這樣,容不得一絲疏忽。
1.2 數(shù)組下標
數(shù)組常常也是引起程序不穩(wěn)定的重要因素,C語言數(shù)組的迷惑性與數(shù)組下標從0開始密不可分,你可以定義int test[30],但是你絕不可以使用數(shù)組元素test [30],除非你自己明確知道在做什么。
1.3 容易被忽略的break關(guān)鍵字
1) 不能漏加的break
switch…case語句可以很方便的實現(xiàn)多分支結(jié)構(gòu),但要注意在合適的位置添加break關(guān)鍵字。
程序員往往容易漏加break從而引起順序執(zhí)行多個case語句,這也許是C的一個缺陷之處。
對于switch…case語句,從概率論上說,絕大多數(shù)程序一次只需執(zhí)行一個匹配的case語句,而每一個這樣的case語句后都必須跟一個break。
去復(fù)雜化大概率事件,這多少有些不合常情。
2) 不能亂加的break
break關(guān)鍵字用于跳出最近的那層循環(huán)語句或者switch語句,但程序員往往不夠重視這一點。
1990年1月15日,AT&T電話網(wǎng)絡(luò)位于紐約的一臺交換機宕機并且重啟,引起它鄰近交換機癱瘓,由此及彼,一個連著一個,很快,114型交換機每六秒宕機重啟一次,六萬人九小時內(nèi)不能打長途電話。
當時的解決方式:工程師重裝了以前的軟件版本。。。事后的事故調(diào)查發(fā)現(xiàn),這是break關(guān)鍵字誤用造成的。
《C專家編程》提供了一個簡化版的問題源碼:
?
1.?network?code()?? 2.?{ 3.?????switch(line) 4.??????{ 5.?????????case??THING1: 6.??????{ 7.?????????????doit1(); 8.??????????}?break; 9.?????????case??THING2: 10.??????{ 11.?????????????if(x==STUFF) 12.??????????????{ 13.?????????????????do_first_stuff(); 14.?????????????????if(y==OTHER_STUFF) 15.?????????????????????break; 16.?????????????????do_later_stuff(); 17.?????????????}??/*代碼的意圖是跳轉(zhuǎn)到這里…?…*/?? 18.?????????????initialize_modes_pointer(); 19.??????}?break; 20.?????????default?: 21.?????????????processing(); 22.?????}?/*…?…但事實上跳到了這里。*/?? 23.?????use_modes_pointer();?/*致使modes_pointer未初始化*/?? 24.?}
?
那個程序員希望從if語句跳出,但他卻忘記了break關(guān)鍵字實際上跳出最近的那層循環(huán)語句或者switch語句。
現(xiàn)在它跳出了switch語句,執(zhí)行了use_modes_pointer()函數(shù)。但必要的初始化工作并未完成,為將來程序的失敗埋下了伏筆。
1.4 意想不到的八進制
將一個整形常量賦值給變量,代碼如下所示:
?
1.?int?a=34,?b=034;?
?
變量a和b相等嗎?
答案是不相等的。
我們知道,16進制常量以’0x’為前綴,10進制常量不需要前綴,那么8進制呢?
它與10進制和16進制表示方法都不相同,它以數(shù)字’0’為前綴,這多少有點奇葩:三種進制的表示方法完全不相同。
如果8進制也像16進制那樣以數(shù)字和字母表示前綴的話,或許更有利于減少軟件Bug,畢竟你使用8進制的次數(shù)可能都不會有誤使用的次數(shù)多!
下面展示一個誤用8進制的例子,最后一個數(shù)組元素賦值錯誤:
?
1.?a[0]=106;???????/*十進制數(shù)106*/?? 2.?a[1]=112;??????/*十進制數(shù)112*/??? 3.?a[2]=052;???????/*實際為十進制數(shù)42,本意為十進制52*/?
?
1.5指針加減運算
**指針的加減運算是特殊的。**下面的代碼運行在32位ARM架構(gòu)上,執(zhí)行之后,a和p的值分別是多少?
?
1.?int?a=1; 2.?int?*p=(int?*)0x00001000; 3.?a=a+1; 4.?p=p+1;
?
對于a的值很容判斷出結(jié)果為2,但是p的結(jié)果卻是0x00001004。
指針p加1后,p的值增加了4,這是為什么呢?
原因是指針做加減運算時是以指針的數(shù)據(jù)類型為單位。
p+1實際上是按照公式p+1*sizeof(int)來計算的。
不理解這一點,在使用指針直接操作數(shù)據(jù)時極易犯錯。
某項目使用下面代碼對連續(xù)RAM初始化零操作,但運行發(fā)現(xiàn)有些RAM并沒有被真正清零。
?
1.?unsigned?int?*pRAMaddr;?????????//定義地址指針變量?? 2.?for(pRAMaddr=StartAddr;pRAMaddr?
通過分析我們發(fā)現(xiàn),由于pRAMaddr是一個無符號int型指針變量,所以pRAMaddr+=4代碼其實使pRAMaddr偏移了4*sizeof(int)=16個字節(jié),
所以每執(zhí)行一次for循環(huán),會使變量pRAMaddr偏移16個字節(jié)空間,但只有4字節(jié)空間被初始化為零。
其它的12字節(jié)數(shù)據(jù)的內(nèi)容,在大多數(shù)架構(gòu)處理器中都會是隨機數(shù)。
1.6關(guān)鍵字sizeof
不知道有多少人最初認為sizeof是一個函數(shù)。
其實它是一個關(guān)鍵字,其作用是返回一個對象或者類型所占的內(nèi)存字節(jié)數(shù),對絕大多數(shù)編譯器而言,返回值為無符號整形數(shù)據(jù)。
需要注意的是,使用sizeof獲取數(shù)組長度時,不要對指針應(yīng)用sizeof操作符,比如下面的例子:
?
1.?void?ClearRAM(char?array[])?? 2.?{ 3.?????int?i?; 4.?????for(i=0;i?
我們知道,對于一個數(shù)組array[20],我們使用代碼sizeof(array)/sizeof(array[0])可以獲得數(shù)組的元素(這里為20),但數(shù)組名和指針往往是容易混淆的,
有且只有一種情況下數(shù)組名是可以當做指針的,那就是**數(shù)組名作為函數(shù)形參時,數(shù)組名被認為是指針,同時,它不能再兼任數(shù)組名。
**注意只有這種情況下,數(shù)組名才可以當做指針,但不幸的是這種情況下容易引發(fā)風(fēng)險。
在ClearRAM函數(shù)內(nèi),作為形參的array[]不再是數(shù)組名了,而成了指針。
sizeof(array)相當于求指針變量占用的字節(jié)數(shù),在32位系統(tǒng)下,該值為4,sizeof(array)/sizeof(array[0])的運算結(jié)果也為4。
所以在main函數(shù)中調(diào)用ClearRAM(Fle),也只能清除數(shù)組Fle中的前四個元素了。
1.7增量運算符’++’和減量運算符‘--‘
增量運算符”++”和減量運算符”--“既可以做前綴也可以做后綴。
**前綴和后綴的區(qū)別在于值的增加或減少這一動作發(fā)生的時間是不同的。
**作為前綴是先自加或自減然后做別的運算,作為后綴時,是先做運算,之后再自加或自減。
許多程序員對此認識不夠,就容易埋下隱患。
下面的例子可以很好的解釋前綴和后綴的區(qū)別。
?
1.?int?a=8,b=2,y; 2.?y=a+++--b;?
代碼執(zhí)行后,y的值是多少?
這個例子并非是挖空心思設(shè)計出來專門讓你絞盡腦汁的C難題(如果你覺得自己對C細節(jié)掌握很有信心,做一些C難題檢驗一下是個不錯的選擇。
那么,《The C Puzzle Book》這本書一定不要錯過),你甚至可以將這個難懂的語句作為不友好代碼的例子。
但是它也可以讓你更好的理解C語言。根據(jù)運算符優(yōu)先級以及編譯器識別字符的貪心法原則,第二句代碼可以寫成更明確的形式:
?
1.?y=(a++)+(--b);??
當賦值給變量y時,a的值為8,b的值為1,所以變量y的值為9;賦值完成后,變量a自加,a的值變?yōu)?,千萬不要以為y的值為10。
這條賦值語句相當于下面的兩條語句:
?
1.?y=a+(--b); 2.?a=a+1;?
1.8邏輯與’&&’和邏輯或’||’的陷阱
為了提高系統(tǒng)效率,邏輯與和邏輯或操作的規(guī)定如下:
**如果對第一個操作數(shù)求值后就可以推斷出最終結(jié)果,第二個操作數(shù)就不會進行求值!
**比如下面代碼:
?
1.?if((i>=0)&&(i++?<=max)) 2.?{ 3.????????//其它代碼?? 4.?}?
在這個代碼中,只有當i>=0時,i++才會被執(zhí)行。這樣,i是否自增是不夠明確的,這可能會埋下隱患。邏輯或與之類似。
1.9結(jié)構(gòu)體的填充
結(jié)構(gòu)體可能產(chǎn)生填充,因為對大多數(shù)處理器而言,訪問按字或者半字對齊的數(shù)據(jù)速度更快,
當定義結(jié)構(gòu)體時,編譯器為了性能優(yōu)化,可能會將它們按照半字或字對齊,這樣會帶來填充問題。比如以下兩個個結(jié)構(gòu)體:
第一個結(jié)構(gòu)體:
?
1.?struct?{?? 2.?????char? c; 3.???? short s; 4.?????int?? x; 5.?}str_test1;?
第二個結(jié)構(gòu)體:
?
1.?struct?{?? 2.?????char? c; 3.?????int?? x; 4.???? short s; 5.?}str_test2;?
這兩個結(jié)構(gòu)體元素都是相同的變量,只是元素換了下位置,那么這兩個結(jié)構(gòu)體變量占用的內(nèi)存大小相同嗎?
其實這兩個結(jié)構(gòu)體變量占用的內(nèi)存是不同的,
對于Keil MDK編譯器,默認情況下第一個結(jié)構(gòu)體變量占用8個字節(jié),第二個結(jié)構(gòu)體占用12個字節(jié),差別很大。第一個結(jié)構(gòu)體變量在內(nèi)存中的存儲格式如圖2-1所示:
img
圖2-1:結(jié)構(gòu)體變量1內(nèi)存分布
第二個結(jié)構(gòu)體變量在內(nèi)存中的存儲格式如圖2-2所示。
對比兩個圖可以看出MDK編譯器是是怎么將數(shù)據(jù)對齊的,這其中的填充內(nèi)容是之前內(nèi)存中的數(shù)據(jù),是隨機的,所以不能在結(jié)構(gòu)之間逐字節(jié)比較;
另外,合理的排布結(jié)構(gòu)體內(nèi)的元素位置,可以最大限度減少填充,節(jié)省RAM。
img
圖2-2 :結(jié)構(gòu)體變量2內(nèi)存分布
2-不可輕視的優(yōu)先級
C語言有32個關(guān)鍵字,卻有34個運算符。要記住所有運算符的優(yōu)先級是困難的。稍不注意,你的代碼邏輯和實際執(zhí)行就會有很大出入。
比如下面將BCD碼轉(zhuǎn)換為十六進制數(shù)的代碼:
?
1.?result=(uTimeValue>>4)*10+uTimeValue&0x0F;??
這里uTimeValue存放的BCD碼,想要轉(zhuǎn)換成16進制數(shù)據(jù),實際運行發(fā)現(xiàn),如果uTimeValue的值為0x23,按照我設(shè)定的邏輯,result的值應(yīng)該是0x17,但運算結(jié)果卻是0x07。
經(jīng)過種種排查后,才發(fā)現(xiàn)’+’的優(yōu)先級是大于’&’的,相當于(uTimeValue>>4)*10+uTimeValue與0x0F位與,結(jié)果自然與邏輯不符。
符合邏輯的代碼應(yīng)該是:
?
1.?result=(uTimeValue>>4)*10+(uTimeValue&0x0F);??
不合理的#define會加重優(yōu)先級問題,讓問題變得更加隱蔽。
?
1.?#define?READSDA?IO0PIN&(1<<11)??//讀IO口p0.11的端口狀態(tài)?? 2.??????????? 3.?if(READSDA==(1<<11))??????????//判斷端口p0.11是否為高電平??? 4.?{ 5.?????//其它代碼?? 6.?}?
編譯器在編譯后將宏帶入,原代碼語句變?yōu)?
?
1.?if(IO0PIN&(1<<11)?==(1<<11)) 2.?{ 3.?????//其它代碼??? 4.?}?
運算符'=='的優(yōu)先級是大于'&'的,代碼IO0PIN&(1<<11) ==(1<<11))等效為IO0PIN&0x00000001:判斷端口P0.0是否為高電平,這與原意相差甚遠。
因此,使用宏定義的時候,最好將被定義的內(nèi)容用括號括起來。
按照常規(guī)方式使用時,可能引起誤會的運算符還有很多,如表2-1所示。C語言的運算符當然不會只止步于數(shù)目繁多!
有一個簡便方法可以避免優(yōu)先級問題:不清楚的優(yōu)先級就加上”()”,但這樣至少有會帶來兩個問題:
過多的括號影響代碼的可讀性,包括自己和以后的維護人員
別人的代碼不一定用括號來解決優(yōu)先級問題,但你總要讀別人的代碼
無論如何,在嵌入式編程方面,該掌握的基礎(chǔ)知識,偷巧不得。建議花一些時間,將優(yōu)先級順序以及容易出錯的優(yōu)先級運算符理清幾遍。
3 - 隱式轉(zhuǎn)換
C語言的設(shè)計理念一直被人吐槽,因為它認為C程序員完全清楚自己在做什么,其中一個證據(jù)就是隱式轉(zhuǎn)換。
C語言規(guī)定,不同類型的數(shù)據(jù)(比如char和int型數(shù)據(jù))需要轉(zhuǎn)換成同一類型后,才可進行計算。
如果你混合使用類型,比如用char類型數(shù)據(jù)和int類型數(shù)據(jù)做減法,C使用一個規(guī)則集合來自動(隱式的)完成類型轉(zhuǎn)換。這可能很方便,但也很危險。
這就要求我們理解這個轉(zhuǎn)換規(guī)則并且能應(yīng)用到程序中去!
當出現(xiàn)在表達式里時,有符號和無符號的char和short類型都將自動被轉(zhuǎn)換為int類型,在需要的情況下,將自動被轉(zhuǎn)換為unsigned int(在short和int具有相同大小時)。這稱為類型提升。
提升在算數(shù)運算中通常不會有什么大的壞處,但如果位運算符 ~ 和 << 應(yīng)用在基本類型為unsigned char或unsigned short 的操作數(shù),結(jié)果應(yīng)該立即強制轉(zhuǎn)換為unsigned char或者unsigned short類型(取決于操作時使用的類型)。
?
1.?uint8_t??port?=0x5aU; 2.?uint8_t??result_8; 3.?result_8=?(~port)?>>?4;?
假如我們不了解表達式里的類型提升,認為在運算過程中變量port一直是unsigned char類型的。
我們來看一下運算過程:~port結(jié)果為0xa5,0xa5>>4結(jié)果為0x0a,這是我們期望的值。
但實際上,result_8的結(jié)果卻是0xfa!
在ARM結(jié)構(gòu)下,int類型為32位。變量port在運算前被提升為int類型:~port結(jié)果為0xffffffa5,0xa5>>4結(jié)果為0x0ffffffa,賦值給變量result_8,發(fā)生類型截斷(這也是隱式的!),result_8=0xfa。
經(jīng)過這么詭異的隱式轉(zhuǎn)換,結(jié)果跟我們期望的值,已經(jīng)大相徑庭!正確的表達式語句應(yīng)該為:
?
1.?result_8=(unsigned?char)?(~port)?>>?4;?????????????/*強制轉(zhuǎn)換*/?
在包含兩種數(shù)據(jù)類型的任何運算里,兩個值都會被轉(zhuǎn)換成兩種類型里較高的級別。類型級別從高到低的順序是long double、double、float、unsigned long long、long long、unsigned long、long、unsigned int、int。
這種類型提升通常都是件好事,但往往有很多程序員不能真正理解這句話,比如下面的例子(int類型表示16位)。
?
1.?uint16_t??u16a?=?40000;?????????????/*?16位無符號變量*/?? 2.?uint16_t??u16b=?30000;???????????/*16位無符號變量*/?? 3.?uint32_t??u32x;???????????????????/*32位無符號變量?*/?? 4.?uint32_t??u32y; 5.?u32x?=?u16a?+u16b;?????????????????/*?u32x?=?70000還是4464???*/?? 6.?u32y?=(uint32_t)(u16a?+?u16b);????/*?u32y?=?70000?還是4464???*/?
u32x和u32y的結(jié)果都是4464(70000%65536)!不要認為表達式中有一個高類別uint32_t類型變量,編譯器都會幫你把所有其他低類別都提升到uint32_t類型。正確的書寫方式:
?
1.?u32x?=?(uint32_t)u16a?+(uint32_t)u16b;??????或者: 2.?u32x?=?(uint32_t)u16a?+?u16b;?
后一種寫法在本表達式中是正確的,但是在其它表達式中不一定正確,比如:
?
1.?uint16_t?u16a,u16b,u16c; 2.?uint32_t??u32x; 3.?u32x=?u16a?+?u16b?+?(uint32_t)u16c;/*錯誤寫法,u16a+?u16b仍可能溢出*/??
在賦值語句里,計算的最后結(jié)果被轉(zhuǎn)換成將要被賦予值的那個變量的類型。這一過程可能導(dǎo)致類型提升也可能導(dǎo)致類型降級。降級可能會導(dǎo)致問題。比如將運算結(jié)果為321的值賦值給8位char類型變量。程序必須對運算時的數(shù)據(jù)溢出做合理的處理。很多其他語言,像Pascal(C語言設(shè)計者之一曾撰文狠狠批評過Pascal語言),都不允許混合使用類型,但C語言不會限制你的自由,即便這經(jīng)常引起B(yǎng)ug。
當作為函數(shù)的參數(shù)被傳遞時,char和short會被轉(zhuǎn)換為int,float會被轉(zhuǎn)換為double。
當不得已混合使用類型時,一個比較好的習(xí)慣是使用類型強制轉(zhuǎn)換。
強制類型轉(zhuǎn)換可以避免編譯器隱式轉(zhuǎn)換帶來的錯誤,同時也向以后的維護人員傳遞一些有用信息。
這有個前提:你要對強制類型轉(zhuǎn)換有足夠的了解!下面總結(jié)一些規(guī)則:
并非所有強制類型轉(zhuǎn)換都是由風(fēng)險的,把一個整數(shù)值轉(zhuǎn)換為一種具有相同符號的更寬類型時,是絕對安全的。
精度高的類型強制轉(zhuǎn)換為精度低的類型時,通過丟棄適當數(shù)量的最高有效位來獲取結(jié)果,也就是說會發(fā)生數(shù)據(jù)截斷,并且可能改變數(shù)據(jù)的符號位。
精度低的類型強制轉(zhuǎn)換為精度高的類型時,如果兩種類型具有相同的符號,那么沒什么問題;需要注意的是負的有符號精度低類型強制轉(zhuǎn)換為無符號精度高類型時,會不直觀的執(zhí)行符號擴展,例如:
?
1.?unsigned?int?bob; 2.?signed?char?fred?=?-1; 3.???? 4.?bob=(unsigned?int?)fred;??????????????/*發(fā)生符號擴展,此時bob為0xFFFFFFFF*/??
審核編輯:湯梓紅
評論
查看更多