从缺陷中学习C/C++(txt+pdf+epub+mobi电子书下载)


发布时间:2020-10-24 06:27:27

点击下载

作者:刘新浙,刘玲,王超,李敬娜等

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

从缺陷中学习C/C++

从缺陷中学习C/C++试读:

前言

这是一本在研究大量C/C++程序Bug基础上集结而成的书!

这是一本汇集众多一线C/C++编程人员智慧的书!

这是一本让您学好C/C++,绕过编程陷阱和障碍的必备案头书!为什么写这样一本书

▪在不同的项目或产品中,不同的开发人员重复着同样的Bug,甚至同一个人重复相同的Bug。如果将时间周期拉得更长一些看:一个程序员,从刚毕业参加工作到具备丰富编程经验,从一个新手到成为专家,在这个过程中,每个人都在重复着前人走过的弯路,重复着同样的编程错误。测试人员在日常工作中积累了大量验证Bug方面的经验,这些Bug是有价值的,总结出来可以让更多人受益。

▪C/C++是软件/互联网行业最常用的编程语言之一,相对其他语言学习难度高,从新手到专家往往需要多年的磨练。另一方面,C/C++开发的系统往往更容易产生严重的生产事故,一旦出现事故,定位问题根源也比较困难。所以,每一个程序员掌握扎实的C/C++基础知识,对于构建稳定可靠的生产系统非常重要。我们希望通过这本书帮助C/C++程序员以最快速度全面了解C/C++编程中的陷阱,编写健壮可靠的代码,从而达到提高软件质量、减少生产故障、提高工作效率的目的。

相对其他C/C++类书籍,本书有以下特点和优势:

▪从具体Bug中学习

全书由102个案例组成,每个案例分析一个Bug。读者掌握了一个案例就是掌握了一个知识点,就能避免一类问题。由于是从具体Bug案例中学习,这种学习方式更直接,更让人印象深刻。普通的C/C++编程书,即便看完后,写代码时也无法避免Bug,这是因为,书虽然看完了,知识也理解了,但你并不知道哪里有陷阱。

▪来源于工程实战的宝贵经验

本书中的所有案例都来自于软件/互联网行业开发生产中遇到的实际问题,都是在前人的错误和弯路中总结出来的实战经验,堪称C/C++编程方面的“干货”。

▪学习起来更有成就感

本书由一个个案例组成,在每个案例中,先给出错误代码示例,然后描述Bug的现象和后果,随后对该Bug进行具体分析,最后给出解决方案及建议。这种案例分析式的组织方式,会引导读者先对案例中提出的问题进行思考,当看到案例分析和解决方案时常常有恍然大悟的感觉,让学习过程变得简单,并充满乐趣。

▪更宽的知识面

一个C/C++程序员即使工作多年,由于受所接触项目和技术方向的限制,视野(C/C++编程中值得注意的知识点)往往是有限的。这本书中的案例收集于大量工程项目,几乎涵盖了C/C++编程中的方方面面,远超出一般程序员所能接触的范围。掌握了这本书中的内容,能避免大多数问题。本书适用范围

▪本书适合已经写过一些C/C++代码、期望尽快积累实战经验的C/C++程序员阅读学习。本书也适合打算提高代码编写和代码阅读分析能力的软件测试人员,书中的每个案例都可应用于白盒测试中。本书代码运行环境

▪本书中的所有案例代码都是针对Linux C/C++环境,在Redhat Linux环境下编译(GCC,G++)测试通过。代码可以通过人民邮电出版社网站(www.ptpress.com.cn)下载。

最后,由于本书作者在C/C++编程方面的经验和技能有限,书中可能会有一些描述不够清晰或不正确的地方,读者如有发现请及时告诉我们,我们会再进行修正。我们的联系邮箱是cppbugbook@gmail.com,编辑联系邮箱是zhangtao@ptpress.com.cn。在此表示感谢!

期望这本书能真真切切地帮助到他人。编者第1章 基础问题

本章从基础问题讲起,涉及运算符优先级、宏定义、类型转换(显式和隐式)、指针、数组等C/C++编程的一些基本概念和知识点。这些问题虽然简单,但如果不引起重视,也会给编程带来很多麻烦。1.1 运算符优先级引起的问题

▪代码示例

//To get 2*n+1

int func ( int n )

{

  return n << 1 + 1;

}

▪现象&后果

上述代码中的函数func本意是期望计算2*n+1,但程序实际运行结果是4*n。

▪Bug分析

这段代码使用左移1位来代替乘以2的运算,是很好的方法,但是编程者弄错了运算符“<<”和“+”的优先级。C/C++语言规定运算符“+”的优先级高于运算符“<<”,因此,上述语句“return n<<1+1”等同于“return n<<(1+1)”,所以,会先进行加法运算,再进行左移运算,得到结果4*n。

修正的方法是在表达式中添加必要的括号。

▪正确代码

//To get 2*n+1

int func ( int n )

{

  return (n << 1) + 1;

}1.2 不加括号的宏定义引起的错误

▪代码示例

#define PERIMETER(X,Y) 2*X+2*Y

int main(void)

{

  int lenth = 5;

  int width = 2;

  int high = 8;

  int result = 0;

  result = PERIMETER(lenth, width) * high;

  printf("result=%d \n", result);

}

▪现象&后果

上述代码是期望通过一个宏先计算一个矩形的周长,然后再乘以高,结果应该为112,但实际计算结果为42,与预期不符。

▪Bug分析

上述代码中,语句“result = PERIMETER(lenth, width) * high”本意是希望先进行宏定义部分的运算,然后再乘以变量high,但是,由于宏替换是在预编译阶段进行的,宏替换本身只是文本替换,上述语句在宏替换后变成了“result = 2 * lenth + 2* width * high”,按照运算符优先级规则会先做乘法运算再做加法运算,故导致运算结果不符合预期。因此,用于表达式的宏,在宏定义时给整体语句加上小括号是比较保险的做法。

▪正确代码

#define PERIMETER(X,Y) (2*(X)+2*(Y))

int main( )

{

  int lenth = 5;

  int width = 2;

  int high = 8;

  int result = 0;

  result = PERIMETER(lenth, width) * high;

  printf("result=%d \n", result);

}

▪编程建议

由于宏替换可能会带来不可预期的结果,因此,使用宏时要非常谨慎。在C++中尽可能少使用宏,改用其他方式来代替。下面是几种常见的替代场景:程序中频繁被调用的代码段或者函数,期望使用宏替换来节省执行时间的,可以用内联函数代替;期望使用宏来存储常量的,可以用 const 变量代替;期望使用宏来“缩短”长变量名的,可以用引用来代替。1.3 污染环境的宏定义

▪代码示例

在文件a.cpp 中:

#define map __gnu_cxx::hash_map

map myhashmap;

在文件b.cpp 中:

#include

#include "a.cpp"

map mymap;

▪现象&后果

上述代码假设由两个人所写。a.cpp为程序员A所写,为了书写方便,A将库函数gnu_cxx::hash_map宏定义为map。b.cpp为程序员B所写,B在b.cpp中包含了库map,同时在其后又包含了a.cpp。这最终导致B使用的map并非其预期的系统库map中的定义,而是gnu_cxx::hash_map。

▪Bug分析

由于宏替换是在预编译阶段执行的,所以在 b.cpp 中,代码行 map mymap在预编译阶段会被宏替换为__gnu_cxx::hash_map mymap,虽然b.cpp中包含了#include ,在编译阶段编译器只会编译宏替换之后的代码,程序员B想要的map在预编译之后,在代码中已经找不到了。

这段代码虽然编译可以通过,也可以正常运行,但是用错了 map 和 hash_map可能会引发不可预期的后果(如性能差别、功能扩展的区别),埋下了程序稳定性差的隐患。

▪正确代码

修改文件a.cpp中的宏名字为hmap:

#define hmap __gnu_cxx::hash_map

hmap myhashmap;

▪编程建议

给宏定义命名时,要避免宏名称与系统库的定义同名。1.4 多语句宏定义使用错误

▪代码示例

#define EXIT(info) std::cerr<

int main( )

{

  int data = 0;

  if(data < 0)

   EXIT("data is a negative number!");

  std::cerr << "data is a non-negative number." << std::endl;

}

▪现象&后果

这段代码定义了一个EXIT宏,当data为负数时,打印“data is a negative number”信息然后程序退出;当data大于等于0时,打印“non-negative”,然后程序结束。但是,实际运行结果是,当 data为非负时,程序退出但没有打印“data is a non-negative number”。

▪Bug分析

上述代码中的if语句,在EXIT宏展开后如下:

if(data<0)

std::cerr<<"data is a negative number!"<

正确地缩进以后,代码变成:

if(data<0)

  std::cerr<<"data is a negative number!"<

exit(1);

显然exit语句不在if分支语句块中,所以,不管data的值是否为负数,exit语句都会被执行。这和原意不符。

就上述问题而言,一种简单的解决办法是用大括号将宏定义的内容括起来:

#define EXIT(info) {std::cerr<

这种方法可以解决上述代码中的问题,但是在如下场景中依然有问题:

Int fuction(int data)

{

If(data < 0)

  EXIT("data is a negative number!");

else

  cout << data << endl;

}

在宏替换之后变成:

int fuction(int data)

{

if(data < 0)

{

std::cerr<

exit(1);

};

else

  cout << data << endl;

}

这时多余的分号会出现语法错误。

所以推荐的另外一种修正做法是使用内联函数来替换宏定义,具体见下面的正确代码。

▪正确代码

inline void EXIT (const char info[])

{

  std::cerr << info << std::endl;

 exit(1);

}

int main( )

{

  int data = 0;

  if(data < 0)

   EXIT("data is a negative number!");

  std::cerr << "data is a non-negative number." << std::endl;

}1.5 char转为int时高位符号扩展的问题

▪代码示例

int main( )

{

  char a = 0x9A;

  int util;

  util = (int)a;

  if( util > 0 )

   printf ("positive\n");

 else

   printf ("negative\n");

}

▪现象&后果

上述代码期望最后输出“positive”,但实际输出结果为“negative”。

▪Bug分析

上述代码中,char变量a的值为0x9A,这个值有迷惑性。0x9A转换为十进制为154,所以,在把a强制转换为int类型的变量util之后以为还是154,所以,期望输出为“positive”。但实际上0x9A的二进制表示为10011010,在强制转换为int时,因为int是有符号的,需要对10011010进行符号扩展,也就是用其最高位1来扩充其他 3个高字节,变成 11111111 11111111 11111111 10011010(假设 int是4个字节),而这个是负数-102的二进制补码表示。所以,在判断util是否小于0时就会输出“negative”。

从另外一个角度来说,变量a的类型为char,一般系统中char为有符号的,0x9A解析为有符号的 char 时,其值实际上也为-102。所以,0x9A 在强制变换前后是保持一致的。

但如果期望 0x9A为正数,实际上需要先把 a强制转换为 unsigned char。这样0x9A才会被解析为154。

▪正确代码

int main( )

{

  char a = 0x9A;

  int util;

  util = (int)(unsigned char)a;

  if( util > 0 )

   printf ("positive\n");

 else

   printf ("negative\n");

}1.6 int转为char时的数据损失

▪代码示例

int main()

{

  char c;

  while ((c = getchar()) != EOF)

 {

  putchar(c);

 }

  return 0;

}

▪现象&后果

各种系统都有自己默认的char类型,可能是unsigned char,也可能是signed char。假如当前系统默认的 char类型是 unsigned char,上述代码运行时会出现死循环。

▪Bug分析

在默认的 char 类型是 unsigned char 的系统中(可以通过 g++编译时加参数-funsigned-char来模拟),上述代码中,getchar()返回一个int型,将被强制转换为unsigned char赋给 c,这样当 getchar返回EOF(-1)时,转换成 unsigned char后的ascii值是 255。然后,系统在比较 c(unsigned char)和EOF时,会将它们均转换为 unsigned int来比较,对前者是(unsigned int)255,对后者是(unsigned int)-1=232。虽然它们都表示-1,但8位的-1和32位的-1之间的差距却很大,永远不会相等,因而会造成死循环。

修正方法是去除int转为char的强制转换。

▪正确代码

int main()

{

  int c;

  while ((c = getchar()) != EOF)

 {

  putchar(c);

 }

  return 0;

}

▪编程建议

在编程过程中,对于变量的强制转换,需要注意类型的截断和扩展,特别是char、unsigned char、unsigned int、int、short等类型在赋值时的隐式转换。1.7 非法的数组下标

▪代码示例

int main()

{

  const int size = 5;

  int intArray[size];

  float floatArray[size];

  for (int i = 0; i < size; i++)

 {

   floatArray[i] = size - i - 2.1;

   intArray[(int)floatArray[i]] = i ;

   printf("i=%d, floatArray[%d]=%f, intArray[%d])=%d \n", i, i,

    floatArray[i], (int)floatArray[i], intArray[(int)

floatArray[i]]);

 }

  return 0;

}

▪现象&后果

数组intArray的下标会出现负值的情况,导致程序出错。

▪Bug分析

上述代码行“intArray[(int)floatArray[i]] = i;”中整型数组 intArray的下标中有floatArray数组的元素。数组下标只能为非负整数,代码中只做了int的强制转换,不能保证它不会出现负数。在这段代码中,当 i 为 4 时,floatArray[4]=−1.1,这时候就出现了负下标−1,因而导致程序出错。

正确做法是,在用一个变量作为数组下标时,使用前需要验证其合理性。

▪正确代码

int main()

{

  const int size = 5;

  int intArray[size];

  float floatArray[size];

  for (int i = 0; i < size; i++)

 {

   floatArray[i] = size - i - 2.1;

   if(floatArray [i] < 0)

   continue;

   intArray[(int)floatArray[i]] = i ;

   printf("i=%d, floatArray[%d]=%f, intArray[%d])=%d \n", i, i,

    floatArray[i], (int)floatArray[i], intArray[(int)

   floatArray[i]]);

 }

  return 0;

}

▪编程建议

C/C++语言中数组下标越界,编译器是不会检查出错误的,但是实际上后果会很严重,可能会导致程序崩溃等。当使用变量作为数组下标访问数组元素时,检查数组下标值的合理性是一个良好的编程习惯,可以避免数组越界的错误发生。1.8 有符号int与无符号int比较的后果

▪代码示例

int array[] = {23, 24, 12, 204} ;

#define TOTAL_ELEMENTS (sizeof(array)/sizeof(array[0]))

int main(void)

{

  int d = -1;

  if(d <= TOTAL_ELEMENTS)

  printf("TRUE\n");

 else

  printf("FALSE\n");

}

▪现象&后果

程序运行结果打印的是FALSE而不是TRUE。

▪Bug分析

由于sizeof()的返回类型是无符号整型,因此,上述代码中TOTAL_ELEMENTS的值是 unsigned int类型。if语句在比较 signed int型变量d和TOTAL_ELEMENTS宏返回的 unsigned int型值的时候,signed int型变量被转换为 unsigned int型变量。-1转换成 unsigned int的结果是一个非常巨大的正整数(在 32位操作系统上会转换成2^32-1),导致if判断为假。

解决的办法是将宏TOTAL_ELEMENTS返回的结果强制转换为int型。

▪正确代码

int array[] = {23, 24, 12, 204} ;

#define TOTAL_ELEMENTS (int)(sizeof(array)/sizeof(array[0]))

int main(void)

{

  int d = -1;

  if(d <= TOTAL_ELEMENTS)

  printf("TRUE\n");

 else

  printf("FALSE\n");

}1.9 有符号的困惑

▪代码示例

struct data

{

  int flag: 1;

  int other: 31;

};

int status()

{

  return 1;

}

int main()

{

  struct data test;

  test.flag = status();

  if (test.flag == 1)

 {

   printf("test.flag=1 ,it's true !\n");

 }

 else

   printf("test.flag != 1 ,it's false !!\n");

}

▪现象&后果

无论函数status的返回值是1还是其他值,都无法运行到if分支里面。

▪Bug分析

这段代码在结构体中定义了一个int型的位域变量,而用一个bit来表示int时,这一位是用来表示符号位的,带符号的一个bit的位域变量的取值范围是0或-1(无符号的一个bit的位域变量的取值范围是0或1)。stauts()的返回值1赋给flag时会出现溢出,flag值变为-1,所以status无论返回什么值给flag,都不会是1,这样就无法运行到if语句分支。

解决的办法是将包含标志位的变量设置为无符号型:unsigned int flag:1。

▪正确代码

struct data

{

  unsigned int flag: 1;

  int other: 31;

};

int status()

{

  return 1;

}

int main()

{

  struct data test;

  test.flag = status();

  if (test.flag == 1)

 {

   printf("test.flag=1 ,it's true !\n");

 }

 else

   printf("test.flag != 1 ,it's false !!\n");

}1.10 整除的精度问题

▪代码示例

int main()

{

  float result;

  result = 1 / 6;

  printf("result = %f\n", result);

}

▪现象&后果

代码计算出来result的结果是0,不符合预期。

▪Bug分析

由于 1 和 6 都是整型常量,两个整型常量的运算结果依然是整型,因此,1/6的结果为0,不会保留小数部分。

如果希望保留小数部分,至少需要把一个数写成小数。

▪正确代码

int main()

{

  float result;

  result = 1.0 / 6;

  printf("result = %f\n", result);

}

▪编程建议

关于C语言的隐式类型转换,总结有以下两点。(1)赋值时,一律是右边值转换为左边类型,但是右边是表达式时,会先进行运算,然后才对运算的结果进行数据类型转换。(2)当不同类型的变量进行计算时,遵循由低级向高级转换的规则,例如,char型会转换为整型,short型转换为int型,float型会转换为double型。1.11 浮点数比较的精度问题

▪代码示例

int main(void)

{

  float f = 1.0 / 3.0;

  float expect_f = 0.333333;

  double d = 1.0 / 3.0;

  double expect_d = 0.333333;

  printf("f=%f, expect_f=%f, d=%lf, expect_d=%lf \n", f, expect_f,

 d, expect_d);

  if (f == expect_f && d == expect_d)

   printf(" Equal! \n");

 else

   printf(" Not equal! \n");

}

▪现象&后果

上述代码中,即使f和expect_f相等并且d和expect_d相等,if的判定条件也未必为真。

▪Bug分析

在计算机中,浮点数表示精度的位数有限,因此,不能准确地表示一个小数(IEEE 754规定的单精度 float数据类型的表示精度为 7位有效数字,双精度 double为16位有效数字),所以,在代码中对浮点数据类型进行比较时不建议使用== 、<= 、>=、 !=等比较运算符。例如,在计算中作如下比较:if (result == expect_result),这个结果几乎是永远不可能为真的。即使某一次比较结果为真,那也是偶然的,因为,这个比较结果是不稳定的:数据、编译器的微小改变都可能使程序产生不同的结果。

浮点数进行比较时,一般比较它们之间的差值在一定范围之内。

▪正确代码

int main(void)

{

  float f = 1.0 / 3.0;

  float expect_f = 0.333333;

  double d = 1.0 / 3.0;

  double expect_d = 0.333333;

  printf("f=%f, expect_f=%f, d=%lf, expect_d=%lf \n", f, expect_f,

 d, expect_d);

  if (fabs(f - expect_f) < 0.00001 && fabs(d - expect_d) < 0.00001)

   printf(" Equal! \n");

 else

   printf(" Not equal! \n");

}1.12 最小负整数取相反数溢出

▪代码示例

int main()

{

  int minInt = 0xffffffff;

  if (minInt < 0)

 {

   minInt = -minInt;

 }

  printf("%d\n", minInt);

  return 0;

}

▪现象&后果

程序目的是取相反数,但是取反后溢出,输出为1。

▪Bug分析

有符号的数据类型,均有正负数之分,如:int、float、double。int表示范围不对称,例如,在32位机器上,int范围为-2147483648~2147483647,如果对-2147483648取反得到的数将是溢出之后的1。所以,在对int类型数据进行取反处理时,需要额外处理这种特殊情况。

▪正确代码

int main()

{

  int minInt = 0xffffffff;

  if(minInt == 0xffffffff)

 {

   printf("minInt=%d\n", minInt);

 }

  else if (minInt < 0 )

 {

   minInt = -minInt;

 }

  printf("%d\n", minInt);

  return 0;

}

▪编程建议

在对数据进行操作时,要考虑数据的取值范围,避免数据溢出造成程序稳定性差的问题。1.13 临时变量溢出

▪代码示例

long multiply(int m, int n)

{

  long score;

  score = m * n;

  return score;

}

▪现象&后果

当m、n都取较大的值时,如1亿(在int范围之内的较大值),导致程序运行后处理结果不正确。

▪Bug分析

在64位操作系统下,int型通常占4个字节,long型通常占8个字节,两个int型变量相乘的值范围是long型变量的值范围。上述代码中,score=m*n 这行代码在执行时,m和n相乘的结果会先存储在一个临时的int变量中,然后再赋值给long变量score,这个临时变量是很容易溢出的。所以,需要在表达式运算前先对m和n做数据类型转换。

▪正确代码

long multiply(int m, int n)

{

  long score;

  score = static_cast(m) * static_cast(n);

  return score;

}

▪编程建议

对隐式的类型转换,一般来说向上是安全的,向下会出现数据截断丢失,导致数据错误。

事实上,上述正确代码只在long型字节数是int型字节数两倍的情况下才是正确的,如果在某些平台下,long 型和int 型字节数一样的话(如32 位操作系统int型和long型通常都占4个字节),仍然需要注意两个int型相乘结果溢出的问题。1.14 size_t导致的死循环

▪代码示例

int main(void)

{

  size_t size = sizeof(int);

  while( --size >= 0 )

 {

   cout << "size=" << size << endl;

 }

  return 0;

}

▪现象&后果

while条件永远为真,程序进入死循环。

▪Bug分析

size_t是 sizeof操作符返回的结果类型,size_t在32位系统中是 unsigned int,在 64位系统中是 unsigned long int。上述代码中,每执行一遍语句“while(--size >=0)”,size就会减1,当size的值等于0并再次作−−size运算时,size会因溢出再次等于它取值范围内的最大值,所以,size的值恒大于等于0,while条件一直为真。

▪正确代码

int main(void)

{

  size_t size = sizeof(int);

  while( --size > 0 )

 {

   cout << "size=" << size << endl;

 }

  if(size == 0 )

   cout << "size=" << size << endl;

  return 0;

}1.15 误用short引起缓冲区溢出

▪代码示例

#define MAX_BUF 256

#define OVER_SHORT 50000 //大于short最大值32767

void testCode(char *input)

{

  short len = strlen(input);

  char buf[MAX_BUF];

  cout << "strlen(input) is:" << strlen(input) << endl;

  cout << "len is :" << len << endl;

  cout << "MAX_BUF is:" << MAX_BUF << endl;

  if(len < MAX_BUF)

 {

   //do copy

   strcpy(buf, input);

 }else{

   cout << "overflow!" << endl;

 }

}

int main()

{

  char *str = new char[OVER_SHORT];

  memset(str, 'a', OVER_SHORT);

  str[OVER_SHORT - 1]='\0';

 testCode(str);

  return 0;

}

▪现象&后果

程序运行时缓冲区溢出引起错误。

▪Bug分析

strlen()的返回值是 size_t类型,size_t在 32位系统是 unsigned int型,通常占 4个字节,在 64位系统是unsigned long int型,通常占 8个字节,而 short类型一般占两个字节(short的取值范围为-2^15~2^15-1)。一个size_t类型变量赋值给short类型时,如果size_t超过了short能表示的最大值时,将会引起溢出。上述代码中,执行 len = strlen(input)语句时,由于 input字符串长度(49999)超过了 short的最大值,导致 len溢出变成负数,从而导致 if判断为 true,执行 strcpy(buf, input)语句,而 buf缓冲区长度小于input字符串长度,造成缓冲区溢出并引发错误。

正确的做法是将函数TestCode中的变量len的数据类型改为size_t。

▪正确代码

void testCode(char *input)

{

  size_t len = strlen(input);

  char buf[MAX_BUF];

  cout << "strlen(input) is:" << strlen(input) << endl;

  cout << "len is :" << len << endl;

  cout << "MAX_BUF is:" << MAX_BUF << endl;

  if(len < MAX_BUF)

 {

   //do copy

   strcpy(buf, input);

 }else{

   cout << "overflow!" << endl;

 }

}1.16 区分continue和return

▪代码示例

int main()

{

  double minScore = 0.0;

  double maxScore = 100.0;

  double allScore[10] = {99.0, 98, 88, -1, 62, 75.6, -10, 67, -1};

  double sumScore = 0.0;

  double averageScore = 0.0;

  int validNum = 0;

  for(int i = 0; i < 10; i++)

 {

   if(allScore[i] > maxScore || allScore[i] < minScore)

  {

    cout << "Bad score:" << allScore[i] << endl;

    return 1;

  }

   sumScore += allScore[i];

  validNum++;

 }

  if( validNum > 0 )

   averageScore = sumScore / validNum;

  cout << "average score:" << averageScore << endl;

  return 0;

}

▪现象&后果

这段代码原本是为了计算数组中有效成绩的平均成绩,但是却没有计算出来。

▪Bug分析

在C语言中,用return来返回某个值并退出程序。在上段代码中,遍历到数组中的-1时会执行return,结束程序,因此,-1后面的成绩无论是否有效,都不能计算出平均成绩。这里应该把return改成continue。在for循环中,continue是结束本轮循环,执行下一次循环,因此,可以过滤掉无效成绩,计算出有效成绩的平均值。

这段代码本身并没有语法错误,只是混淆了return和continue的概念,使用不当导致结果不符合预期。

正确的做法是将main函数 for循环中的 return 1 改为 continue。

▪正确代码

int main()

{

  double minScore = 0.0;

  double maxScore = 100.0;

  double allScore[10] = {99.0, 98, 88, -1, 62, 75.6, -10, 67, -1};

  double sumScore = 0.0;

  double averageScore = 0.0;

  int validNum = 0;

  for(int i = 0; i < 10; i++)

 {

   if(allScore[i] > maxScore || allScore[i] < minScore)

  {

    cout << "Bad score:" << allScore[i] << endl;

   continue;

  }

   sumScore += allScore[i];

  validNum++;

 }

  if( validNum > 0 )

   averageScore = sumScore / validNum;

  cout << "average score:" << averageScore << endl;

  return 0;

}1.17 指针常量和常量指针的区别

▪代码示例

int main()

{

  int x = 2;

  int y = 4;

  int *const px = &x

  cout << *px << endl;

  const int *py = &y

  cout << *py << endl;

  px = &y

  cout << *px << endl;

 *py = 1;

  cout << *py << endl;

  return 0;

}

▪现象&后果

编译代码,会提示类似如下的错误:

error: assignment of read-only variable 'px'

error: assignment of read-only location

▪Bug分析

问题的根源是没有区分指针常量和常量指针的概念。上述代码中 px 是指针常量,它的概念是:指针本身是常量。它指向的地址是不可改变的,但地址里的内容可以通过指针改变。所以,语句px=&y将y的地址重新赋给指针px,编译器会报错。指针 py 是常量指针,它的概念是:指向常量的指针,顾名思义,就是指针指向的是常量,即,它不能指向变量,它指向的内容不能被改变,不能通过指针来修改它指向的内容,但是指针自身不是常量,它自身的值可以改变。代码中,语句*py=1 通过指针 py 的解引用(dereference),来改变指针指向的内容值,所以编译器会报错。

▪正确代码

int main()

{

  int x = 2;

  int y = 4;

  int *const px = &x

  cout << *px << endl;

  const int *py = &y

  cout << *py << endl;

  py = &x //将x的地址赋值给常量指针py

  cout << *py << endl;

 *px = 1; //通过指针常量px,改变px所指的value

  cout << *px << endl;

  return 0;

}1.18 字符数组和指针不总是等价的

▪代码示例

文件sub.c中:

char str[] = "Hello World!\n";

main函数所在文件中:

#include

extern char *str;

int main()

{

  std::cout << str << std::endl;

  return 0;

}

▪现象&结果

g++编译报错。报错信息如下:

error: conflicting declaration 'char* str'

error: 'str' has a previous declaration as 'char str [14]'

▪Bug分析

关键字extern可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量或函数时在其他模块中寻找其定义。编译上述代码时,编译器会去sub.cpp中查找 *str变量,但是,sub.cpp中没有定义str这个指针变量,只是定义了str[]数组。因此查找失败,导致编译器报错。所以,char数组和char指针在很多时候可以相互替换,但这种在不同文件中用extern引用时,它们仍然是不同的。

▪正确代码

extern char str[];

int main()

{

  printf("in main.c , str:%s\n", str);

  return 0 ;

}1.19 结构体成员变量初始化的隐患

▪代码示例

struct rectangle

{

  int lenth;

  int width;

};

int main( )

{

  struct rectangle rect1 = {4, 2};

  return 0;

}

▪现象&后果

这段代码虽然编译和运行时不会有任何问题,但是,一旦结构体的成员变量顺序发生了变化,就可能导致错误。

▪Bug分析

这段程序对结构体的内存布局作了假设。一旦调整了结构体成员变量的顺序,调用方(示例中的main函数 )即使不修改代码,编译时也不会有错误提示,但是程序的逻辑却发生了变化,这往往会导致错误发生。尤其是当这个结构体是第三方提供的时候,这种调整往往会被调用方的程序员所忽略。

▪正确代码

struct rectangle

{

  int lenth;

  int width;

};

int main( )

{

  struct rectangle rect1 = {rect1.lenth = 4, rect1.width = 2};

  return 0;

}

▪编程建议

对结构体进行赋值时,建议具体到结构体的成员变量名。1.20 返回值非void的函数没有返回值

▪代码示例

bool initA()

{

  printf("init A done.\n");

  return true;

}

bool initB()

{

  printf("init B done.\n");

  return true;

}

bool initC()

{

  time_t seconds = time(NULL);

  printf("seconds is %d.\n", seconds);

 if(seconds%2==0){

   printf("init C done.\n");

 }else{

   printf("init C fail.\n");

 }

}

bool init()

{

  return initA() && initB() &&initC();

}

int main()

{

  if( init() )

 {

   printf("all init processes return true.\n");

 }

 else

 {

   printf("at least one init returns false.\n");

 }

}

▪现象&后果

上述代码假设一个初始化函数init包含3个子init函数initA、initB和initC。只有当3个子init函数都成功,才打印初始化成功信息。只要其中一个子init函数初始化失败,就打印初始化失败信息。为简化程序,代码中initA和initB一直返回true,但initC会根据当前时间秒数,如果是偶数,就认为成功;如果是奇数,就认为失败。

上述代码在实际运行中会出现一种错误情况,就是当 initC 打印的时间秒数是偶数,表示这个子init函数成功,但在main函数中打印出来的信息却是init失败。

▪Bug分析

由于C/C++标准没有规定函数一定要有返回值,gcc/g++编译器通常不会检查函数是否有返回值,因此,bool方法在没有return语句的情况下,使用gcc/g++编译并不会报错。但是,没有return语句的函数的返回值是不确定的,它通常是函数最后一个表达式的值,它存储在EAX寄存器中。

上述代码的问题正是在于,initC函数没有显式return一个bool返回值,其值为不确定,所以会出现“虽然initC成功,但init结果总是失败”这样的情况。

改正的方法是在 initC函数中,如果成功,就显式地 return true;如果失败,也显式地 return false。

▪正确代码

initC函数修改如下:

bool initC()

{

  time_t seconds = time(NULL);

  printf("seconds is %d.\n", seconds);

 if(seconds%2==0){

   printf("init C done.\n");

   return true;

 }else{

   printf("init C fail.\n");

   return false;

 }

}

▪编程建议

非void类型的函数,建议在每个分支都要有明确的返回值。1.21 cin>>和getline混用导致的奇怪问题

▪代码示例

int main ()

{

  int zipcode;

  string address;

  cout << "Please Input Zipcode:" << endl;

  std::cin >> zipcode;

  cout << "Please Input Address:" << endl;

  getline(cin, address);

  cout << "Zipcode:" << zipcode << endl;

  cout << "Address:" << address << endl;

  return 0;

}

▪现象&后果

程序的功能是用操作符“>>”和getline分别从命令行读取两个输入:一个整型的 zipcode 和一个字符串形式的 address。但运行时发现,程序在用户输入 zipcode后不会等待用户输入address,即用getline输入address代码会被自动跳过,用户没有机会输入地址字符串。

▪Bug分析

cin是C++标准输入流istream类型对象,代表标准输入设备,相当于C语言里的stdin。在程序中包含iostream头文件即可使用cin对象。istream类重载了抽取操作符“>>”,能够读取C++中的各种基础数据类型。抽取操作符“>>”根据后面变量的类型读取数据,从非空白符号开始,遇到Enter、Space、Tab键时结束。std::getline函数则用于从istream中读取一行数据,当遇到换行符“\n”时结束返回。

这个问题的根源在于抽取操作符“>>”。当用户在命令行输入 zipcode 值时, cin>>读取到一个连续的数字(无空格)后立即停止,把用户按下回车键时,换行符“\n”留在了输入流里。cin 不会主动删除输入流里的换行符。这样,“\n”被后续的getline读到,因为getline遇到换行符结束,所以,getline不会等待用户在终端输入数据,而是立即结束返回。因而造成上面所说的奇怪现象,这种情况下 address得到一个空的赋值。一个简单的解决办法是在每一个 cin>>后加一个 cin.ignore(),用于清除留在输入流里的换行符。

▪正确代码

int main ()

{

  int zipcode;

  string address;

  cout << "Please Input Zipcode:" << endl;

  std::cin >> zipcode;

 cin.ignore();

  cout<< "Please Input Address:" << endl;

  getline(cin, address);

 cout<<"Zipcode:"<

 cout<<"Address:"<

  return 0;

}

▪编程建议

上面的简单方法虽然能解决问题,但建议最好不要在同一段代码中混用抽取操作符“>>”和 std::getline()。因为如果混用,需要特别注意“>>”是否在输入流中留下了换行符,如果留下了换行符或者其他字符或者标志,往往会造成意想不到的麻烦。一种更好的办法是使用getline一次读取一行用户输入,将用户输入视为字符串,然后再从该字符串中解析自己需要的数据。下面的代码提供了这样的一种方案。

int main ()

{

  int zipcode;

  string address;

  string input = "";

  while (true)

 {

   cout << "Please Input Zipcode:" << endl;

   getline(cin, input);

   stringstream myStream(input);

   if ( myStream >> zipcode )

   break;

  else

    cout << "Invalid number, please try again" << endl;

 }

  cout << "Please Input Address:" << endl;

  getline(cin, address);

  cout << "Zipcode:" << zipcode << endl;

  cout << "Address:" << address << endl;

  return 0;

}1.22 小结

通过以上这些基本问题的分析,我们可以了解C/C++语言最基本的一些特性,很多复杂问题在经过分解之后其根源也可以归结为这些基本问题。本章列出的 20的个例子无法覆盖基础知识的方方面面,期望读者从中得到启发,做到举一反三,进而可以更好地分析复杂问题。第2章 编译问题

本节主要介绍编译环境中遇到的一些典型问题,如动态、静态链接库的加载,命名空间,编译参数的使用等。编译相关的问题,与实际代码的逻辑关联性不大,但对代码的运行却可能产生很大的影响,而且这种影响是全局性的。一旦出现问题也更加难以定位,如果错误地将问题定位到代码上,会耽误很多时间去追踪。因此,理解编译链接阶段的原理和实现,可以帮助我们避免许多环境上的问题,专心于代码的开发。2.1 动态链接库加载错误版本

▪代码示例

在程序 test.cpp 中, main 函数里使用了动态链接库 libiscore.so 中的函数avg_score,如下:

int main(int argc, char **argv)

{

  int sum = 40000;

  float average = avg_score(sum);

  cout << "score per morth: " << average << endl;

  return 0;

}

在系统目录/usr/lib64下有libiscore.so.1.0,其中avg_score函数的实现如下:

float avg_score(int s) {

  return (float)s / 12;

}

而在用户目录/home/admin/IScore/lib下有libiscore.so.2.0,其中avg_score函数实现在1.0版本上有所修改:

float avg_score(int s) {

  int tmp = (int)(((float)s / 12) * 100);

  return (float)tmp / 100;

}

▪现象&后果

test.cpp编译并没有问题,执行时输出 score per morth: 3333.333252,而预期的输出是只保留小数点后两位,即 score per morth: 3333.33,实际与期望不符。

▪Bug分析

Linux下动态链接库的加载是通过dlopen来实现的,dlopen函数按指定模式打开指定的动态链接库文件。它有一个加载顺序:(1)RPATH,(2) LD_LIBRARY_PATH,(3)/etc/ld.so.cache 维护的so 列表,(4)/lib 和/usr/lib。RPATH可以在编译的时候通过-r来指定,执行的时候就会到这个路径下去加载,如果不存在就会出错,不会出现加载不兼容版本的问题了。LD_LIBRARY_PATH 是最常用的一种方式,可以把需要的目录加进去。在执行前,通常可以通过ldd命令来检查程序所指向的库是否正确:

ldd test

  libiscore.so => /usr/lib/libiscore.so.1 (0x00002b093c48f000)

  libdl.so.2 => /lib64/libdl.so.2 (0x0000003242600000)

  libpthread.so.0 => /lib64/libpthread.so.0 (0x0000003242e00000)

  librt.so.1 => /lib64/librt.so.1 (0x0000003246200000)

  libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x0000003246600000)

  libm.so.6 => /lib64/libm.so.6 (0x0000003242a00000)

  libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x0000003245e00000)

  libc.so.6 => /lib64/libc.so.6 (0x0000003242200000)

  /lib64/ld-linux-x86-64.so.2 (0x0000003241200000)

▪正确代码

将所需so的路径加进LD_LIBRARY_PATH中,比如路径是/home/admin/IScore/lib:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/a/lib

▪编程建议

so使用前先 export LD_LIBRARY_PATH是一个良好的编程习惯。2.2 相同名称静态库的链接顺序

▪代码示例

程序 test.cpp 使用了两个包 libexam.so 和 libbucket.a ,分别在目录/home/admin/sear/lib和/home/admin/algo/lib下,编译命令如下:

gcc test.cpp -o test -L/home/admin/sear/lib -L/home/admin/algo/lib-lexam -lbucket

然而,这两个路径下都有libbucket.a这个包,但却有着不同的功用。

▪现象&后果

程序test.cpp原本想链接的是/home/admin/algo/lib下的libbucket.a,但是在实际使用时才发现链接上了错误的包。

▪Bug分析

编译链接时,当碰到参数-l 时,如上述-lbucket ,首先,链接器会将libbucket.a/libbucket.so作为目标文件到几个标准的系统目录下查找如/lib,然后,如果找不到,则去编译参数-L指定的path下寻找,如果-L参数有多个,则按从左至右的顺序依次查找,一旦找到就会停止查找。例如,这里会优先到/home/admin/sear/lib,找到之后,就不会去后面的-L下查找了。

▪正确代码

考虑-L的目录查找顺序,或者为包更名,避免冲突。

▪编程建议

良好的lib命名规范可以避免同类问题的发生。2.3 使用命名空间来区分不同cpp中的同名类

▪代码示例

程序test.cpp使用了两个动态链接库,即algo_util和algo_common,在编译链接命令中用-l作了指定,如:

gcc test.cpp -o test -L/home/a/lib64 -lalgo_util -lalgo_common

但是这两个库中存在相同名称的类log4cpp,用来打印日志,且有相同的接口,如:

void ERROR(string message);

但接口的实现在两个包中却不相同。在程序test.cpp里,用algo_util包里的接口来打印algo_util相关的日志,用algo_common包里的接口来打印algo_common相关的日志。

▪现象&后果

程序运行时,发现所有用到log4cpp类打印日志的地方都是调用了algo_util包中的接口。

▪Bug分析

上述编译链接命令中,有两个-l参数,即-lalgo_util -lalgo_common,编译器在查找包时,是按从左至右的顺序,因此,对编译器来说 libalgo_util.so 是优先于libalgo_common.so的,这个顺序,会记录到二进制目标文件 test的 needed section中,可以通过ldd命令来查看:

ldd test

  linux-vdso.so.1 => (0x00007fff602d9000)

  libalgo_util.so => /home/a/lib64/libtesta.so (0x00007f3df0375

 000)

  libalgo_common.so => /home/a/lib64/libtestb.so (0x00007f3df01

 73000)

  libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3defda2

 000)

  /lib64/ld-linux-x86-64.so.2 (0x00007f3df0579000)

可以看出libalgo_util.so在前,libalgo_common在后。执行test的时候,遇到外部函数就会到动态链接库中查找,当按照顺序在libalgo_util.so中找到之后,就不会去后面的库中查找了。

▪正确代码

可以在 export LIBRARY_PATH中将 algo_common所在路径提前,但是解决不了根本。最好是在algo_util和algo_common程序代码中都增加命名空间:

namespace util {

  // codes of algo_util

};

namespace common {

  // codes of algo_common

};

然后在使用的时候引用命名空间,并用标识符来指定使用哪个命名空间下的函数::

using namespace common;

using namespace util;

{

  common::ERROR("error info");

}

▪编程建议

注意命名空间的使用,不能滥用,防止冲突。2.4 C++模板编译时依赖名称查找

▪代码示例

template

class ClassA

{

 protected:

   int num;

};

template

class ClassB : public ClassA

{

 public:

   void init() { num=0; }

};

▪现象&后果

上面这段代码在早年版本的GCC下编译能通过,但使用新的GCC版本编译时出现错误:

'num' was not declared in this scope

▪Bug分析

因为在编译时,C++会分两个阶段的名字查找来解析模板的语法,第一阶段会扫描所有非依赖名称,第二阶段(实例化模板)会扫描所有的依赖名称。所谓依赖名称,是指那些在编译时被认为是依赖于模板参数,只有在模板实例化时才能消歧的名称;反之则是非依赖名称。在上面的代码中, num变量被编译器看作是非依赖名称,所以要在第一阶段的解析过程中识别num变量,但由于目前编译器还没有实现在模板基类中对非依赖名称的查找,所以在类 ClassB作用域没有找到num变量,故报出上面的错误。

早期的GCC并没有真正实现两阶段名字查找机制,在对模板类的语法解析时,将所有的名字查找都留到了实例化的时候进行,所以上面的代码可以通过编译。

▪正确代码

在变量num之前加上this->,使变量num成为依赖名称,这样对变量num的查找会在第二阶段进行,程序会正常编译通过:

void init() { this->num=0; }2.5 违背ODR原则可能会带来的意想不到的问题

▪代码示例

testapp.h:

#include

#include

#include

#include

using namespace std;

class Test {

 public:

  Test();

   virtual ~Test();

 private:

   vector v1, v2, v3, v4, v5;

   bool isv1, isv2, isv3, isv4, isv5;

   int flag;

   string name;

   bool usev1, usev3, usev5;

};

class testApp{

 public:

   void init();

   Test t1;

   Test t2;

   vector sv;

};

testapp.cpp:

#include " testapp.h"

Test::Test() {}

Test::~Test() {}

void testApp::init(){

  std::cout << sv.size() << std::endl;

}

main.cpp:

#define _GLIBCXX_DEBUG

#include "testapp.h"

int main(){

  testApp* obj = new testApp();

 obj->init();

  std::cout << obj->sv.size() << std::endl;

}

▪现象&后果

执行程序,输出结果如下:

18446744073709025614

0

从上面的代码可以看到,main函数中对象obj的成员sv并没有被赋值,其size()本应为0,但两次打印sv.size()出现很大差距。

▪Bug分析

ODR(One-Define-Rule),即单定义规则:C++标准规定,对于类和内联函数,在每个编译单元中最多被定义一次,而且要保证不同编译单元之间定义的一致性;对其他函数,在所有文件中只能存在一处定义。一个编译单元是对程序员提交给编译器的文件执行预处理后的结果,预处理包括按照条件编译命令(#if,#ifdef)去除掉未被选中的代码块、去除注释、递归地插入“#include”指令所引用的文件、将宏展开等操作。

上面的代码经过预处理器之后会产生两个编译单元,即main.cpp和testapp.cpp,两者都包含了testapp.h的代码,即类TestApp和Test的定义。但是经过编译之后,由于main.cpp中存在宏_GLIBCXX_DEBUG,导致类Test在两个编译单元中的定义有微小的差别。加上_GLIBCXX_DEBUG 的编译方式会改变 C++标准类模板,如std::vector的大小和定义,所以在setup函数中和main.cpp 对testApp类的访问,会因为对 Test 有不同的定义和大小,最终导致不一致结果的发生(关于宏_GLIBCXX_DEBUG,更详细的说明可查阅C++官方文档)。

▪正确代码

将宏定义#define _GLIBCXX_DEBUG从main.h中移到 testapp.h中,保持一致性。或者去掉宏定义,以编译参数的方式加到编译命令中:

g++ -o test testapp.cpp main.cpp –D _GLIBCXX_DEBUG

▪编程建议

单定义规则是构建一个良好C++工程的基础,尤其当程序文件较多,声明与实现相分离,需要include头文件的情况下,更容易违反。多次引用.h文件造成变量重复定义是比较常犯的错误,编译器也能事先发现。像上述类型较为隐蔽,也不易追查,须更加注意,养成良好的编码规范。2.6 变量共用内存时使用O2优化编译

▪代码示例

int main()

{

  int64_t mem = 0;

  int32_t* low = (int32_t*)&mem

  int32_t* high = low + 1;

 *high = 0xffffeeee;

 *low = 0xaaaabbbb;

  printf("mem=%lx, high=%x, low=%x\n", mem, *high, *low);

  return 0;

}

▪现象&后果

上述代码能顺利通过编译,但使用不同的编译参数编译后,运行时 mem 的输出结果却完全不同。例如,当编译选项不包含“-O2”时,输出为:

"mem=ffffeeeeaaaabbbb, high=ffffeeee, low=aaaabbbb"

但在编译选项中加上“–O2”时,输出为:

"mem=0, high=ffffeeee, low=aaaabbbb"

即当使用优化选项O2时,mem的值为0。有无O2编译选项使mem的值完全不同。

该问题在 g++ (GCC) 3.4.6和 g++ (GCC) 4.1.2环境下均存在。

▪Bug分析

上面代码的目的是希望在一个int64位的变量中存放两个int32位变量,当改变32位变量的值时,64位变量的值也相应改变。但当加上-O2优化编译选项,则int64变量mem的值没有发生变化,依然为0。问题分析的过程是,首先发现使用-O1选项编译正常,但-O2选项不正常。于是分析O2比O1实际多增加了哪些优化内容,最终确定是优化标识-fstrict-aliasing起了作用。如果编译时使该编译优化标识失效,则编译后的程序运行正常,即使用如下命令行编译后正常:

g++ sample.cpp -g -O2 -fno-strict-aliasing

-fstrict-aliasing 优化标识生效会在编译过程中应用最严格的别名(aliasing)规则,不允许一种变量类型与另一种不同的变量类型处于相同的内存地址。所以,这里不允许int32与int64的变量处于同一块内存地址,因此,在编译优化之后,mem、low和high已经指向了不同的内存地址,给low赋值只能改变它自己的值,并不能操作mem的地址空间。

▪正确代码

去掉-O2 编译选项即可。或者使用-O 选项,但是将让优化标识-fstrict-aliasing失效,即:

g++ sample.cpp -O2 -fno-strict-aliasing

▪编程建议

优化编译可以在一定程度上提升性能,但它有可能会带来一些不可预料的问题,而且经过优化的程序也不易调试,因此,如果对性能的要求不高,在编译时可通过编译选项“-O0”去掉编译器的优化。2.7 小结

编译链接是程序代码变为机器可执行代码的第一步,也是很重要的一步,了解编译器链接器的过程和原理,能有效避免后续执行中出现的一些与程序逻辑无关的问题。这章主要列举了编译链接过程中容易出现的一些问题,包括动态和静态链接库的链接加载顺序、编译参数等,相信读者从这几个案例中可以有所借鉴。第3章 库函数问题

本章主要介绍库函数的使用中会遇到的问题。使用库函数可以降低软件开发的难度,提高代码编写的效率。这一章主要涵盖的内容有,调用字符串库函数时需要注意对字符串结束符‘\0’的处理,复制字符串的时候要注意内存空间是否写溢出,函数调用前需要做必要的初始化,函数使用后对其返回值需要做正确处理,容器类的增、删操作要注意迭代器失效等,忽视这些方面将带来各种各样的问题。3.1 sprintf函数引起的缓冲区溢出

▪代码示例

int main()

{

  char src[50] = "abcdefghijklmnopqrstuvwxyz";

  char buf[10] = "";

  int len = sprintf(buf, "%s", src);

  printf("src=%s\n", src);

  printf("len=%d\n", len);

  printf("buf=%s\n", buf);

  return 0;

}

▪现象&后果

程序运行时,用sprintf函数把字符数组src的内容往字符数组buf复制时会溢出,可能出现段错误(Segmentation fault)。

▪Bug分析

上述代码从一个字符数组src复制字符串到另外一个字符数组buf中,src的字符串长度为26,但buf的长度只有10,用sprintf函数进行复制的时候会把src的所有字符往buf里写,从而引起buf溢出。

正确的做法是在复制之前检查 buf 的长度是否足够,或者直接用更安全的snprintf函数代替sprintf。

▪正确代码

int main()

{

  char src[50] = "abcdefghijklmnopqrstuvwxyz";

  char buf[10] = "";

  int len = snprintf(buf, sizeof(buf), "%s", src);

  if(len > sizeof(buf) - 1)

 {

   printf("[Error] Source string length is %d. The buf size %d is

  not enough. Copy incomplete!\n", len, sizeof(buf));

 }

 else

 {

   printf("src=%s\n", src);

   printf("len=%d\n", len);

   printf("buf=%s\n", buf);

 }

  return 0;

}

▪编程建议

在libc参考手册对sprintf函数的说明中有一个警告,如果复制的字符串长度超过提供的 buf 串的长度,sprintf 函数会变得很危险。为了避免这个问题,可以用snprintf函数来代替sprintf函数。但在使用snprintf的时候,在调用这个函数之后需要对返回值作检查,如果返回值比分配的buf长度要大,表示复制不完整,则需要重新分配大的空间之后再一次调用snprintf函数。

在libc参考手册中,也同时提到,在实际使用过程中,用asprintf函数代替snprintf函数会更方便些。asprintf函数不需要预先分配buf,它能在复制过程中根据实际复制源字符串的大小动态分配空间,具体可参考libc参考手册。3.2 snprintf函数format参数的问题

▪代码示例

int main()

{

  char buf[10] = "";

  char src[10] = "hello %s";

  int len = snprintf(buf, sizeof(buf), src);

  printf("buf=%s\n", buf);

  return 0;

}

▪现象&后果

上述代码的字符串src中如果存在格式化字符串(如本例中src包含“%s”),程序运行时最后打印出的 buf 值将是不可预期的字符串或者出现段错误(Segmentation fault)。

▪Bug分析

snprintf函数本身是可变参数函数,原型如下:

int snprintf( char* buffer, int buf_size, const char* format, ... )

当函数只有3个参数时,如果第三个参数中没有包含定义格式化的字符串,函数调用没有问题,但如果第三个参数中包含定义格式化的字符串,如例子中的“%s”,由于没有提供第四个参数,函数将会访问一个不确定的内存地址读取内容作为这个参数的值,从而导致不可预期的输出或者段错误。正确的做法是提供完整的参数,防止出现参数遗漏的现象。

▪正确代码

int main()

{

  char buf[10] = "";

  char src[10] = "hello %s";

  int len = snprintf(buf, sizeof(buf), "%s", src);

  printf("buf=%s\n", buf);

  return 0;

}3.3 错误使用snprintf函数返回值

▪代码示例

int main()

{

  char buf[10] = "";

  char src[30] = "hello world! hello world!";

  int len = snprintf(buf, sizeof(buf), "%s", src);

  printf("return len=%d\n", len);

  buf[len] = '\0';

  printf("buf=%s, bufLen=%d\n", buf, strlen(buf));

  return 0;

}

▪现象&后果

上述代码运行时返回的len是25,写buf[len]=’\0’时将出现错误。

▪Bug分析

snprintf 函数返回的是预写入的字符串长度。以上述代码为例,如果源字符串src的长度小于等于sizeof(buf)-1,返回值则为实际写入目标字符串buf的字符数,也就是 src 的长度;如果 src 的长度大于 sizeof(buf)-1,实际写入 buf 的字符数为sizeof(buf)-1,但返回值依然为src的长度。但snprintf返回值不一定就是src的长度,例如,当格式化字符串包含其他字符时,如上面的“%s”改为“ab=%s”,返回值就比src的长度要大。实际上snprintf返回值可以理解为当buf大小没有限制时写到buf的字符个数。

在例子中len=25,buf[25]=‘\0’产生写越界,从而产生段错误,可能导致重大问题。上述代码实际上是想在snprintf复制完之后显式地在buf结尾处添加一个‘\0’。但实际上,snprintf函数在复制结束时自动就会处理字符串结束标志‘\0’的问题,不用额外处理。

snprintf函数的返回值常被用来和目标字符串buf的大小进行比较,以判断此次复制是否完全。

▪正确代码

int main()

{

  char buf[10] = "";

  char src[30] = "hello world! hello world!";

  int len = snprintf(buf, sizeof(buf), "%s", src);

  printf("return len=%d\n", len);

 if(len>sizeof(buf)-1)

 {

   printf("[Error] Source string length is %d. The buf size %d

  is not enough. Copy incomplete!\n", len, sizeof(buf));

 }else{

   printf("buf=%s, bufLen=%d\n", buf, strlen(buf));

 }

  return 0;

}3.4 字符串复制不完整

▪代码示例

int main()

{

  char str[] = "ab\0c";

  char buf[5];

  strcpy(buf, str);

  int bufSize = sizeof(buf);

  printf("bufSize=%d\n", bufSize);

  for(int i=0; i

 {

   printf("%c\n", buf[i]);

 }

  return 0;

}

▪现象&后果

复制到目标字符串buf中的内容与源字符串str中的内容不相同。

▪Bug分析

字符串在内存中是以二进制流的形式存在的,对二进制流来说‘\0’也是一个正常的01序列,但这个‘\0’对C语言的字符串来说是一个特殊的字符,它是用来标志字符串的终结符,因此,对字符串函数strcpy来说,其操作的范围,只是字符串的开始到它遇到的第一个‘\0’。故当字符串中间存在‘\0’时,‘\0’后面的字符就会被忽略。正确的做法是使用memcpy函数,这个函数是对内存的复制,如前面说的,任何字符都只是01序列,‘\0’也一样。所以,如果被复制的字符数组中含有‘\0’,可以使用memcpy,复制的长度由它的第三个参数决定,不会依赖‘\0’。

▪正确代码

int main()

{

  char str[] = "ab\0c";

  char buf[5];

  memcpy(buf, str, 5);

  int bufSize = sizeof(buf);

  printf("bufSize=%d\n", bufSize);

  for(int i=0; i

 {

   printf("%c\n", buf[i]);

 }

  return 0;

}

▪编程建议

strcpy函数和memcpy函数之间的区别:strcpy只能用于字符串复制,不需要指定长度,它遇到被复制字符串的结束符‘\0’就结束;memcpy 可用于复制任意内容,复制长度由其第三个参数决定。3.5 string类的c_str方法使用不当

▪代码示例

int main()

{

  string str = "abcd";

  const char *pcStr = str.c_str();

  printf("cStr=%s, pcStr=%p\n", pcStr, pcStr);

 str.append("efg");

  const char *pcStr2 = str.c_str();

  printf("cStr2=%s, pcStr2=%p\n", pcStr2, pcStr2);

  return 0;

}

▪现象&后果

在语句“str.append("efg")”之后,指针 pcStr 成为“野指针”,可能造成严重后果。

▪Bug分析

string类的c_str()方法返回的是一个常量指针,它所指向地址的内容是不会改变的。上述代码中,在没有改变str的值之前,指针变量pcStr就是str的首地址,但当通过append函数对str追加了一些内容之后,因为append会先开辟一段新内存,然后再将原来的值复制过来,所以,str的首地址已经发生了改变,而pcStr指向的内存实际上已经被释放,因而pcStr成为“野指针”。

改正的方法是,在将str.c_str()赋值给pcStr之后,不去改变str的值 ;或者当需要改变 str 的值时,使用 strcpy 函数把 str.c_str 返回的内容复制出来,再赋值给pcStr。下面正确代码使用的是第二种方法。

▪正确代码

int main()

{

  string str = "abcd";

  char pcStr[20];

 strcpy(pcStr,str.c_str());

 str.append("efg");

  return 0;

}

▪编程建议

最好使用strcpy函数来操作string类的c_str方法返回的指针,从而避免带来“野指针”。3.6 string类的“[]”操作符使用不正确

▪代码示例

void messageCopy(const char* msg, size_t length, string &str)

{

  for( int i = 0; i < length; i++)

 {

   str[i] = msg[i];

 }

}

int main()

{

  char message[10]="123456789";

  string str;

 str.reserve(10);

  messageCopy(message, 10, str);

  printf("str=%s, length=%d\n", str.c_str(), str.size());

  if (str.empty())

 {

   printf("empty message\n");

 }

  return 0;

}

▪现象&后果

上述代码通过messageCopy函数接收一个字符串,并通过string类的empty函数判断返回的字符串 str是否为空,若为空则打印“empty message”。但不管给的源字符串message是多长的字符串,程序总是打印“empty message”,但直接打印str.c_str函数的输出并不为空。

▪Bug分析

问题出在messageCopy函数上。在函数里面输入的源字符串msg的内容被逐个按字符复制给传入的目标字符串str,确实msg的内容也复制到了str的内存空间中,这通过打印str.c_str函数的输出可以验证。string对象内部维持了一段连续空间,以及表示所存字符串长度的 size。这里的问题就是,string 对象 size 的值与实际存在string对象中的字符串长度出现了不一致。上述代码中,虽然改动了str的内存空间,但并没有触发size的改变,而empty()函数只是根据原有的size是否为0作判断,而没有重新计算str所占内存长度,所以,造成非预期结果。

▪正确代码

int main()

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载