第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会执行一次循环体再进行判断,从而导致数组索引越界错误。