🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
内存分配和释放几乎是所有程序的基本需求,同时也是最常出问题的地方之一,有谁没有听说过内存泄漏问题呢?通过遵循以下几条简单的规则,你可以避免很多常见的内存分配问题。 原则1: 谁申请谁释放 说明 原则上,哪个对象、函数申请的系统堆内存,在用完后应由原申请者释放,所谓“谁借的谁还”。 但这个原则说起来容易,真正要完全做到却很麻烦。比如经常遇到这样的情况:程序内部要传递一些信息,这些信息一般都是需要时产生,用完消除,所以应该存放在系统堆内存中。但是这样就导致该内存的产生者必须知道什么时候所有人都不再使用该内存,即需要回收了。这可不是一句两句能实现的,拿捏不好就会发生回收之后又有人要用、或所有人都不再用了却没有及时回收的情况。 当然,传递也可以用内存拷贝方式(而不是上述的传递指针或引用方式),这样,就不存在回收的问题了。但当信息很大或引用者很多时,拷贝的代价就成了问题。这引出另一个有名的计算机问题,即程序运行时,数据像波浪一样从内存中的一处移动到另一处,可以想象,这种移动毫无意义,肯定不是好的解决方案。 原则2: 当对象消亡时确保指针成员指向的系统堆内存全部被释放 说明 否则会造成内存泄漏。 原则3:malloc、free, new、delete,new[ ]、delete[ ]要成对使用 说明 1) 调用new所包含的动作: 从系统堆中申请恰当的一块内存。 若是对象,调用相应类的构造函数,并以刚申请的内存地址作为this参数。 2) 调用new[n]所包含的动作: 从系统堆中申请可容纳n个对象外加一个整型的一块内存。 将n记录在额外的那个整型内存中(其位置依赖于不同的实现,有的在申请内存块开头,有的在末尾)。 调用n次构造函数初始化这块内存中的n个连续对象。 3) 调用delete所包含的动作: 若是对象,调用相应类的析构函数(在delete参数所指的内存处)。 将该内存返还系统堆。 4) 调用delete[]所包含的动作: 从new[]记录n的地方将n值找出。 调用n次析构函数析构这块内存中的n个连续对象。 将这一整块内存(包括记录n的整型)归还系统堆。 可以看出,分配和释放单个元素与数组的操作不同,这就是为什么一定要成对使用这些操作符的原因。 原则4: 确保所有new(malloc)出来的东西适时被delete(free)掉 说明 不需要的内存不能被及时释放(回系统),就是大家常听到的内存泄漏(memory1eak)。狭义的内存泄漏是指分配的内存不再使用,但却永远不被释放。从更广的意义上说,没有及时释放也是内存泄漏,只是程度较轻而已。 内存泄漏不管有多小,最终都会耗尽所有内存(只要运行的时间足够长)。内存泄漏的问题极难查找,因为当出现问题时,内存己经耗尽,此时CPU运行什么程序(哪怕是系统程序),什么程序就崩溃。可以看出,崩溃时报告的出错信息与引起问题的代码毫无关联。另外内存耗尽的时间是不确定的,且一般会是一个较长的时间(几天或几星期都可能),这就更增加了再现和定位问题的难度。 目前有一些工具可以帮助查找内存泄漏问题,比如Rational公司的RMfy。这些工具大大提高了查找内存泄漏的能力。但防患于未然才是治本之道。 原则5: 当所指的内存释放后,指针应有一个合理的值 说明 除非该指针变量本身将要消失(out of scope),否则应置为空(NULL)。 例子 void Channel_T::disconnect (void) { delete i_pConnection; // 释放资源 i_pConnection=NULL; // 指针指向一个合理的值 //… / * * 注意:delete不会也不可能将i_Connection置成NULL, *因为delete掉的是i_Connection指向的一片内存空间 *而不是它本身 */ } 原则6: 记住给字符串结束符申请空间 例子 pName=new char[strLen+1]; // +1是为了给“\0”留出空间 说明 字符串有隐含的结束符“\0”,申请空间时别忘了。 这种数组越界的错误很难查。因为一方面越界的概率一般很小,不好再现。另一方面,即使越界,也不一定导致程序出错,因为只有越界后要访问其他进程的内存空间时,系统才意识到有问题。或者越界修改自己的其他内存值而造成严重问题,比如导致该值成为非法,否则程序还能向前推进。越界后是否出错还跟编译器或编译时的参数设定有关,比如有的编译器或参数设定(如打开优化功能)会要求内存变量一个紧挨一个排列。另一些却选择诸如字对齐方式,这会留下一些空隙,如果越界正好落在空隙中,就什么问题也没有。所以,有时发现,调试版(打开debug编译选项)不出问题,优化版总出问题。