- 程序设计缺陷分析与实践
- 尹浩 于秀山编著
- 123字
- 2018-12-27 12:36:09
第2章C/C++语言程序设计缺陷分析
C/C++是一种介于汇编语言和高级语言之间的中级语言,由于其容易理解,便于阅读和书写,成为很多程序员的首选编程语言。在本章中,笔者将C/C++程序典型的代码缺陷分类列出,并结合软件测试实践,对每种缺陷都给出一段实例代码,方便读者理解。
2.1 编码风格
2.1.1 符号误用问题
符号是程序的一个基本组成单元,其作用相当于句子中的单词,每个符号都有其特定的含义。在程序语句中,符号的误用常常引起语句错误,导致程序异常。
1.布尔型变量被赋值
例1:
1 void f(bool flag)
2 {
3 int a;
4 bool ok;
5 ok=true;
6 if(flag=ok) // flag是一个bool型变量
7 a++;
8 }
例1中,第6行条件判断语句,存在布尔型变量flag被赋值的错误。
2.“==”运算符与“=”赋值符混用
由于“= =”运算符和“=”赋值符形似,二者经常混用。例2和例3中的if语句应使用运算符“==”,但误用了赋值符“=”。
例2:
1 void foo(int i, int j)
2 {
3 int t;
4 if(i=j)
5 t++;
6 }
例2中,第4行语句本意是比较i和j的值是否相等,而“if(i=j)”执行的操作是把j值赋给i,再判断i的值是否为0。
例3:
1 class A
2 {
3 int t;
4 int q();
5 };
6 main()
7 {
8 int num;
9 A a;
10 if (i=a.q())
11 {
12 num++;
13 }
14 }
例3中,第10行条件判断语句中,“==”判断符误用为“=”赋值符,函数调用语句出现在if条件判断语句“=”赋值符的右边。
例4:
1 f()
2 {return 1;}
3 main()
4 {
5 int a;
6 int array[3]={1,12,13};
7 if ((a==array[2])< 0)
8 f();
9 else return -1;
10 }
例4中,第7行语句本意是将array[2]赋值给a后,再将a的值和0作比较,如果条件为真则执行函数f();当使用“==”后,“(a= =array[2])”的值只能是0或1,不会小于0,函数f()永远不会执行。
3.按位运算符“&”、“|”和逻辑运算符“&&”、“||”混用
例5:
1 f()
2 {return 1;}
3 main()
4 {
5 int a=8;
6 int b=1;
7 if(a&b) f();
8 }
例5中,第7行条件判断语句“if(a&b)”本意是逻辑与运算“&&”,被误用为按位与运算符“&”后,“8&1”结果为0,函数f()将不会执行。
4.单引号‘ ’和双引号“ ” 字符混用
程序中单引号的字符,比如'a'代表一个字符,字符在编译器中有其对应字符集中的序列值,也可以说单引号字符代表一个整数;而双引号代表一个字符串,字符串在编译器中代表一个指向无名数组起始字符的指针。
例6:
1 main ()
2 {
3 char * p;
4 p='a';
5 }
6
例6中,指针P是字符型的指针,‘a’代表一个整数,赋值号两边的数据类型不匹配。
5.赋值表达式错用其他操作符
例7:
1 int main(int k)
2 {
3 int j=1;
4 k=0;
5 if(j)
6 {
7 j>k;
8 return j;
9 }
10 else
11 {
k++;
12 return k;
}
13 }
例7中,第7行语句错用操作符“>”代替赋值表达式操作符“=”,使得该语句没有任何意义。
6.分号使用不当
程序中不小心多了或少了一个分号,这个分号也许会作为不会产生任何实际效果的空语句,但在某些情况下,却可能造成不良后果。
例8:
1 #include <stdlib.h>
2 main()
3 {
4 int a, c ;
5 int array[3]={10, 06, 12};
6 scanf("%d",&a);
7 if(a < 0);
8 c=a;
9 }
例8中,第7行语句多了一个分号,那么紧跟在if语句之后的语句“c=a;”就是一条单独的语句,和条件判断部分完全没有任何关系了。
例9:
1 #include <stdlib.h>
2 void f()
3 {
4 int a;
5 float array[3];
6 scanf("%d",&a);
7 …
8 if (a < 0)
9 return
10 array[2]=a;
11 }
例9中,第9条语句少了一个分号,程序会把“array[2]=a;”语句中分号之前的内容作为return的返回内容,而该程序中函数f()不应返回任何内容。
有时程序中会误用中文分号作为程序中所使用的英文分号,在这种情况下,编译器会对这个错误的分号产生一条告警信息。
7.自增/自减(++/--)运算符和变量间有空格
程序中无论是自增或自减运算符,运算符和变量之间不能有空格。
例10:
1 main()
2 {
3 int i=6;
4 int j=7;
5 -- i;
6 j ++;
7 }
例10中,自减或自增符号和变量i、j之间都有空格,为了增加程序的可读性,应该去除自减或自增符号和变量i、j间的空格。
8.错误使用自增/自减(++/--)运算符
自增/自减运算符(++/--)表达简练,因此在C/C++编程中会经常用到,但就是这个几乎在每个程序中都会用到的运算符,如果不注意细节,就会产生错误。
例11中的程序遍历数组arr[10]的每个元素,如果元素的值不等于2,则执行自增/自减操作,并打印自增/自减后的值。
例11:
1 #include<stdio.h>
2 int main()
3 {
4 int arr[10] = {2,3,1,2,3,3,1,2,2,3};
5 int tmp = 0;
6
7 for(int i = 0; i<10; i++)
8 {
9 if(arr[i] < 2)
10 {
11 tmp = arr[i]++;
12 printf("arr[%d] = %d\n", i, tmp);
13 }
14 else
15 if(arr[i] > 2)
16 {
17 tmp = arr[i]--;
18 printf("arr[%d] = %d\n", i, tmp);
19 }
20 }
21 return 0;
22 }
按照程序设计意图,每次打印语句中的元素值应该都是2,但是查看结果发现,打印出来的值没有变,这是因为错误使用了自增/自减运算符。
单独使用a++和++a时,其结果是一样的,都相当于a=a+1,但tmp= a++和tmp= ++a得到的结果是不一样的:
tmp= a++,相当于tmp = a;a=a+1。
tmp= ++a,相当于a=a+1;tmp = a。
前者先赋值再自增,后者先自增再赋值,即包含的赋值操作的顺序不一样,结果也就不一样了,同时两者的效率也不一样,对于a++,需要用一个临时变量来保存a的值,然后执行自增操作;而对于++a来说,整个表达式的值就是a的值,无需进行中间值的复制操作,因此其效率要高一些。
在例11中,第11行语句和第17行语句赋给tmp的值并非是数组元素自增/自减后的值,而是数组元素本来的值。
在需要使用自增/自减运算符时,如无特殊情况,建议使用++a(--a)格式的语句。
9.字符串结束符被误用
C语言中没有专门的字符类型,通常用字符数组来存放字符串,以'\0'作为字符串的结束符。如果结束符'\0'被误用为"\0"时,可能导致程序崩溃。
例12:
1 Main()
2 {
3 char a[10];
4 for(i=0;i<9;i++)
5 a[i]=i;
6 a[9]= '\0';
7 …
8 f(a);
9 }
例12中,第6行语句欲把‘\0’作为结束符赋值给字符型数组a的最后一个元素,在这种情况下,C++编译器不会把字符‘\0’赋值给一个数组元素,但C编译器会这样做。这种错误在编程时不易察觉,需要注意。
10.对浮点型变量进行相等比较
浮点数在内存中的存储机制和整数不同,有舍入误差,在计算机中用以表示某个近似实数,但无法精确。具体来说,这个实数由一个整数或定点数乘以某个基数(计算机中通常是2)的整数次幂得到,这种表示方法类似于基数为10的科学记数法。因为浮点数是非精确存储,所以比较浮点数是否相等时,不能用关系运算符中的“= =”、“!=”、“>=”和“<=”进行比较运算。
例13:
1 int main()
2 {
3 float a;
4 float b;
5 …
6 if(a==b)
7 return 1;
8 …
9 }
例13的第6行语句中,直接使用关系运算符“==”比较两个浮点数是否相等。当需要判断两个浮点数是否相等时,应该先设定一个精度,比较两个浮点数差的绝对值是否在这个精度范围内。
修改方法:将第6行语句改为“if(fabs(a-b) < 1.0E-10)”。
11.程序中运算符的优先级使用错误
编程时如果不知道运算符的优先级,可能导致程序中运算表达式错误,进而引起程序异常。
对于运算符之间的优先级,记住关键两点:
① 所有逻辑运算符的优先级都低于任何一个关系运算符;
② 移位运算符的优先级比算术运算符低,但比关系运算符高。
例14:
1 #include <stdlib.h>
2 main()
3 {
4 int a, b, c;
5 scanf("%d",&a);
6 scanf("%d",&b);
7 c = ( a & b!=0 )
8 }
例14中,第7行语句本意是“c = ( (a & b)!=0 )”,即a和b先做按位与运算,运算后的值再和0作比较,但由于运算符“!=”的优先级大于“&”,所以条件表达式变为“c=(a&(b!=0))”,即b和0比较的结果,再和a值按位与运算,导致错误的结果。
12.使用逗号导致程序发生错误
例15:
1 void FpParser(USHORT *pUnit,USHORT carrierNumber,
USHORT frameLen)
2 {
3 …
4 if(snmp_var.syntax==SNMP_SYNTAX_OCTETS)
5 {
6 memcpy(cMsg,snmp_var.value.string.ptr,
snmp_var.value.string.len),
7 FpParser((USHORT*)cMsg,g_config.m_CarrNum,
g_config m_FramLen); }
8 …
9 }
例15中第6行语句,函数之间用逗号分隔,在这种情况下,只返回最右边函数的值。
修改方法:分开写,都用分号。
2.1.2 变量初始化问题
使用未初始化变量是很常见的程序错误,编译器把变量存放在内存中的某个位置,如果变量没有正确初始化,则该变量存储的数据是未知的,在程序中直接使用该变量可能引起计算错误甚至软件崩溃等问题。虽然许多编译器在检查出使用未初始化变量时都会给出一个警告(Warning),但并不要求程序员必须修改此问题,而且,没有一个编译器能发现所有的使用未初始化变量的错误。
13.条件判断语句导致使用未初始化的堆内存
程序中使用内存分配函数malloc()为结构体分配堆内存后,可能导致分配的堆内存没有初始化就被使用,此处特指使用条件判断语句导致堆内存未初始化的情况。
例16:
1 struct student{
2 int num;
3 char[10] name;
4 };
5 int main(int t)
6 {
7 struct student *s=malloc(sizeof(struct s));
8 if (t>0)
9 {
10 s->num=t;
11 s->name="liyue";
12 }
13 return s->num;
14 }
例16中,第8行语句if条件不成立时,第13行语句return返回的将是没有初始化的“s->num”。
14.使用未初始化的堆内存
程序中使用内存分配函数malloc()为结构体分配堆内存后,可能存在堆内存没有初始化就被使用的错误。
例17:
1 struct student{
2 int num;
3 char[10] name;
4 };
5 int main(int t)
6 {
7 struct student *s=malloc(sizeof(struct s));
8 int t;
9 t= s->num;
10 }
例17中第9行语句,结构体s还没有初始化就被使用。
15.条件判断语句导致读取结构体中未赋值的局部变量
程序中的条件判断语句,可能导致在结构体中局部成员没有赋值的情况下,就读取该成员的值或将该成员作为参数传递给某个函数。
例18:
1 struct s
2 {
3 int a;
4 int b;
5 };
6 main(int t)
7 {
8 struct s x;
9 if(t>0)
10 {
11 x.a=2;
12 x.b=3;
13 }
14 max(x.a, x.b)
15 }
例18中,当if条件不成立时,结构体x中的成员变量将没有值就被作为参数传递给max()函数。
16.读取结构体中未赋值的局部变量
程序中存在结构体的局部变量,在没有赋值的情况下,就读取该成员的值或将该成员作为参数传递给某个函数。
例19:
1 struct s
2 {
3 int a;
4 int b;
5 };
6 main()
7 {
8 struct s x;
9 x.b=0;
10 max(x.a,1);
11 }
例19中第10行语句,结构体x中的成员变量a还没有赋值,就作为参数传递给函数max()。
17.条件判断语句导致类成员未初始化
程序中的条件判断语句,可能导致程序中类的构造函数没有给类域内的成员进行初始化。
例20:
1 class c
2 {
3 private: int i;
4 int j;
5 bool flag;
6 public: c()
7 {
8 if(flag)
9 {
10 i=0;
11 j=1;
12 }
13 }
14 };
例20中,当flag变量取值为真时,类c构造函数中i和j才被赋值;当flag取值为假时,将无法在类c构造函数中给i和j赋值。
18.没有使用构造函数初始化类成员
类中所有的成员变量应该在类构造函数中初始化。
例21:
1 class c
2 {
3 private: int i;
4 int j;
5 bool flag;
6 public: c()
7 {
8 i=0;
9 j=1;
10 }
11 };
例21中,类C的构造函数中缺少对成员变量flag的赋值语句。
19.引用被初始化为地址可变的对象
引用总是指向一个对象,引用只能在定义时被初始化一次,之后不可改变。引用和指针之间的区别如下:
① 指针指向一块内存,它的内容是所指内存的地址,而引用是某块内存的别名。指针是一个实体,而引用仅是一个别名。
② 引用使用时无需解引用(dereference,*ptr,ptr为指针),而指针需要解引用。
③ 引用只能在定义时被初始化一次,之后不可改变,而指针可改变。
④ 引用没有const类型,而指针有const类型,且const类型的指针不可改变。
⑤ 引用不能为空,而指针可以为空。
例22:
1 void tmp(int* b)
2 {
3 if (b==NULL)
4 {
5 b=new int;
6 *b=200;
7 }
8 else
9 {
10 *b=100;
11 }
12 }
13 main()
14 {
15 int a=0;
16 tmp(&a);
17 int* c=NULL;
18 tmp(&c); // 指针可以为空,但引用不能为空。
19 return;
20 }
例22的第18行语句中,变量c是一个空指针,因为引用不能为空,所以“&c”不正确,正确的语句是“tmp(c)”。
20.静态成员未初始化
类中的静态数据成员是所有类对象共享的内容,其存放的是所有对象的值,而不是某个对象的值。
静态数据成员的初始化在类体外进行,初始化时不用加访问权限符,因静态数据成员是类的数据成员,所以在初始化时应指出其类名。
例23:
1
2 class T
3 {
4 public:
5 T (int a,int b);
6 bb( );
7 private:
8 int x,y;
9 static int s;
10 };
11 T::T (int a,int b)//构造函数的实现部分;
12 {
13 x=a;
14 y=b;
15 }
16 void T::bb( )//成员函数的实现部分;
17 {
18 s=s+x+y;
19 cout<<"s="<<S<
20 }
21 main( )
22 {
23 T t1(10,20), t2(5,3);
24 t1.bb( );
t2.bb( );
25 }
例23中,T类中有一个静态数据成员s,该数据成员应该在类体外被定义。
修改方法:在第11行语句之前,添加为静态数据成员s赋初值的语句:“int T::s=0”。
21.赋值运算符(operator=)未给所有变量赋值
程序中虽然使用赋值运算符(operator=),但没有给所有变量赋值。
例24:
1 class A
2 {
3 private:
4 int _x, _y, _z;
5 public:
6 A( ) { }
7 A& operator=( const A& a )
8 {
9 _x = a._x;
10 _y = a._y;
11 return *this;
12 }
13 };
例24中,第7行语句使用“operator=”本意是给所有的成员进行赋值,但事实上仅仅对类A中的成员变量“_x、_y”进行了赋值,没有对类A中所有的成员变量进行赋值。
修改方法:在第10行语句之后添加语句:“_z = a._z”。
22.工程头文件中包含变量的定义
头文件中最好只做变量的声明,不做变量的定义。因为工程中的头文件会被.c或.cpp文件多次包含,如果头文件中包含变量的定义,势必造成变量的重复定义,所以变量的定义应该写在.c或.cpp文件内。
例25:
头文件 header.h
int t=2;
例25中的头文件header.h包含了变量t的初始化,头文件header.h如果多次被源码(.c或.cpp)文件包含,编译时会造成变量的重复定义。
修改方法:将int t=2语句修改为:“int t;”。
23.无符号整数初始化为负数
无符号整数不能识别负数,所以初始化无符号整数为负数是错误的。
例26:
1 F()
2 {
3 unsigned int y = -21;
4 }
例26中第3行语句,无符号整数y被错误赋值为负数“−21”。
修改方法:将有符号整数赋值为负数:“signed int y = −21”。
一般情况下,有符号整数和无符号整数均在内存中占用16位,但这两种整数类型的取值范围不同,有符号整数取值范围是“−32768到32767”,无符号整数取值范围是“0到65535”。
例27:
1 BOOL fun(size_t cbSize)
2 {
3 if(cbSize > 1024)
4 rerurn FALSE;
5 char *pBuf = new char[cbSize -1];
//未对new的返回值进行检查
6 memset(pBuf,0x90,cbSize -1);
7 …
8 return TRUE;
9 }
例27中,第5行语句调用new分配内存后,未对调用结果的正确性进行检测。如果cbSize为0,则“cbSize -1”为−1,但是memset( )函数中第3个参数本身是无符号数,因此会将−1视为正的0xffffffff,导致执行此函数时程序崩溃。
24.变量未完全初始化
为了避免使用没有初始化的变量,程序中所有的变量都应该初始化。
如果不初始化声明的整数变量,将得到一个随机值;如果不初始化声明的静态变量,编译器会自动初始化为0;如果不初始化声明的指针变量,那么它所指向的内容是无法确定的,这种情况下使用该指针变量时将产生不可预料的后果。
例28:
1 f()
2 {
3 int *t;
4 …
5 }
例28中,f()函数中仅仅声明了一个指向整数的指针t,却没有对t进行初始化,指针变量t指向的地址将是不确定的,如果其他程序调用f()函数,使用没有初始化的指针变量t,可能产生不可预料的错误。
修改方法:将“int *t;”修改为:“int *t=1;”。
25.显式调用未定义的构造函数
如果程序员自定义的类中没有构造函数,编译器就会为该类产生一个默认的构造函数,用于创建类对象时调用,但这个编译器产生的构造函数是无参函数,函数体为空,不做任何操作。为了给类中各成员变量赋初值,应该在创建的类中自定义构造函数。
例29:
1 class A
2 {
3 public:
4 int b;
5 int a;
6 };
7 A* f()
8 {
9 A* a=new A();
10 return new A();
11 }
例29中,类A中并没有定义自己的构造函数,而在f()函数中调用“A()”。
修改方法:在类A的定义中添加对构造函数“A(){ int b=1; int a=2;}”的声明。
26.通过赋值方式初始化类的常量成员
调用类的构造函数时,构造函数将参数值传给类中相应的数据成员。当构造函数的参数值是常量(const)时,常量参数值只能通过初始化方式传给类中相应的数据成员。
例30:
1 class A {
2 public:
3 A( const char* file, const char* path )
4 {
5 myFile = file;
6 myPath = path;
7 }
8 private:
9 string myFile;
10 string myPath;
11 };
例30中的第5行语句和第6行语句,因为类A的构造函数A()中两个参数值都是常量,所以应该通过初始化方式而非赋值方式给类中成员传值。修改后的程序如例31所示。
例31:
1 class A {
2 public:
3 A( const char* file, const char* path ) :
myFile(file), myPath(path) {};
4 private:
5 string myFile;
6 string myPath;
7 };
例32:
1 class student
2 {
3 public:
4 student();
5 protected:
6 const int a;
7 const int &b;
8 };
9 student::student(int i, int j)
10 {
11 a=i;
12 b=j;
13 }
例32中第9行语句,类中常量成员初始化操作没有在初始化表里完成。
修改方法:将第9行语句改为:“student::student(int i, int j):a(i),b(j){}”。
27.构造函数内成员变量初始化顺序错误
类中构造函数按照成员在类中的声明顺序执行初始化操作,如果忽略了这一点,将导致初始化错误。
例33:
1 class X
2 {
3 public:
4 X( int y );
5 private:
6 int i;
7 int j;
8 };
9 inline X::X( int y ) : j( y ), i( j ) {} ;
例33中,构造函数类X中成员变量的初始化顺序是先i再j,第9行语句初始化列表中将成员变量i初始化为值j,而此时成员变量j还没有被初始化,所以第9行语句初始化列表错误。
修改方法:将第9行语句改为:“inline X::X( int y int x ) : j( y ), i( x) {}”。
28.构造函数内变量初始化和声明的顺序不一致
C++中,构造函数有一种特殊的初始化方式——“初始化列表”,构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化值。如果类存在继承关系,派生类必须在其初始化列表里调用其基类的构造函数。
初始化时,编译器会记录成员变量在初始化列表中的初始化顺序,以保证析构函数对成员变量的释放顺序正确。
类的构造函数按照成员在类中的声明顺序执行初始化操作,因此如果类中构造函数的初始化列表和成员变量的声明顺序不一致,势必造成析构函数释放成员变量的顺序错误。
例34:
1 class A
2 {
3 public:
4lt@span i=1> lt@span i=1> lt@span i=1> lt@span i=1> lt@span i=1> lt@span i=1> A(lt@span i=1> intlt@span i=1> xlt@span i=1> )lt@span i=1> :lt@span i=1> a(lt@span i=1> xlt@span i=1> ),lt@span i=1> b(lt@span i=1> alt@span i=1> ) {};
5 private:
6 int b;
7 int a;
8 };
9 class B : public A
10 {
11 public:
12 B( int );
13 private:
14 int t;
15 float u;
16 };
17 B::B( int y ) : u( y ), A( 1 ), t( u ) { };
例34中,A类的初始化列表(第4行语句)中成员变量的初始化顺序是先a成员再b成员,而A类中成员变量声明的顺序是先b成员再a成员,两者初始化顺序不一致,第17句中也存在同样的错误。
修改方法:将第4行语句改为:“A( int x ) : b( x ), a (b)”,第17行语句改为:“B::B( int y ) : t(y), A( 1 ), u( t ) { }”。
29.同一外部变量被多次定义
工程里同一外部变量在不同源文件中被重复定义时,不同的编译器对这种情况可能有不同的处理方式。为了避免外部变量被重复定义,应保证程序中同一外部变量只能定义一次。
例35:
在sourc1.c中定义int a=1;
在sourc2.c中声明 extern int a;//a是一个外部变量
在sourc3.c中定义int a=0;
当把sourc1、sourc2和sourc3源码同时编译链接在一个工程里时,此时源文件sourc2.c中引用的外部变量a有两个值(在sourc1和sourc3中均有定义)。
30.同名外部变量类型不一致
同一个外部变量在两个不同的源文件中被声明为不同的类型,在这种情况下,当程序运行时,存在很多可能的情况,包括程序崩溃等。
例36:
在sourc1.c中声明extern int a;
在sourc2.c中定义long a;
例36中,在sourc1.c中声明引用int类型的外部变量a,而在sourc2.c中定义变量a的类型为long型,同名变量a的类型不一致。
2.1.3 函数返回值问题
C/C++是由变量或者函数这些外部对象构成的。函数是一个能完成一定功能的执行代码段,一般使用return语句由被调函数向调用函数返回值,如果函数的返回值处理错误,将会导致函数功能不能正确实现。
31.无参函数调用错误
函数调用时,即使是调用无参函数,也应该包括参数列表。
例37:
1 #include <stdlib.h>
2 F(); //无参函数
3 main()
4 {
5 int t;
6 scanf("%d",&t);
7 if (t>=0)
8 F;
9 }
例37中,第7、8行语句本意是,如果if条件成立,调用无参函数F(),而第8行语句函数名F后未添参数列表,所以该行语句不做任何操作。
32.函数返回值类型和函数声明返回值类型不一致
函数返回值类型不一致,包括函数分支返回值类型不一致和由于函数调用造成返回值不一致。其中,函数分支返回值类型不一致是指函数如果有返回值,函数的每个分支都应该有返回值;函数如果没有返回值,函数的每个分支都应该无返回值。函数分支返回值类型不一致见例38和39。函数调用造成返回值不一致,是指程序中主函数确定了返回值类型,而程序中调用函数返回值类型和主函数的返回值类型不一致,导致程序中返回值类型不一致,见例40。
例38:
1 int f(int t)
2 {
3 return t;
4 }
5 void main()
6 {
7 int i;
8 i=4;
9 f(4);
10 }
例38中,main()函数是没有返回值的,但在第9行语句调用函数f()产生了返回值,造成程序中返回值不一致。
例39:
1 void f(int t)
2 {
3 }
4 int main()
5 {
6 int i;
7 i=4;
8 f(4);
9 }
例39中,main()函数有int类型的返回值,但其调用了没有返回值的函数f(),导致程序中返回值不一致。
例40:
1 #include <stdio.h>
2 char g(int t)
3 {
4 char s;
5 scanf("%c",&s);
6 return s;
7 }
8 int main()
9 {
10 int i;
11 i=4;
12 g(4);
13 }
例40中,main函数显式返回int类型值,但在main函数内部却调用了显式返回char类型值的g函数,导致程序返回值类型不一致。
33.分支返回值的类型不一致
程序中的某个函数含有一组分支语句,其中某些分支不提供返回值,某些分支却提供返回值,而该函数隐式返回类型为int类型的值,这种缺陷可能引起潜在的致命错误。
例41:
1 #include <math.h>
2 #include <stdio.h>
3 f (int i)
4 {
5 return i;
6 }
7 main ()
8 {
9 int j;
10 scanf("%d",&j);
11 if(j)
12 {
13 …
14 f(j);
15 }
16 }
例41中,第11行if语句条件成立时,main()函数将返回一个int类型的值;当if语句条件不成立时,main()函数将不返回任何值,导致程序中分支返回值不一致。
34.无返回值的函数产生返回值
例42:
1 void t()
2 {
3 int t;
4 return t;
5 }
例42中,第1行语句void表明t()是一个没有返回值的函数,但第4行语句存在返回int类型的语句,导致错误发生。
35.有返回值的函数未产生返回值
程序中有明确的返回语句,但程序中调用的函数却没有返回值,例如,一个有返回值的主程序调用无返回值的函数。
例43:
1 void g(int t)
2 {
3 }
4 int main()
5 {
6 int i;
7 i=4;
8 g(4);
9 }
例43中,main()函数有int类型的返回值,但调用了没有返回值的函数g,导致程序中返回值不一致。
2.1.4 其他
36.switch语句变量类型不一致
switch语句可以产生具有多个分支的流程控制语句,使用时需要注意三种错误:switch语句中变量类型不一致性、switch语句错误省略了break语句和switch语句中case条件后没有可执行的语句。
当多个枚举类型的值被混用在switch分支表达式或switch分支条件语句中时,容易造成case条件变量的值和switch语句中变量类型不一致。
例44:
1 void choice ()
2 {
3 enum Q1{Q1Send, Q1Recv}; //枚举类型Q1
4 enum Q2{Q2None, Q2Send, Q2Recv};//枚举类型Q2
5 enum Q1 q;
6 switch (q)
7 {
8 case Q2Send: f(); break;
9 case Q2Recv: g(); break;
10 case Q1Send: f(); break;
11 case Q2None: g(); break;
12 }
13 }
例44中,Q1、Q2都是枚举类型,q的取值范围是枚举类型Q1的值,但在程序第8、9、11行语句中,q的取值是枚举类型Q2的值,导致程序错误。
37.switch语句中遗漏break语句
switch语句中的控制流程能够通过条件判断执行正确的case部分,执行结束后跳出switch语句。在switch语句中遗漏break语句时,程序会执行满足条件的case语句及其以后的case语句,直至整个switch语句结束。因此,在使用switch语句时,不能遗漏break语句。
例45:
1 main()
2 {
3 enum date {Monday, Sunday, Thursday, weekday} ;
4 date q;
5 switch (q)
6 {
7 case Monday: printf("monday");
8 case Sunday: printf("Sunday");
9 case Thursday: printf("Thursday");
10 case weekday: printf("weekday");
11 }
12 }
例45中,当q取值为Sunday时,因为在case语句中遗漏了break语句,程序输出错误的结果:SundayThursdayweekday。
38.switch语句中case条件后没有可执行的语句
switch语句中当变量满足某种case条件,却没有可执行的语句时,应该在switch语句中略去该case语句。
例46:
1 int g(int i)
2 {
3 switch(i)
4 {
5 case 0: return 1;
6 case 1:
7 case 2:
8 default: return 0;
9 }
10 }
例46的g函数中,当变量i值为1或2时,不执行任何语句。
修改方法:在switch语句中略去i值为1或2时的case分支,修改后的程序如例47所示。
例47:
1 int g (int i)
2 {
3 switch (i)
4 {
5 case 0:
6 return 1;
7 default:
8 return 0;
9 }
10 }
39.switch各case语句返回值类型不一致
例48:
1 int System_comm( int GetLastCode)
2 {
3 switch (Communication)
4 {
5 case Nmc_Cfg_Add:
6 return (-1);
7 break;
8 case Nmc_Cfg_Mod:
9 return (1U);
10 break;
11 case Nmc_Cfg_Del:
12 return (1L);
13 break;
14 …
15 default:
16 break;
17 }
18 }
例48中,case的返回值类型不一致。
修改方法:进行一致性检查,避免返回值类型不一致。
40.switch语句中使用布尔表达式
例49:
1 void UserCfg (void)
2 {
3 BOOL cUserName = FALSE;
4 …
5 switch (cUserName)
6 {
7 case TRUE:
8 …
9 break;
10 case FALSE:
11 …
12 break;
13 default:
14 …
15 break;
16 }
17 }
case语句在多条件语句情况下使用,例49的程序中只有TRUE和FALSE两种情况,在这种非真即假的条件下,default语句变成不可达代码。
修改方法:使用if和else条件语句。
41.switch语句中含有定义以外的case取值或case取值列举不完全
swith语句与枚举类型列举的变量不匹配会引起程序错误。
例50:
1 typedef enum Communication_ways
2 {
3 wire,
4 wireless,
5 satelite,
6 } Commu_ways;
7
8 Communication_ways Commu_way;
9 int method;
10 …
11 void Ex_profession(void)
12 {
13 switch (Commu_way)
14 {
15 case wire:
16 method = 1;
17 break;
18 case wireless:
19 method = 2;
20 break;
21 case other:
22 method = 4;
23 break;
24 default:
25 break;
26 }
27 }
例50中,程序遗漏了case satelite的情况,并且还加入了case other语句。
修改方法:增加原有枚举类型的变量,去除不存在的变量。
42.case语句中包含另外一个case语句
switch函数的case语句和default语句必须在同一层次。
例51:
1 void reason(void)
2 {
3 switch (chooser)
4 {
5 case first:
6 a1 = b1;
7 break;
8 case second:
9 case third:
10 a1 = c1;
11 if (a1 == b1)
12 {
13 case fourth:
14 …
15 }
16 break;
17 default:
18 errorflag = 1;
19 break;
20 }
21 }
例51中,第13行语句把case fourth包含在case third下,会引起程序错误。
修改方法:把case fourth提取出来与其他case语句并列。
43.使用多余的for循环表达式
for循环表达中,只应该完成与循环条件有关的操作。
例52:
1 void Ex_profession(void)
2 {
3 Uint_32 loop;
4 Uint_32 myVar = 0;
5 const Uint_32 max = 10U;
6 …
7 for (loop = 0, myVar = 1U; loop < max; loop++)
8 {
9 …
10 }
11 }
例52中第7行语句,for循环表达式包含对变量myvar的赋值操作,虽然对程序运行结果没有影响,但使程序可读性差。
44.循环中含有多出口
例53:
1 for(int i = 0;i<num;i++)
2 {
3 l_pRecordset=m_Ado.ExecuteQuery(cSql,iCount);
4 if(l_pRecordset!=NULL)
5 {
6 …
7 if(vtTemp.vt != VT_NULL)
8 {
9 …
10 break;
11 }
12 …
13 }
例53中,第10行语句在循环中插入了“break”,导致该循环存在多出口,不符合结构化编程要求。
修改方法:把该行单独提取出来进行操作。
45.枚举成员初始化不标准
例54:
1 public enum Test1{x,y,z};
2 public enum Test2{x=1,y,z};
3 public enum Test3{x=2,y=3,z=4};
4 public enum Test4{x,y,z=1};
可以不对枚举成员进行初始化,此时枚举成员使用其默认值:第一个枚举成员的默认值为0,其后的枚举成员值依次加1,因此对于第1行语句,x、y、z的值分别为0、1、2。
也可以对枚举成员进行明确的初始化,但只有两种初始化方式是安全的,一种是初始化全部成员,另一种是只初始化第一个成员。在例54中,第1、2、3行语句是安全的,而第4行语句是不安全的。
46.整型数字和字符间赋值错误
内存中字符数据以ASCII码存储,即以整数表示。如果把字符型的值赋给整数类型的变量,那么该整数变量的值就是该字符的ASCII码。例如,将字符值赋值给整型变量int a='b',则a=98。当字符型数据赋给整型变量时,由于字符只占一个字节,而整型变量占两个字节,因此将字符数据的前八位放到整型变量的低八位中;如果把整数赋给字符变量,那么取该整数值的低八位部分所对应的ASCII码字符赋值给字符型变量。
例55:
1 main()
2 {
3 char a = 256 ;
4 int d = a ;
5 cout<<d<<endl ;
6 }
例55中,第3行语句把整数256赋值给字符a后,再把字符a的值赋值给整数d,整数d的值是0,而不是256。
47.头文件中含有多个类
一个头文件只能声明一个类。
例56:
1 #ifndef STATIC_258_H
2 #define STATIC_258_H
3 typedef unsigned int Uint_32;
4 typedef unsigned short Uint_16;
5
6 class Person
7 {
8 public:
9 Person();
10 explicit Person(const Uint_32 personNum);
11 explicit Person(const Person &person);
12 Person & operator=(const Person &person);
13 ~Person();
14 protected:
15 private:
16 Uint_32 personalNumber;
17 };
18
19 class MalePerson : public Person
20 {
21 public:
22 MalePerson();
23 explicit MalePerson(const Uint_32 personNum);
24 MalePerson(const Uint_32 personNum,
25 const Uint_16 weight);
26 explicit MalePerson(const MalePerson&mperson);
27 MalePerson&operator=(const MalePerson&mperson);
28 ~MalePerson();
29 protected:
30 private:
31 Uint_16 weightAmount;
32 };
33 #endif
例56中,在同一个头文件中声明了多个类。
修改方法:把多个类分开在不同的头文件中声明。
48.头文件名称错误
例57:
1 #include <\ptype.h>
C语言规定,源代码的头文件名中不能有“'、\、/、*”等特殊字符。
修改方法:去掉头文件中的特殊字符。
49.同一区域内出现同名变量
例58:
1 void CBaseProc (int ChooseNumber)
2 {
3 int DefaultValue;
4 BOOL GetOidMib = FALSE;
5 …
6 if (GetOidMib)
7 {
8 int DefaultValue = 126;
9 DefaultValue = DefaultValue + 1;
10 }
11 DefaultValue = ChooseNumber;
12 …
13 }
C语言允许在不同区域使用重命名的变量,例如在程序块和命名空间。但在例58中,在程序块以及更深一级嵌套使用相同变量,导致程序无法分辨当前变量使用的是哪一个变量。
50.实参和形参不一致
例59:
1 void MISUNIT( UINT_16 p_1, UINT_16 p_2 )
2 {
3 …
4 }
5 void static_collection(UINT_32 p_1, UINT_16 p_2)
6 {
7 MISUNIT( p_1, p_2);
8 …
9 }
例59中,MISUNIT( )函数的形参和实参前后不一致,会使程序出错。
51.断言的布尔表达式值为假
在编程过程中,程序员常常会预先做一些假定,然后使用断言来对这些假定进行检查。断言被当作异常处理的一种高级形式,用于创建更稳定、更高质量且易于排查错误的代码。在C语言中使用assert()函数来实现断言,函数原型:
void assert(bool expression);
assert()计算表达式expression,如果值为假,那么它先向stderr打印一条出错信息,然后通过调用abort来终止程序运行。
断言常被用来调试程序,但如果程序在发布后未关闭断言且存在断言表达式为假的情况,则被视为不可控异常。
例60:
1 #define CONSTANT 100
2
3 void asrt_test()
4 {
5 int n,m;
6 …
7 scanf("%d", &n);
8 if(n<100)
9 n=100;
10 assert(n>=100);
11 m=sqrt(n);
12 assert(m>10);
13 assert(m>10);
14 …
15 }
例60中,由于有第8、9行语句,使第一个断言的表达式恒为真。但是第12行语句中的断言表达式就存在m=10的情况使断言为假,从而使程序终止运行。但如果程序运行到第13行语句,断言是不会有问题的,因为该断言的表达式已经在第12行通过判断。
52.数值运算导致上溢出/下溢出
程序员在编程时会涉及频繁的数值计算,由于计算机(编译器)中的每种数据类型都有相应的表示范围,如果在进行数值计算时没有考虑到这一点,就可能造成数值溢出。如果数值大于该类型数据的最大值,称为上溢出(Overflow);如果数值小于该类型数据的最小值,称为下溢出(Underflow)。
32位机器中几种主要数值类型的表示范围如表2-1所示。
表2-1 32位机器中几种主要数值类型的表示范围
例61:
1 void Flow_Sub(char *tmpBuf,…)
//tmpBuf从外部输入得到
2 {
3 int tmp;
4 int indNum;
5 float f1,f2;
6 …
7 tmp = tmpBuf[4]-1;
8 …
9 indNum = tmpBuf[5];
10 for(int n = 0; n < indNum; n++)
11 {
12 …
13 }
14 …
15 f1 = 3.40282347e+38f;
16 f2 = (float) 3.40282347e+38;
17 …
18 }
例61中,由于tmpBuf值从外部输入获得,其值无法确定,因此在第7行语句中直接使用可能产生整数的下溢出,而在第10行语句的“n++”操作时可能产生上溢出。浮点数的最大值(即常量MAXFLOAT)用十进制表示应该为:340282346638528859811704183484516925440.0。在第15行和第16行语句使用两种不同的方式将一个常数换成浮点数。第16行语句使用强制转换,编译器在执行此操作时分作两步:第一步将常数转换为double类型并存为一个临时变量,第二步再将临时变量转换为float类型。由于常量转换为double类型是取符合精度要求的最小较大值,而常量转换为float类型是取符合精度要求的最大较小值,因此第16行语句的转换会产生一个浮点数上溢出,而第15行语句则不会产生溢出。
53.循环结束条件设置错误导致无限循环
当for、do-while、while等循环体的循环结束条件永远不会为真时,循环进入无限循环。
例62:
1 int main()
2 {
3 int n = 5;
4 do
5 {
6 printf("Now the value of n is :%d\n",n--);
7 n--;
8 }while(n!=0);
9 return 0;
10 }
例62中,由于循环体中n每次循环减去2,因此n永远不会为0,do-while进入无限循环。
54.忽略while和do-while的区别
C/C++语言编程中会用到大量循环结构,使用循环结构可以在很大程度上简化程序设计。while语句和do-while语句就是两种很常见的循环结构,大多数情况下二者实现结果一致,但二者在细节上有所区别。
例63:
1 void Func1(int arr[100])
2 {
3 int i;
4 int sum = 0;
5 scanf(%d, &i);
6 if(i >= 0)
7 {
8 while(i < 100)
9 {
10 sum = sum + arr[i];
11 i++;
12 }
13 printf(%d, sum);
14 }
15 }
16
17 void Func2(int arr[100])
18 {
19 int i;
20 int sum = 0;
21 scanf(%d, &i);
22 if(i >= 0)
23 {
24 do
25 {
26 sum = sum + arr[i];
27 i++;
28 } while(i < 100);
29 printf(%d, sum);
30 }
31 }
例63中,分别使用Func1()和Func2()实现以下功能:求数组arr[]的某个元素开始的所有元素之和并打印。当0≤i<100时,两个函数结果相同。由于while循环语句是先判断后执行,而do-while语句是先执行后判断,当i<0或者i≥100时,Func1()中while循环条件为假而不执行循环体,Func2()中do-while会执行一次循环体再进行判断,从而导致数组索引越界错误。