当前位置:   article > 正文

编写高质量代码:改善C++程序的150个建议(十四)_编写高质量代码 150个建议 十四

编写高质量代码 150个建议 十四

第3章 说一说“内存管理”的那点事儿

  在C++的世界里,“烫”和“屯”是我们遇到得最多的两个汉字(限于VC用户)。可能有人不禁要问:这是为什么呢?

  答案是:在VC中,栈空间未初始化的字符默认是-52,补码是0xCC。两个0xCC ,即0xCCCC在GBK编码中就是“烫”;堆空间未初始化的字符默认是-51,两个-51在GBK编码中就是“屯”。 二者都是未初始化的内存。

  C++赋予了我们直接面对内存、操作内存的能力,但是内存管理却一直以来被认为是C++语言的一大难点。因为在C++语言中,缺少GC(垃圾回收器),内存管理需要程序员手动完成,并且还要为可能的失误承担后果。

  正如下面的“代码故事”:

  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. /*  
  4.    在经历过无数的"烫烫烫烫烫","屯屯屯屯屯屯"之后,  
  5.    我们都知道了:内存原来是可以驾驭的...  
  6. */  
  7. int main()  
  8. {  
  9.    /*  
  10.       原来内存管理是这样的,即便结果完美无缺,  
  11.       但却危机四伏...  
  12.    */  
  13.    const char *src="Hello Csdn!";  
  14.    char *dest=(char*) malloc(strlen(src));  
  15.    memcpy(dest,src,strlen(src)+1);  
  16.    printf("%s\n",dest);  
  17.    return 0;  
  18. }  
  19. /*  
  20.   代码结束了,故事也到此为止。但是我们要做的还很多。  
  21.   希望我们能少遇到一点烫和屯...  
  22. */

  所以,我们要说说内存管理那点事儿,争取早日练就内存管理的高深技艺。

  建议27:区分内存分配的方式

  在C/C++语言中,用内存管理的水平去划分高手与菜鸟已经成为一种不成文的约定:可以从中获得更好的性能、更大自由的被称作C++高手,而程序经常面临着莫名其妙的崩溃,一遍遍的调试,费时又费力的则可能是菜鸟级别的。而这一切都源于那让人又爱又恨的C++内存管理的灵活性。其中,多样的内存分配方式就是其灵活性的最好例证之一。

  一个程序要运行,就必须先将可执行的程序加载到计算机内存里,程序加载完毕后,就可以形成一个运行空间,并按照图3-1所示的那样进行布局。

  代码区(Code Area)存放的是程序的执行代码;数据区(Data Area)存放的是全局数据、常量、静态变量等;堆区(Heap Area)存放的则是动态内存,供程序随机申请使用;而栈区(Stack Area)则存放着程序中所用到的局部数据。这些数据可以动态地反应程序中对函数的调用状态,通过其轨迹也可以研究其函数机制。其中,除了代码区不是我们能在代码中直接控制的,剩余三块都是我们编码过程中可以利用的。在C++中,数据区又被分成自由存储区、全局/静态存储区和常量存储区,再加上堆区、栈区,也就是说内存被分成了5个区。这5种不同的分区各有所长,适用于不同的情况。

  栈(Stack)区

  在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元将自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是所分配的内存容量有限。

  堆(Heap)区

  堆就是那些由new分配的内存块,其释放编译器不会管它,而是由我们的应用程序控制它,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统就会自动回收。

自由存储区

  自由存储区是那些由malloc等分配的内存块,它和堆十分相似,不过它是用free来结束自己生命的。

  全局/静态存储区

  全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有作此区分,它们共同占用同一块内存区。

  常量存储区

  这是一块比较特殊的存储区,里面存放的是常量,不允许修改。

  上述5种分区中,最常用的就是堆与栈,容易混淆的也是堆与栈。在BBS论坛里,几乎到处都能看到堆与栈的争论。堆与栈的区分问题,似乎是每一个C++程序员成长路上都会遇到的永恒话题。那么堆与栈之间到底有什么分别与联系呢?这就是接下来我要阐述的问题。

  首先,还是分析下面的代码片段:

  1. const int COUNT = 10;  
  2. void Function()  
  3. {  
  4.       string* pStr = new string[COUNT];  
  5. }

  你是否相信就这么简单的一个函数,它却涉及了5种内存分区中的3种呢?COUNT是一个常量,被安置在了常量存储区,不可修改;pStr是局部变量,理所应当地放入栈里;而通过new string[COUNT]获得的则是一块堆空间。多么精妙,多么不可思议!当然,上述代码片段只是一个示例,是经不起推敲的,因为它会引起内存泄露(缺少与new对应的delete去释放内存)。

  似乎脱离了主题,还是言归正传,说说堆与栈的区别。总的来说,二者的区别主要有以下几个方面:

  管理方式不同

  对于栈来讲,它是由编译器自动管理的,无须我们手工控制;对于堆来说,它的释放工作由程序员控制,容易产生memory leak。

  空间大小不同

  一般来讲在32位系统下,堆内存可以达到4GB的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定空间大小的。

  碎片问题

  对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而产生大量的碎片,使程序效率降低。对于栈来讲,则不存在这个问题,其原因还要从栈的特殊数据结构说起。栈是一个具有严明纪律的队列,其中的数据必须遵循先进后出的规则,相互之间紧密排列,绝不会留给其他数据可插入之空隙,所以永远都不可能有一个内存块从栈中间弹出,它们必须严格按照一定的顺序一一弹出。

  生长方向

  对于堆来讲,其生长方向是向上的,也就是向着内存地址增加的方向增长;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长的。

  分配方式

  堆都是动态分配的,没有静态分配的堆。栈有两种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数完成,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放的,无须我们手工实现。

分配效率

  栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:它会分配专门的寄存器存放栈的地址,而且压栈出栈都会有专门的指令来执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),则可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存了,然后返回。显然,堆的效率比栈要低得多。

  堆和栈相比,由于堆使用了大量new/delete,容易造成大量的内存碎片,而且它没有专门的系统支持,效率很低,另外它还可能引发用户态和核心态的切换,以及内存的申请,代价会变得很高。所以栈在程序中是应用最广泛的,就算是函数的调用也会利用栈去完成,函数调用过程中的参数、返回的地址、EBP和局部变量都是采用栈的方式存放的。所以,我们推荐大家尽量多用栈,而不是用堆。

  虽然栈有如此多的好处,但是由于和堆相比它不是那么灵活,有时候会分配大量的内存空间,在遇到这种情况时还是用堆好一些。

  请记住:

  内存分配具有多种不同的方式,它们各具特点,适用于不同的情形。所以,要在合适的地方采用合适的方式完成内存的分配。

  建议28:new/delete与new[]/delete[]必须配对使用

  operator new和operator delete函数有两个重载版本:

  1. void* operator new (size_t);       // allocate an object  
  2. void* operator new [] (size_t);    // allocate an array  
  3. void operator delete (void*);      // free an oject  
  4. void operator delete [] (void*);   // free an array

  熟悉C语言的朋友看到这里可能会很奇怪:在C语言中,无论申请的是单个对象,还是一个数组,管理内存所用的都是malloc/free,但是为什么到了C++里会出现两个呢?何况建议21中已经说明,new/delete在功能上比前者更加强劲。

  先分析以下代码片段存在的问题:

  1. class Test  
  2. {  
  3. public:  
  4.     Test() { cout << "ctor" << endl; }  
  5.     ~Test() { cout << "dtor" << endl; }  
  6.     Hello(){ cout << "Hello C++" << endl; }  
  7. };  
  8. int main()  
  9. {  
  10.     cout <<"Test 1:"<< endl;  
  11.       Test* p1 = new Test[3];  
  12.       delete p1;  
  13.  
  14.       cout <<"Test 2:"<< endl;  
  15.       Test* p2 = new Test;  
  16.       delete[] p2;  
  17.  
  18.       return 0;  
  19. }

  上述代码看起来井然有序:我们采纳了建议21,用new完成了内存申请,并且使用了与之对应的delete来释放内存。可是,执行结果却显示上述代码存在问题。在Test 1中,构造函数调用了3次,构造了3个Test类型对象,而在删除时,却只析构了一个对象p1[0]。在Test 2中,构造函数调用了一次,但是析构函数却被调用了多次,将本不属于该对象的空间当成该类型对象进行了清理。也许各大厂商意识到了这个问题,于是让编译器能够检测出这样的错误。所以在VC++2010中,如果出现上述情形,编译器就会给出“debug assertion failed”或“堆被损坏”的错误信息。

  C++告诉我们在回收用new分配的单个对象的内存空间时用delete,在回收用new[]分配的一组对象的内存空间时用delete[]。下面我们就分析一下它们的实现原理。

无论new还是new[],C++必须知道返回指针所指向的内存块的大小,否则它就不可能正确地释放掉这块内存,这一点很像C语言中的malloc。但是在用new[]为一个数组申请内存时,编译器还会悄悄地在内存中保存一个整数,用来表示数组中元素的个数。因为在delete一块内存时,我们不仅要知道指针指向多大的内存,更重要的是要知道指针指向的数组中对象的个数。因为只有知道了对象数量才能一一调用它们的析构函数,完成对数组中所有对象的清理。如果使用的是delete,则编译器只会将指针所指的对象当作单个对象来处理。所以对于数组,需要使用delete[]来处理;符号[]会告诉编译器在delete这块内存时,先去获取保存的那个元素数量值,然后再进行一一清理。如果你对汇编有所了解,那么你可以通过反汇编代码对此一探究竟。

  也许你会认为C++这么设计绝对是多此一举,因为单个对象只是对象数组的一个特例,无论是一个对象,还是对象数组,我们都对元素个数进行记录,这样也就不再需要两个版本的new和delete了。但是C++之父之所以没有选择这么做,也许是为了坚持他认定的C++设计风格和宗旨:决不多费一点力。殊不知,这么做的直接后果就是需要程序员付出更多的细心与努力。

  需要注意的是,由于内置数据类型没有构造、析构函数,所以在针对内置数据类型时,释放内存使用delete或delete[]的效果是一样的。例如:

  1. int *pArray = new int[10];  
  2. ... // processing code  
  3. delete pArray; //等同于delete[] pArray;

  虽然针对内置类型,delete和delete[]都能正确地释放所申请的内存空间,但是如果申请的是一个数组,建议还是使用delete[]形式。

  所以,使用new和delete的一个简单有效的原则就是:如果在调用new时使用了[],则你在调用delete时也使用[],如果在调用new的时候没有用[],那么也不应该在调用时使用[]。new和delete、new[]和delete[]必须对应着使用。

  对于那些喜欢typedef的人,还有一点需要提醒。因为在这种情况下很容易出现new[]和delete的混用。如下面的代码片段所示:

  1. typedef int scorers[LESSONS_NUM];  
  2. int *pScorer = new scorers;

  这该使用哪一种形式的delete呢?如下所示。

  1. delete pScorer; // Wrong!!!  
  2. delete[] pScorer; // Right

  为了避免出现这样的错误,建议不要对数组类型做typedef,或者采用STL中的vector代替数组。

  请记住:

  new和delete、new[]和delete[]必须对应使用,否则会出现未定义行为,导致程序崩溃。



声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/512179
推荐阅读
相关标签
  

闽ICP备14008679号