乐趣区

【C++】一种典型隐秘的多次delete的情况

本文介绍分析一种多次 delete 动态内存的情况。说是典型,是因为这个问题已经在我两个同事身上发生过;说是隐秘,是因为一旦发生问题,靠肉眼很难确定原因。
预备知识
不同于 C 语言通过 malloc 和 free 等函数实现动态内存的分配和释放,C++ 引入了 new 和 delete 运算符实现。基本的用法如下:
int* p = new int;
delete p;
如果上述代码的 p 多次释放会出现什么情况呢?
int* p = new int;
delete p;
delete p;
很明显会引起程序崩溃,这是我本地执行的错误信息,错误提示也给出了 double free 的字样,告诉我们这可能是两次释放导致的问题。
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 空指针是不出错的,所以执行不会出错。
基于工程上的考虑,delete 一个指针后,要把指针赋值为空指针,借此提高代码健壮性。但这往往会掩盖问题,使得问题查找更难,比如上述问题,我们应该去分析为什么两次 delete,而不是通过 p = nullptr; 暂时掩盖问题。
上面情况更好的解决方式是使用智能指针或 RAII,尽量在高层代码里不混杂底层逻辑。在此不絮叨了,进入下一部分。
问题
将问题场景简化为以下的例子:
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= 无效地址},由于 list 与 ls 共享 elements,所以 main 中的 elements 变量的值为 {count=5, elements= 无效地址 y}。(4)main 函数体结束 list 也即将超出作用域,编译器调用 list 的析构函数,由于其 elements 已经被 delete 了,再释放一次会出现内存错误,导致程序终止。
那怎么修正这个问题呢?只要将 processList 的 ls 改为引用传参即可,引用传参仅将使用权传给函数,所有权还属于原对象,所以不会执行析构函数。
void processList(List& ls) {

}
当然,还有一种方式是改写重载赋值和拷贝构造函数,实现“深拷贝”,这样也能解决问题。标准库中的 string 和 vector 是较常用的类,所以本身实现了深拷贝,所以不会出现以上问题。在包含的元素较多时,需要考虑性能问题。
C++ 的零成本抽象,带来性能优势的同时,一定也要细细考虑其带来的复杂成本。比如,GC 用爽的人很可能即会犯以上的错误,往往不是因为不知道,而是因为相关意识不强烈。所以,一定要小心,小心,再小心。
请关注我的公众号哦。

退出移动版