原创:常见C编程错误

类别:编程语言 点击:0 评论:0 推荐:

 

 

 

 

 

 

 

常见编程错误

 

 

 

 

我们总结出一些简单而常见的编程错误, 特列举出其中可能造成潜在危害的例子以供参考,希望对各位有所帮助。

 

一、指针及内存申请、释放

       指针被定义时,就像是拥有了一块指路牌,不同类型的指针将被用来指向不同类型的实物,恰如景点类指示牌指向某个景点,售票处类指示牌指向售票处,这些不同类型的指示牌(不同类型的指针)刚建立时都是白板,也就是其指向是未定义的,这时千万不能直接使用。

例:

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

本文地址:http://com.8s8s.com/it/it28151.htm