<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
常見編程錯誤
我們總結出一些簡單而常見的編程錯誤, 特列舉出其中可能造成潛在危害的例子以供參考,希望對各位有所幫助。
一、指針及內存申請、釋放
指針被定義時,就像是擁有了一塊指路牌,不同類型的指針將被用來指向不同類型的實物,恰如景點類指示牌指向某個景點,售票處類指示牌指向售票處,這些不同類型的指示牌(不同類型的指針)剛建立時都是白板,也就是其指向是未定義的,這時千萬不能直接使用。
例:
u8 dfd2_10Cmd_CheckEraseBlockSuccess(u16 * pp_AdrrBeginErase)
{
……
volatile u16* pl_Adress_Of_Erase;
do
{
vl_Data1 = *pl_Adress_Of_Erase;//Dangerous!!!
……
其中的指針pl_Adress_Of_Erase尚未指向任何東西就開始被使用了,危險。
要使用這些指示牌的第一個工作就是在上面畫上與該類型指示牌對應類型的實物的圖像或文字,比如“飛來峯”,同時把指示牌的箭頭指向飛來峯的方向(這就是給指針賦值,讓其指向實物)。改變指針的值就比如將指示牌的內容重新刷成“西湖”,同時把指示牌的箭頭扳過去。除非強制指定類型,否則這塊用於指示景點的指示牌是不能用於指向非景點的。有時,指針的指向實物要在用時才建立起來的,比如建立一個指針,讓它指向臨時緩存,這個緩存是臨時的,那麼就需要立刻建立起來,通常我們可以利用malloc來申請一塊內存,要注意的是,這塊內存的內容是隨機的,因此需要清理才能使用(使用memset等)。類似的過程是:我們要新建景點,首先要申請獲得一塊土地,剛申請得到的土地上雜草叢生(需要清除乾淨),然後指示牌將被刷成“新景點”並將箭頭指向新的土地,這樣別人就可以通過指示牌找到對應的土地進行處理了。一個低級的錯誤經常發生在這樣的情況:我們擁有了一塊白板的指示牌,未指向任何地方,但是編程者十分清楚該指示牌將被用來指向“新景點”。在新景點的土地尚未申請的情況下,我們發出一個動工命令,希望其中的推土機去剷平那塊新景點的土地,於是危險的情況發生了!白板指示牌的箭頭當前所指方向是不確定的(新建指針的指向是隨機的),推土機按照這個胡亂的指向開始去剷除!這種情況在很多運行環境下將導致嚴重的非法操作!
如果指針指向的內存是動態申請得來的,在使用完畢時應該將內存釋放掉,同時該指針的內容應該設爲NULL(相當於擦除指示牌內容,包括其箭頭指向),這樣可以讓調用者發現指針爲空而報錯並暫停處理。拿“新景點”的指示牌爲例,若新景點的土地已收回而指示牌不更新,別人按照指示牌的指向繼續進行訪問(比如加蓋一幢樓),這將造成嚴重的後果!
指針是有生存期的,常常我們會建立一些局部(比如函數內或循環內等等)的指針,那麼這些指針的生存週期也就在該局部中,如果這些指針所指向的申請的內存,在該段局部程序結束前沒有釋放,那麼指針消亡時指向的內存還被程序佔用,若該內存已經沒有被其它指針所指向,就導致了內存的泄漏。特別對一個長時間運行的程序而言,每次運行這一段程序就申請一段內存,但是有借無還,不久內存就會因被申請完而崩潰(在Windows系統中,物理內存用完時會將硬盤作爲虛擬內存,所以還可抵擋一陣,但系統性能迅速下降),內存申請與釋放的機制沒有理順,再多的內存終將迅速導致崩潰。
二、數組超界
數組的長度若爲N,則可以訪問的數組下標爲0~(N-1),這一點常常由於粗心而被疏忽導致超界。
例:
“mms_ui_retrieve_hndlr.c", line 2320: Warning: C2914W: out-of-bound offset 32 in address
void mms_ui_get_mm_contents_conf_hndlr(…)
(*(text_file_names+index)).file_name[FS_MAX_FILENAME_SIZE] = 0; //should be FS_MAX_FILENAME_SIZE-1 !!!!
另外一個原因是由於擴展或其它修改,導致定義和使用不一致造成超界。最常見的就是定義數組時用了宏來表示長度,而程序中使用的地方卻直接用數字作爲下標,一旦數組的長度由於宏被改變而改變,這些直接以數字作爲數組下標訪問的程序總會產生問題,訪問範圍超出數組將導致嚴重的後果。
例:
hfd1sem.c
u32 hfd1_1InitStartMode()
a_EraseSector[TYPE_16K] = a_AddressSector[i].v_AddressBeginSector;
而相關定義爲:
#define NB_ERASE_SECTOR 0x02
GLOBAL u32 a_EraseSector[NB_ERASE_SECTOR];
#define TYPE_64K 0x00
#define TYPE_8K 0x01
#define TYPE_16K 0x02
由此可見,TYPE_16K或許是後續升級添加的,但是數組a_EraseSector的元素個數(NB_ERASE_SECTOR)並未同時升級,導致了數組訪問超界。
三、函數返回
1、應返回而未返回
這類錯誤是很危險的,編譯器報warning爲:implicit return in non-void function。一個函數若是僅使用其功能,調用時不依賴其返回值的話問題不大,但是,一旦上層調用依賴其返回值的話可能就會碰上麻煩了。換個角度講,既然是給函數設計了返回值,大多數情況下就是要依賴它的返回值的。
例:
u32 mms_ui_strlen(const u8 *srcString)
{
if(srcString[0] == 0x80)
{
/*It is a unicode*/
return srcString[1];
}
else
{
/*It is USASCII*/
strlen((const char*)srcString);
}
}
在這個典型的錯誤例子裏,該函數需要返回字符串的長度,但是其中一個條件分支卻忘記返回一個值,那麼調用該函數的地方
len = mms_ui_strlen((u8*)&string_buf[0]); )就極其驚險了!
另外一種情況是程序中返回了,但是沒有返回值:
例:
u16* dsc0_46FillIconInColorIconBuf(…)
if((mv_u16BitmapXwidth == 0) || (mv_u16BitmapYheight == 0))
return;
然而,對上述函數的調用是要使用返回值的:
pu16IconBufPtr = dsc0_46FillIconInColorIconBuf(…);
2、不該返回而返回
這種情況算是比較微小的問題了,功能沒有影響,但終究顯示出了設計、編程的粗糙。
3、返回局部變量
這是一個比較容易犯的錯誤,運行時導致的結果或許是致命的。首先我們都清楚局部變量(包括指針)的生命週期都是局部而短暫的,在函數中返回一個局部變量的時候有兩種情況:返回局部的數值變量、結構變量等,其實是將該變量的值返回出去,調用的地方得到了正確的值就行了。另一種情況是需要返回函數中的一個緩存(比如一個局部字符串)的內容,常常可以看到以下的錯誤程序:
例:
char * func()
{
char buf[20] = {0x00};
char *p = buf;
int i;
sprintf(buf, "0123456789987654321");
return buf; //return p; //SAME!!!
}
編譯時將產生警告:function returns address of local variable,即返回了局部變量的地址。因爲一旦退出該函數,局部的數組buf可能將被另作它用,其中的內容不可預知。但是通常這種錯誤比較隱蔽,原因是一般情況下通過返回的地址去訪問,原來的局部數組的內容還沒完全被修改,有時還是完整的。但是我們應該非常清楚這是一類不可忽視的錯誤,必須保證不再訪問局部的已不能確定是否存在的內容。解決的辦法是使用生存期足夠長的數組,或者使用動態申請得到的內存(調用malloc)。
4、指針作爲參數
指針作爲參數時,除了可以修改該指針所指的內存的內容外,我們常常會以爲指針本身被修改後在調用完本函數後繼續有效,但情況並非如此!
例:
void RemoveSpecialHeader(char *p_src)
{
if (p_src == NULL)
{
return;
}
if (p_src[0] == 0x0A)
{
p_src = p_src +1;
}
}
char strEditBuf[] = “/x0aTEXT begin here …”;
char *p_Str = strEditBuf;
printf("0x%02X/n", p_Str[0]); //show in hex format: 0xNN
RemoveSpecialHeader(p_Str);
printf("0x%02X/n", p_Str[0]); //show in hex format: 0xNN
這段程序就是想讓函數RemoveSpecialHeader()對傳入的指針p_Str的字符串進行特殊首字符判斷,若發現則跳過該字符(修改p_Str指針)。傳給函數RemoveSpecialHeader()的參數是p_Str這一指針的值,進入函數後,函數擁有的卻是p_Str的副本,所以意在修改p_Str的值,其實是僅僅修改了這個指針副本的值,調用RemoveSpecialHeader()結束後,指針p_Str的值並未被改變,依舊指向strEditBuf的第一個元素,運行的結果(兩次打印0x0A) 證明了這一點。這種在函數內部修改傳入的指針的情況與修改傳入的變量一樣,都是徒勞而已。
四、其它常見誤用
1、Memcpy
memcpy的原型是void* memcpy(void * out, const void * in, size_t n);
它的作用是將從 in指針指向的內存地址處,拷貝n各字節到out指針指向的內存。我們的程序中經常需要將一塊內存清成全爲0x00,應該使用 memset函數。但是實際的很多地方都錯誤的調用了memcpy,要命的是這樣的問題在編譯階段連個警告也不會產生,但實際結果卻完全可以算是錯誤了。
例:
Lk4driv.c
ColorWindowShow()
memcpy(p1_WindowDisplay->WindowString, 0, sizeof(winstring));
這裏的memcpy的功能將從內存0x00000000的地方開始去讀取一些字節,寫到目的內存裏。在很多操作系統裏,對內存的訪問地址是有嚴格限制的,明顯0x00000000的地方不是用戶程序可以訪問的地方,因此在Windows中將導致非法操作,unix中將導致Segmentation fault並被強制中止!
同樣的,memcpy(dest, ‘/0’, length)也是完全一樣的錯誤,原因是其中的’/0’其實就是0x00, 函數將把它當作一個地址。
另外,memcpy(dest, “/x00”, length)這種寫法也是有問題的,”/x00”作爲字符串,本身只有一個字節,加上一個結束符0x00,這個字符串在編譯的時候放在了用戶程序的數據段中(當然訪問權限沒問題),調用時將從該字符串的首地址開始拷貝,但是按length來拷貝,通常都要超出那僅有的兩個字節,最終多拷貝了很多緊跟在字符串後面的雜數據。
所以,memcpy從0x00000000讀取並拷貝是很危險的,應該使用:
memset(dest, 0x00, length);
2、== 與=
if 判斷處誤用“=”:
例:
void mms_ui_save_in_pending_hndlr()
if(validity_period = MMS_YES)
這種錯誤導致本身僅進行判斷的變量被出乎意料且無條件的修改了值,對後續程序走向造成重大影響。
注意:有些情況下條件中用“=”是特殊的,雖然產生同樣的警告,但設計目的就是先將變量賦值,然後判斷變量是否爲TRUE或FALSE。特別在讀取文件、串口、網絡數據時常用,但這種方式比較讓人混淆,不值得推薦,最好分兩步寫。
例:條件中先取值再判斷
configuration.c中:
static void setProp (…)
if (p = (Property *) jam_malloc (sizeof(Property)))
{
……
}
賦值處誤用 “==”:
例:
no side effect in void context: '<expr> == <expr>'
bool sms0_4if()
case IF_SAVED_IN_FLASH:
{
v_status == FALSE;//Should be ‘=’
……
}
這種情況導致變量的值一直沒有被修改,也將對程序走向造成重大影響。
3、對指針取長度
例:
char str[10];
char *p = str;
sizeof(p)
這裏,本意是要通過指針p對str取長度,但是事實是sizeof(p)只返回指針p本身的大小(4),而不是其指向的內容的長度。
4、條件判斷取非(!)誤用取反號(~)
當要判斷的變量的值爲0時:
~0 與 !0一樣,條件爲 TRUE,但一旦變量值不爲0時,比如!2爲FALSE,~2的意思就是將0x02逐位取反,所以~2還是爲TRUE。所以!與~的誤用只有在後面的值爲0是才相同,其它情況都是不相同的!
例:
static bool minute_right=FALSE;
void lk8_1UpdateTime(…)
if ((~minute_right)&&(test_minute_times<4)&&((t_Minute-u16True_minute)==1)) {
test_minute_times++;
}
5、混淆字符與字符串
一個字符一般就是一個字節而已,用單引號來定義,而字符串是用雙引號括起來表示的。字符的長度非常明顯,而字符串的長度卻總是定義時的預置字符總數加上默含的一個結尾符:0x00。這一點在字符串含單個可見字符時常常混淆,大家一定要分清“0”(0x30 0x00)、‘0’(0x30)、“/x00”(0x00 0x00)以及‘/0’(0x00)的概念。
例:
dir2sub.c", line 500: Warning: C2203W: non-portable - not 1 char in '...'
void dir2_7save_phone_book_data(…)
strchr(ga_DirPhoneNb,'*w')
這裏,strchr的第二個參數是一個字符,而實際的情況是單引號中間兩個普通字符(非轉義字符),頗讓人費解。若是想查找字符串的話,strchr函數是無法一次完成的,而且字符串也該用雙引號括起。
查找字符串:
strpbrk - find chars in string,
eg. nPos = strpbrk(strAddr, "@:")
6、超出範圍
例:16位值賦值給8位變量
u32 mat73_04MapColorLCD(…)中
u8 vp_bgColor = 0xffff;
類似的問題可能是變量類型更改或升級數據長度而遺忘升級變量等造成的,起碼是修改時考慮不周全,可能會造成一些錯誤。
比如 u8 vp_bgColor = BGCOLOR_WHITE;而在定義BGCOLOR_WHITE的時候當初是0xff, 升級修改時將BGCOLOR_WHITE擴成了16位,導致了原來的賦值出了問題。
字符串的使用中也常常有超出範圍的,比如15個字節的數組中塞了15個字節,導致該字符串本身沒有了字符串結束符0x00, 對它的複製等操作若不指定長度,一直會衝出該字符串直至後面內存中出現0x00爲止,可謂隱患不小。
例:
char* lk4_202GetMMSSettingStr(…)
static ascii va_IpAddress[15] = "000.000.000.000";
還有一種情況是,一個緩存只有n個字節,拷貝的時候卻從中讀取遠大於n的字節,或者往其中寫的時候超過了n個字節,這樣做將導致不可預知的崩潰。
例:
blackjack.c
u8 bufferptr[400];
void DSP_DrawPString(…, u8 *pStr, …)
{
if(*pStr == UNI_HEADER)
{
u16Ypos += 100;
memcpy(bufferptr,pStr+1,400);
}
else
{
memcpy(bufferptr,pStr,400);
}
}
調用處:
u8 bTempstr[4];
……
DSP_DrawPString(…, bTempstr, …);
7、變量使用時尚未賦值
這種情況通常發生在疏忽的地方,以下爲兩種常見的情況:
例:
int char_width;//沒有初值!
if (ConditionA)
{
char_width = 16;
}
else
{
if (ConditionB)
{
char_width = 24;
}
//else的情況沒有給char_width賦值,造成疏忽
}
if(char_width == 16) ……
使用char_width,但此時有可能char_width的值是不確定的
例:
int attrib1,attrib2;//沒有初值!
switch (sometype)
{
case AA:
{
attrib1 = 12;
attrib2 = 16;
}
break;
case BB:
{
attrib1 = 12;
//忘記給attrib2賦值
}
break;
}
//此處使用attrib2即造成後果不可預料
五、技巧與提示
1、獲取unix命令執行結果
使用nohup可以將命令(程序或shell指令)的輸出全部保存到文件中,默認的文件名是nohup.out,若該文件已存在則每次累加。
例:將compile_all.sh的結果全部輸出到nohup.out文件
nohup ./compile_all.sh
nohup命令與管道重定向不太一樣,“>>”和“>”在默認狀態下似乎不能將標準錯誤(standard error)重定向,而nohup可以。
2、數組初始化
將數組中所有的元素都初始化爲0x00,其實非常簡單,只要顯式的將第一個元素設爲0x00就行了,編譯器將自動將其他元素也初始化爲0x00。
例:
char strAddress[50] = "/x00"; //整個字符串初始化,每個字節爲0x00
注意:若想用這種只指定第一個元素值的方法來初始化數組的值全爲其它值是不合情理的,比如:
例:
long lTotals[100] = {300}; //整個數組元素將只有首個爲300, 其餘全爲0