本文介绍分析一种屡次delete动态内存的状况。说是典型,是由于这个问题已经在我两个同事身上发生过;说是隐秘,是由于一旦发生问题,靠肉眼很难肯定缘由。linux
不一样于C语言经过malloc
和free
等函数实现动态内存的分配和释放,C++引入了new
和delete
运算符实现。基本的用法以下:函数
int* p = new int; delete p;
若是上述代码的p
屡次释放会出现什么状况呢?性能
int* p = new int; delete p; delete p;
很明显会引发程序崩溃,这是我本地执行的错误信息,错误提示也给出了double free
的字样,告诉咱们这多是两次释放致使的问题。spa
double free or corruption (fasttop): 0x0000000001029c20 *
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x70bfb)[0x7f6469dbcbfb]
/lib/x86_64-linux-gnu/libc.so.6(+0x76fc6)[0x7f6469dc2fc6]
/lib/x86_64-linux-gnu/libc.so.6(+0x7780e)[0x7f6469dc380e]
...
这种状况有没有简单的规避方式吗?我看到好多人这么写:指针
int* p = new int; delete p; p = nullptr; //或 p = NULL; delete p;
因为C++中delete
空指针是不出错的,因此执行不会出错。code
基于工程上的考虑,delete
一个指针后,要把指针赋值为空指针,借此提升代码健壮性。但这每每会掩盖问题,使得问题查找更难,好比上述问题,咱们应该去分析为何两次delete
,而不是经过p = nullptr;
暂时掩盖问题。对象
上面状况更好的解决方式是使用智能指针或RAII
,尽可能在高层代码里不混杂底层逻辑。在此不絮叨了,进入下一部分。ip
将问题场景简化为如下的例子:内存
class List { public: List() { count = 0; elements = nullptr; } //空列表 List(int num) : count(num), elements(new int[num]) {} //num个元素的列表 ~List() { count = 0; delete [] elements; } private: int *elements;//元素的首地址 int count; //元素的个数 }; void processList(List ls) { } int main() { List list(5); processList(list); }
问题的根源出在C++默认的拷贝是“浅拷贝”,即只拷贝当前变量类型所占用的内存。下面按代码执行步骤分析:
(1)List list(5);
创建一个对象,该对象{count=5, elements=x}
(2)传入processList
因为是浅拷贝,此时ls
变量的值为{count=5, elements=x}
,即与main
中的list
共用elements
。
(3)processList
函数体结束
因为ls
对象即将超出做用域,编译器会调用ls
的析构函数,此时ls
变量的值为{count=0, elements=无效地址x}
,因为list
与ls
共享elements
,因此main中的elements
变量的值为{count=5, elements=无效地址x}
。
(4)main
函数体结束list
也即将超出做用域,编译器调用list
的析构函数,因为其elements
已经被delete
了,再释放一次会出现内存错误,致使程序终止。element
那怎么修正这个问题呢?只要将processList
的ls
改成引用传参便可,引用传参仅将使用权
传给函数,全部权还属于原对象,因此不会执行析构函数。
void processList(List& ls) { }
固然,还有一种方式是改写重载赋值和拷贝构造函数,实现“深拷贝”,这样也能解决问题。标准库中的string
和vector
是较经常使用的类,因此自己实现了深拷贝,因此不会出现以上问题。在包含的元素较多时,须要考虑性能问题。
C++的零成本抽象,带来性能优点的同时,必定也要细细考虑其带来的复杂成本。好比,GC 用爽的人极可能即会犯以上的错误,每每不是由于不知道,而是由于相关意识不强烈。因此,必定要当心,当心,再当心。
请关注个人公众号哦。