引子:delete 与 delete[] 没那么简单
表面上看,delete 和 delete[] 的区别很简单:前者删除一个 new 出来的对象,后者删除一个对象数组。但这两者背后其实还牵涉到另一层关系——基本类型与非基本类型的差异。
对于 int、char 这类基本数据类型的数组来说,delete 与 delete[] 的效果是等效的。这个结论听起来有点玄,和课堂上讲的"数组必须用 delete[]"并不一致,下面通过实验一步步验证。
一、基本数据类型数组的实验
通常我们认为 delete[] 是专门用来删除数组的。先看下面这段简单的代码:
、、
auto array = new int[2] ;
针对这段分配,我们分别用两种方式检测内存泄露:
- 用 Dr Memory 检测,泄露 8 bytes。
- 用
_CrtDumpMemoryLeaks()检测,同样是 8 bytes,通过宏_CRTDBG_MAP_ALLOC可以进一步限定具体信息。
以上结论都是早就知道的,真正要验证的是 delete 和 delete[] 在这种情况下到底有没有区别:
- A. 使用
delete:Dr Memory 没有检测到泄露,只是给出一条警告(invalid heap argument);_CrtDumpMemoryLeaks()也没有检测到泄露。 - B. 使用
delete[]:肯定是没有任何问题的。
也就是说,对于基本数据类型数组,使用 delete 虽然不规范、会触发警告,但实际上并没有造成内存泄露。
二、对象数组的实验
接下来换成非基本类型——一个带析构函数的类 A,这才是本文真正要讨论的问题所在:
、、
static int count1 = 5;
class A
{
public:
int x = count1;
~A()
{
cout << "~A" << endl;
}
A()
{
count1++;
}
};
int main(int argc, char *argv[])
{
auto array = new A[2];
//delete array;
_CrtDumpMemoryLeaks();
system("pause");
return 0;
}
两种内存泄露检测工具都显示泄露了 12 bytes。而 sizeof(A) 的结果是 4 字节,两个对象加起来应当是 8 字节,那多出来的 4 字节是哪里来的?经过反复测试后可以确认,这多出的 4 字节是编译器额外分配的、用来记录对象个数的变量,类型应当是 unsigned int。
三、定位首地址看内存布局
把断点定位到 array 的首地址,观察内存信息(原文配图因源站返回 400 已缺失)。
由于 Intel X86 是小端模式,从低地址到高地址依次是 2、5、6;而 array 指向的正是 5 所在的地址。由此可以看出:编译器在真正的对象数据之前,多分配了 4 个字节用来存储对象个数。
为了让这块额外分配的内存也能被完整归还给 heap,就必须使用 delete[] 来删除——使用 delete 是无法正确释放这 4 个字节的。
通过断点调试 delete[] 执行前后的内存状态,也可以证实它会依次释放 2、5、6 这三个值所在的内存。release 模式下同样验证了这一行为。
四、CRT 源码与析构函数的关系
进一步查看 VC 提供的源代码可以发现,dbgdel.cpp 和 dbgdel2.cpp 中就包含了 delete 和 delete[] 等运算符的 CRT 实现源码,可以直接对照源码来理解上述行为。
一个有意思的补充实验:如果类 A 不带析构函数,那么上述多分配 4 字节的问题就不会出现——也就是说,额外记录对象个数这件事,是为了在 delete[] 时能够正确调用每一个对象的析构函数。
结论
- 对于非基本类型的对象数组,删除时必须使用
delete[]。 - 对于基本数据类型的数组,
delete和delete[]都能正确释放内存,但为了保持记忆和写法的一致性,建议仍然统一使用delete[]。