从缺陷中学习C/C++

978-7-115-32159-6
作者: 刘新浙 刘玲 王超 李敬娜 等
译者:
编辑: 张涛

图书目录:

详情

“从缺陷中学习C/C++”这本书收集并整理了102个实例,这些实例都来自于工程一线实践,虽然大多数看起来像是初学者犯的低级错误,但实质上有一定的代表性,有的错误根源是对C++机制的不理解或者编译过程中的副作用,或者C++标准库的实现依赖。通过阅读这些实例,你可以对C++有更细致的理解。

图书摘要

从缺陷中学习C/C++

Lessons Learned from C/C++ Defects

刘新浙 刘玲 王超 李敬娜 等 编著

人民邮电出版社

北京

主要编著人员:

刘新浙、刘玲、王超、李敬娜、李爱华、陈足先。

其他编著人员:

谭淑丹、曹恒智、付英。

素材提供者:

刘新浙、刘玲、刘晓俊、王艳、李爱华、黄元君、谭淑丹、杨晓霞、王竟时、王超、李颖、曹恒智、李敬娜、陈足先、刘云卿、刘霏暄、郜翔、陶旭颖。

本书在编写过程中得到了很多人的帮助。

除本书作者外,原淘宝北京搜索与广告算法测试小组的很多同事都参与了本书的审查工作,提出了文字等方面的一些改进建议,在此一并表示深深的感谢。在本书编写过程中,淘宝公司技术研发部尤其搜索技术部的一些同事,也给予了一些中肯的意见和反馈,在此表示衷心的感谢。

另外,要特别感谢淘宝公司北京测试部门负责人刘立川先生,没有他的鼓励和督促,作者可能就不会在两年多的写作中坚持下来,这本书可能就不会出版面世。

在本书即将出版时,我们非常荣幸地邀请到了著名计算机专家潘爱民老师为本书做推荐序。潘老师在百忙之中抽出时间通读本书,并作序,还给予了非常专业的修改意见,这让本书受益良多。

最后,感谢人民邮电出版社的编辑,在本书出版过程中出谋划策,给予了很专业的建议,并促成了本书的最终顺利出版。

编者

C++是易学难用的语言。经过20多年的发展,C++已经变得极为复杂,很多语言特性看似优美,但给工程上带来了很大的挑战。因此,如何在工程项目中用好C++语言,并不像通过一本教程来学会写C++程序那么简单,这是一个不断积累经验和吸取教训的过程。工程上推荐使用的往往是C++大量语言特性的一个子集,原因是为了避免在工程项目中埋下各种陷阱。

譬如,Google是一家重度使用C++的公司,它有许多开源软件(如Android)使用了C++语言。Google公开了一份“Google C++ Style Guide——Google C++编程风格指南”,阐明了在Google的项目中如何有效地使用C++,应遵守哪些规范,以避免各种可能的陷阱。Google的这份指南值得工程线上的C++程序员认真阅读。阿里巴巴也是一家重度使用C++的公司,内部有大量的工程用到了C++语言。虽然阿里巴巴集团没有统一的C++编程指导,但各个团队都在摸索并制定出符合自己需要的编程规范,处于快速积累经验的过程中。很高兴淘宝广告技术部的测试团队很早就意识到了C++语言在工程中的诸多陷阱,并有意识地将其收集和整理出来,这就是你看到的这本书。

《从缺陷中学习 C/C++》这本书收集并整理了 102 个实例,这些实例都来自于工程一线实践,虽然大多数看起来像是初学者犯的低级错误,但实质上有一定的代表性,有的错误根源是对C++机制的不理解或者编译过程中的副作用,或者是对C++标准库的实现依赖。通过阅读这些实例,你可以对C++有更细致的理解。

当刘新浙邀请我为本书作序时,我欣然答应,并有幸成为这本书的第一个读者。当时觉得难能可贵的一点是,这样一本书并非是深入研究C++工程多年的C++高手在指点技术,而是一群懂C++的测试工程师在跟开发工程师探讨如何避免C++语言本身的各种缺陷。这个角度提炼出来的知识,真正是千金难买,因为他们是从“地雷阵”上走过来分享教训的。

我认真阅读了所有的实例,颇有收获,也愿意推荐给每一位在工程线上从事C++编程工作的工程师。若能够结合本书与“Google C++ Style Guide——Google 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。在此表示感谢!

期望这本书能真真切切地帮助到他人。

编者

在写C/C++程序时,一提到内存,大多数人会想到内存泄露。内存泄露是一个令人头疼的问题,尤其在开发大的软件系统时。一个经典的现象是,系统运行了10天、1个月都好好的,忽然有一天宕机了:OOM(Out Of Memory,内存用光)!于是,痛苦地查找内存泄露根源的过程开始了……本章主要讨论内存使用问题,包括内存泄露、悬挂指针、内存重复申请、变量的作用范围等,涉及指针、数组、引用等的使用。

▪代码示例

#define ARRAY_SIZE 1024

char strArray[ARRAY_SIZE];

strArray[ARRAY_SIZE] = '\0';

▪现象&结果

数组访问越界,程序运行崩溃。

▪Bug分析

C 或C++中,数组的下标地址范围是从[0-(size-1)],下标size已经超出了数组范围。

▪正确代码

#define ARRAY_SIZE 1024

char strArray[ARRAY_SIZE];

strArray[ARRAY_SIZE-1] = '\0';

▪代码示例

int *ip = new int(12);

for (int i = 0; i < 12; i++) {

  ip[i] = i;

}

delete [] ip;

▪现象&结果

产生运行时错误,提示如下的错误信息:

glibc detected *** free(): invalid next size (fast)

▪Bug分析

int *ip=new int(12)表示 new了一个整型变量,值是 12。ip指向这个变量。

new返回的指针ip是int类型,不是一个数组指针,赋值的时候,采用数组的方式,造成越界访问内存,并且在结束的时候用delete[]删除指针数组,造成程序崩溃。

解决方法是:把小括号改写成中括号。

▪正确代码

int *ip = new int[12];

for (int i = 0; i < 12; i++) {

  ip[i] = i;

}

delete [] ip;

▪代码示例

void copy(int a[], int b[]) {

  memcpy(b, a, sizeof(a));

}

▪现象&结果

copy函数执行后,内存中的内容与设想不符,目标数组b中的内容不完整,没有把源数组a中的内容全部复制过来。

▪Bug分析

memcpy函数的原型是 void *memcpy(void *dest, const void *src, size_t n);,它的功能是从源src所指的内存地址的起始位置开始复制n个字节到目标dest所指的内存地址的起始位置中。上述程序中,copy函数的两个形参是数组a和数组b,函数体中调用了memcpy函数,并且为memcpy函数的第三个参数赋值sizeof(a)。程序的本意是期望 sizeof(a)返回数组 a 所占的字节数,通过 memcpy 函数,把源数组 a中的内容全部复制到目标数组 b中。但是数组 int a[]作为 copy函数的形参,在 copy函数体内将退化成指针,所以,sizeof(a)返回的是指针的字节数,而不是数组a的字节数。因此,数组b中只是部分复制了数组a中的内容。解决办法是:在copy函数中增加一个参数,作为数组复制的字节数。

▪正确代码

void copy(int a[], int b[], int len) {

  memcpy(a, b, sizeof(int)*len);

}

void del(int a[], int len) {

  memset(a, 0, sizeof(int)*len);

}

或者用数组的引用方式传参:

void copy(int a[], int (&b)[]) {

  memcpy(a, b, sizeof(b));

}

▪编程建议

数组传递参数时,连同数组长度一起传入是一个好方法。或者用 std::vector 代替数组可以避免不必要的麻烦。使用数组的引用,作为函数的参数,也可以解决上面的问题。

▪代码示例

class MyString {

 public:

 MyString(){

   s_ = (char *)malloc(strlen(str) + 1);

   strcpy(s_, str);

 }

  ~MyString() {

  printf("destory\n");

 }

  friend MyString operator+(const MyString &lstr, const MyString

 &rstr){

   size_t llen = strlen(lstr);

   size_t rlen = strlen(rstr);

   char buf[llen + rlen + 1];

   strcpy(buf, lstr);

   strcat(buf, rstr);

   return MyString(buf);

 }

  operator const char *() const { //当string转换char*时调用

   return _s;

 }

 private:

  char *_s;

 };

  int main()

 {

   MyString s1("hello "), s2("world !");

   const char *p = s1 + s2;

   printf("%s\n", p);

   return 0;

}

▪现象&结果

程序运行时通常是正常的,但有时会出错,特别是在多线程时,会出现奇特的错误:例如,指针p指向的内容不是期望的内容。

▪Bug分析

错误出在main函数中的 const char *p = s1 + s2代码行处。程序会首先生成一个临时对象,用来存储 s1+s2 的值,然后再把临时对象的值赋给 p,随后该临时对象析构。所以,指针p指向了一块非法内存。因为临时对象已经被析构,所以这块非法内存被系统识别为“未使用”的状态,可以被再分配使用。如果在程序中没有其他操作读写这块内存时,其内容还没有被改变,所以,可能输出符合程序预期的正确结果。但是,没有任何方法阻止那块内容的改变。所以,如果有其他操作对这块内存单元进行写操作后,可能输出的是随机值。

▪正确代码

在main函数中显式给出临时对象:

int main()

{

  MyString s1("hello "), s2("world !");

  String temp = t1 + t2;

  const char *p = temp;

  printf("%s\n", p);

  return 0;

}

▪代码示例

char *str = NULL;

  if(!str) {

   char * str = (char*) malloc (100);

   if(!str) {

    return -1;

  }

   str[0] = 'a';

 }

printf("%c\n", str[0]);

▪现象&结果

程序执行时出现coredump。

▪Bug分析

程序出现coredump的原因是代码printf("%c\n",str[0])中使用的str是空指针。程序首行定义了指针str,并且赋值为NULL。然后在if(!str){}语句块中,通过代码行char * str = (char*) malloc (100),重新定义了 str指针,并且为 str指针分配了内存空间。根据变量的作用域规则,char * str = (char*) malloc (100)这行代码定义的局部变量 str,有效范围是在 if(!str){}语句块中。代码最后部分 printf("%c\n",str[0]) ,在if(!str){}语句块范围之外,此处使用的str不是在if(!str){}语句块中经过malloc分配过内存的指针 str,而是程序首行定义的 char *str = NULL;此时 str为NULL,所以访问str[0]出现coredump

▪正确代码

char *str = NULL;

  if(!str) {

   str = (char*) malloc (100);

   if(!str) {

    return -1;

  }

   str[0] = 'a';

 }

printf("%c\n", str[0]);

▪代码示例

int func(int* pRes)

{

  if(pRes == NULL)

   pRes = new int(12);//分配新的内存空间给指针pRes,并赋值

  return 0;

}

int main ()

{

  int *pInt = NULL;

  int val = func(pInt);

 printf("%d\n",*pInt);

  return 0;

}

▪现象&结果

函数返回后,指针pRes所指内容不是12。

▪Bug分析

上述代码中 func函数的形参是指针类型 int *pRes,在函数体中 new了一块内存并赋值12,将内存地址赋值给指针pRes。在main函数中,定义了指针pInt,调用func函数,把pInt作为参数传入func函数中。程序的本意是在func函数退出后,指针pInt所指的内容*pInt为12,但实际结果却不是。其原因是在func函数调用过程中,形参和实参的传递使用了值传递方式,这种情况下,形参变量在函数体内发生了变化,在函数结束之后,形参变量随之释放,不能把变化的结果返回给实参。要改变实参的值,必须使用指针传递或者引用传递。在本程序中,func函数的形参是整形指针类型 int * pRes,要在函数体内改变 pRes的值,并把这个变化返回到main函数中,必须传递pRes的指针。因为pRes本身就是指针,所以应该传递指针的指针,或者指针的引用。

▪正确代码

int func(int *&pRes);

▪代码示例

int main()

{

  int a = 10;

  int* num = &a;

 function_b(num);

 printf("%d", *num);

  return 0;

}

void function_b(int* num)

{

  int b = 20;

  int* buf =&b;

  if(*num < 0)

   num = num;

 else

   num = buf;

  }

▪现象&结果

num指针指向的内容没有发生变化。

▪Bug分析

程序的本意是定义并初始化指针 num,然后通过函数 function_b,改变指针所指的值,即*num。但是实际上没有改变,问题出在function_b中,赋值的用法不正确。应该使用取内容运算符为指针所指的内容赋值,而不是直接为指针赋值。*num=*buf 的含义是把指针 buf 所指的 value 赋值给指针 num 所指的 value,而num=buf的含义是把指针buf的地址赋给指针num。

▪正确代码

int main()

{

  int a = 10;

  int* num = &a;

 function_b(num);

 printf(”%d”, *num);

  return 0;

}

void function_b(int* num)

{

  int b = 20;

  int* buf = &b;

  if(*num < 0)

  *num = *num;

 else

  *num = *buf;

}

▪编程建议

函数中传递指针或引用参数,要注意修改是指针本身还是指针的内容,若不希望改变指针本身,建议加 const声明,如:function_b(int * const num)。

▪代码示例

class MyClass{

private:

  int m_val;

public:

  MyClass(int value){

   m_val = value;

   printf("new class A\n");

 }

  void getValue(){

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

 }

};

void function_a()

{

  MyClass *A = new MyClass (1);

 function_b(A);

 function_c(A);

}

void function_b(MyClass *& A)

{

  MyClass *B = new MyClass(2);

 A->getValue();

 B->getValue();

  delete A;

  A = NULL;

  delete B;

  B = NULL;}

void function_c(MyClass *&A){

 A->getValue();

}

int main()

{

 function_a();

  return 0;

}

▪现象&结果

程序运行时出现coredump。

▪Bug分析

上述代码中,在函数function_a中实例化了MyClass对象指针A,然后调用函数function_b,在function_b中,对指针A操作之后,执行delete操作,将A所指对象释放掉。随后,function_a调用函数function_c,在function_c中又再次操作指针A。

此时,指针A为空,因此,当调用A→getValue时发生coredump。这里存在的一个问题是编码风格不好,对象的分配使用释放混乱。

一个函数分配,一个函数释放,一个函数再次使用。导致function_c不知道前面A已经被释放。避免这种问题的一个办法是:编码时遵循“谁分配,谁释放”的原则。即对象在哪里分配,就在哪里释放。

▪正确代码

void function_a()

{

  MyClass *A = new MyClass (1);

 function_b(A);

 function_c(A);

  delete A;

  A = NULL;

}

void function_b(MyClass *& A)

{

  MyClass *B = new MyClass (2);

 A->getValue();

 B->getValue();

  delete B;

  B = NULL;

}

void function_c(MyClass *&A){

A->getValue();

}

int main()

{

 function_a();

  return 0;

}

▪代码示例

unsigned char* Func(void)

{

  unsigned char *stra;

  stra = (unsigned char *)malloc(10);

  return stra;

}

int main()

{

  unsigned char *strb;

  strb = Func();

  strb = (unsigned char *)malloc(10);

 free(strb);

  return 0;

}

▪现象&后果

用一个指针指向两次动态分配的内存,但只 free 一次,造成内存泄露。使用cppcheck工具检测,可以得到类似下面的信息:(error) Memory leak: strb。

▪Bug分析

Func函数中申请了内存赋值给strb, 然后在main函数中又动态分配了内存赋值给strb。在free(strb)时,实际只是释放了最后一次动态申请的内存,Func函数中申请的内存被漏掉了。第一次申请的内存没有被释放,造成内存泄露。

▪正确代码

unsigned char* Func(void)

{

  unsigned char *stra;

  stra = (unsigned char *)malloc(10);

  return stra;

}

int main()

{

  unsigned char *strb;

  strb = Func();free(strb);

  strb = (unsigned char *)malloc(10);

 free(strb);

  return 0;

}

▪编程建议

这是一个小问题。两次动态分配的内存,但只free一次,造成内存泄露。记得申请释放内存时要注意malloc和free配对,申请几次释放几次。

▪代码示例

char *buff = new char[reslen];

delete buff;

▪现象&结果

申请的数组空间没有全部释放,造成内存泄露。用cppcheck工具执行静态代码扫描可以看到如下信息:(error) Mismatching allocation and deallocation: buf。

▪Bug分析

对于数组类型,如 string *str = new string[10],用delete str和 delete [] str的区别是,delete str只对 str[0]调用了析构函数,而 delete []str则对 str数组里的每个元素都调用了析构函数。对于单个元素,如 int *p =new int (10),delete和delete []都可以释放内存。

▪正确代码

char *buff = new char[reslen];

delete [] buff;

▪编程建议

操作内存的时候,new[]一定要和delete[]对应。

▪代码示例

int func(char* in, int inlen)

{

  char *p = new char[20];

  if ( inlen < 20 ) {

   return 0;

 }

  strncpy(p, in, 20);

  delete [] p;

  return 1;

}

▪现象&结果

若inlen<20,则函数中途退出,而未释放内存,导致内存泄露。

▪Bug分析

上述代码中,func函数内new了一块内存,在if条件分支中,没有释放内存,就直接 return 0,致使函数在条件分支中退出,导致了内存泄露。

▪正确代码

在程序退出前添加释放内存语句 delete [] p。

if ( inlen < 20 ) {

  delete [] p;

  return 0;

}

▪代码示例

int main()

{

  int **pVal = new int* [2];

   for(int i = 0; i < 2;i++){

    pVal[i] = new int[3];

  }

  delete [] pVal;

  return 0;

}

▪现象&结果

二维数组的释放,没有将每个元素逐一释放,造成内存泄露。使用valgrind检测工具检测,可以得到类似的信息,LEAK SUMMARY: definitely lost: 24 bytes in 2 blocks。

▪Bug分析

泄露点在 delete [] pVa,pVal是 2*3的二维数组指针, delete[] pVal只释放了pVal所指向的行空间,没有释放每个pVal[i]所指向的列空间。

▪正确代码

int main()

{

  int **pVal = new int* [2];

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

   pVal[i] = new int[3];

  for(int j = 0; j < 2; j++)

   delete [] pVal[j];

  delete [] pVal;

  return 0;

}

▪代码示例

char *initialize()

{

  char str[300];

  char* ptr = str;

  return ptr;

}

void useMyStr(char * str){

  char tmp[300] = "123";

  printf("%s\n", tmp);

  printf("%s\n", str);

}

int main()

{

  char *myStr = initialize();

 useMyStr(myStr);

  return 0;

}

 y 现象&后果

返回的指针将指向一个不确定内容的地址。

▪Bug分析

在函数initialize中的本地变量char数组分配的内存在栈上,随着函数的返回会被收回。正确的做法是:返回指针对应的内存块需要用函数malloc动态分配。

▪正确代码

char *initialize()

{

  char *myStr = (char*)malloc(300);

  return ptr;

}

void useMyStr(char * str){

  char tmp[300] = "123";

  printf("%s\n", tmp);

  printf("%s\n", str);

}

int main()

{

  char *myStr = initialize();

  if(myStr != NULL)

 useMyStr(myStr);

  delete myStr;

  return 0;

}

  return n;

▪代码示例

int &add(int n, int m)

{

  n = n + m;

}

int main()

{

  int i = 10;

  int b = add(i, 1);

  cout << b << endl;

  return 0;

}

▪现象&后果

预期的输出结果是11,但实际输出的结果不确定。

▪Bug分析

add函数是一个函数引用,而add函数的返回值是形参n,形参是函数内的局部变量,函数执行结束之后,函数内的局部变量就被销毁,内存空间就被收回。因此, add函数返回的内存单元中的值就不确定,b的内容就不确定。正确方法是:确保返回的内存空间不会随着函数的调用结束、被回收。

▪正确代码

int &add(int &n, int m)

{

  n = n + m;

  return n;

}

int main()

{

  int i = 10;

  int b = add(i, 1);

  cout << b << endl;

  return 0;

}

▪代码示例0

void func(const int* pInt, size_t size){

  size = *pInt;

  cout << "size:" << size << endl;

}

int main(){

 vector<int>veclnt;

  func(&veclnt[0], 4);

  return 0;

}

▪现象&后果

程序运行时,产生 core dump。

▪Bug分析

在程序中,vector<int>veclnt定义了一个没有初始化的vector变量veclnt,因此, veclnt只是一个没有内存空间的空vector对象。调用函数func时,&vecInt作为参数,此时就产生了一个不存在的指针。因此,在函数func内对其进行取值操作时,导致程序 core dump。

▪正确代码

int main(){

int func(const int* pInt, size_t size)

{

  if (pInt == NULL) return -1;

}

▪编程建议

使用指针前需要判断指针是否为NULL,避免空指针导致的程序异常。

▪代码示例

struct{

  char flag;

  int i;

} foo;

int main()

{

  foo.flag = 'T';

  int *pi = (int *)(&foo.flag + 1);

 *pi = 0x01020304;

  printf("flag=%c, i=%x\n", foo.flag, foo.i);

  return 0;

}

▪现象&后果

代码中定义了一个结构体,包括一个字符成员flag和整型成员i。在main函数中想通过指针方式将结构体整型成员i赋值为0x01020304,但打印输出显示i的实际值为0x01,赋值错误。

▪Bug分析

上面程序的问题出在指针赋值处,即 int *pi = (int *)(&foo.flag+1)。程序员误以为结构体字符成员flag地址加1就是整型成员i的地址,然后给该地址赋值,期望变量i会得到相应的赋值。但赋值结果并非所期望的。导致这个问题的根源是内存字节对齐。

内存字节对齐是指,为了保证CPU对内存的访问效率,各种类型数据需要按照一定的规则在内存存放,而不是完全字节挨字节的顺序存放。每种数据类型的默认对齐长度依赖于编译器具体实现,不同编译器可能有所不同。大多数情况下,基本数据类型的对齐长度就是自己数据类型所占空间大小(sizeof 值)。例如,char 型占一个字节,那么对齐长度就是一个字节;int型占4个字节,对齐长度就是4个字节,double型占8个字节,对齐长度就是8个字节。

对于结构体数据类型,默认的字节对齐一般需满足3个准则。

(1)结构体变量的首地址能够被其最宽数据类型成员的大小整除。

(2)结构体每个成员相对结构体首地址的偏移量都是该成员本身大小的整数倍,如有需要会在成员之间填充字节。

(3)结构体变量所占总空间的大小必定是最宽数据类型大小的整数倍。如有需要会在最后一个成员末尾填充若干字节,使得结构体所占空间大小是最宽数据类型大小的整数倍。

在结构体foo里,整型成员i占用4个字节,是占用空间最多的成员,所以foo必须驻留在4的整数倍内存地址。字符成员flag的起始地址即为foo的起始地址, flag占用1个字节。整型成员i的起始地址因为必须是4的整数倍,所以不能直接存放于flag+1的位置(flag已占用1个字节,flag+1地址不再是4的整数倍),而是存放于flag+4的位置。因此,flag后面的有3个字节浪费掉了。这样foo一共需要占用8个字节的内存空间,而不是5个字节(char型和int型的sizeof和)。

程序中,给flag+1地址处赋值为一个4字节整数0x01020304,因为有3个字节并未影响到变量i,所以赋值结果为0x01。

▪正确代码

不使用flag地址加1给变量i赋值,直接使用i的地址赋值。

struct{

  char flag;

  int i;

} foo;

int main()

{

  foo.flag = 'T';

  int *pi = &foo.i;

 *pi = 0x01020304;

  printf("flag=%c, i=%x\n", foo.flag, foo.i);

  return 0;

}

▪编程建议

字节对齐的细节与具体编译器实现有关,不同的平台可能有所不同。一些编译器允许程序员在代码中通过预处理指令 #pragma pack(n)或类型属性__attribute__((packed))来改变默认的内存对齐条件。

▪代码示例

int main()

{

  string str1("stack-allocated str1");

  string str2 = "stack-allocated str2";

  string* str3 = new string("heap-allocated str3");

  return 0;

}

▪现象&后果

程序在运行时发生内存泄露。

▪Bug分析

程序中使用了string对象的不同初始化或生成方式,容易让人迷惑,先解释一下。

str1是string对象的显式初始化,调用string类的构造函数string(constchar* s )初始化。

str2是复制初始化,会首先生成一个临时string对象,该临时对象以所赋值字符串为输入,调用 string ( const char * s )构造函数生成。然后以该临时对象的引用为参数调用string类的复制构造函数初始化str2。因为str1和str2对象都是在main函数体内声明的,所以都是分配在栈上。

str3是一个string对象指针,指向一个由new操作符生成的string对象。由于是由new生成的,所以该对象分配于堆上。

上述代码混淆了string对象的用法,不知道该什么时候调用delete释放对象。C++中没有垃圾回收机制,申请动态内存空间后,使用完后必须释放掉,否则会引起内存泄露。

那什么时候必须自己显式调用delete语句呢? 答案是,如果对象是在栈上分配的,不需要人工处理,当超出对象作用范围时,该对象的析构函数会自动被调用以释放该对象;如果对象是使用new操作符在堆上分配的,则必须使用delete操作符释放该对象。

上面代码中str1和str2都是在栈上分配的局部变量,所以,会在程序退出main函数前被自动析构。而str3是由new操作符在堆上分配的,必须使用delete操作符来释放。

▪正确代码

int main()

{

  string str1("stack-allocated str1");

  string str2 = "stack-allocated str2";

  string* str3 = new string("heap-allocated str3");

  delete str3;

  return 0;

}

C/C++内存使用是一个深入的话题。内存使用是对内存申请、读写、释放过程的安排与统筹,从而实现内存的正确、高效使用。本章主要从正确性上讲解了内存使用的全过程,包含了常见的错误案例。希望通过本章节的内容,使读者对C/C++内存使用有正确的了解和认识。

图书在版编目(CIP)数据

从缺陷中学习C/C++/刘新浙等编著.--北京:人民邮电出版社,2013.9

ISBN 978-7-115-32159-6

Ⅰ.①从… Ⅱ.①刘… Ⅲ.①C语言—程序设计 Ⅳ.①TP312

中国版本图书馆CIP数据核字(2013)第151124号

内容提要

C/C++是广泛用于系统和应用软件开发的语言,也是使用最为广泛的编程语言。C/C++易学难用,尤其C++,经过20多年的发展,已经变得非常复杂,给编程人员带来了很大的挑战。那么如何在工程项目中用好C/C++语言、如何绕过Bug构建稳定可靠的生产系统、如何以最快速度全面了解 C/C++编程中的陷阱和障碍,编写出健壮可靠的代码呢?本书将通过 102 个案例,帮助程序员尽快从新手成长为专家。案例涵盖基础问题、编译问题、库函数问题、文件处理、类和对象、内存使用、多线程问题、性能问题等。读者每掌握一个案例就掌握了一个或几个知识点,就能避免一类问题。由于是从大量编程中总结出来的具体Bug案例中学习,这种学习方式更直接,让人印象更深刻。本书将为你成为C和C++高手、编写出完美的程序助一臂之力。

本书适合程序员、测试人员以及C和C++初学者使用,也可以作为各大专院校和培训学校的教学用书。

◆编著 刘新浙 刘玲 王超 李敬娜 等

责任编辑 张涛

责任印制 程彦红 焦志炜

◆人民邮电出版社出版发行  北京市崇文区夕照寺街14号

邮编 100061  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

北京鑫正大印刷有限公司印刷

◆开本:720×960 1/16

印张:11.25

字数:176千字  2013年9月第1版

印数:1-3500册  2013年9月北京第1次印刷

定价:39.00元

读者服务热线:(010)67132692 印装质量热线:(010)67129223

反盗版热线:(010)67171154

相关图书

代码审计——C/C++实践
代码审计——C/C++实践
CMake构建实战:项目开发卷
CMake构建实战:项目开发卷
C++ Templates(第2版)中文版
C++ Templates(第2版)中文版
C/C++代码调试的艺术(第2版)
C/C++代码调试的艺术(第2版)
计算机图形学编程(使用OpenGL和C++)(第2版)
计算机图形学编程(使用OpenGL和C++)(第2版)
Qt 6 C++开发指南
Qt 6 C++开发指南

相关文章

相关课程