C++中delete和delete[]的深层区别

今天又看到群里有人讨论 C++deletedelete[] 的区别,表层原因大家都了解,因为教科书上说得很明白:newdelete 需配对使用, new[]delete[] 需配对使用。

但若问起在什么情况下针对 new[] 申请的资源可以使用 delete 释放而不会有任何问题,能讲清楚这点的人就很少了。因为这涉及到对 newdeletenew[]delete[] 内部实现机制的理解。

根本原因在于, delete 需要调用内存中一个元素的析构函数,而 delete[] 需要调用内存中若干个元素的析构函数,这里就牵涉出一个问题—— delete[] 是如何知道内存中元素的数量的?我们知道 delete[] 中的 [] 并不会传入参数,所以这个数量不会是 delete[] 传过来的,而是在 new[] 的时候保存的,只有这样才得以在 delete[] 的时候依据元素数量逐个调用析构函数。

接下来说 new[] 如何存储这个数量,首先它会动态申请一段内存,然后在这段内存的首地址空间中存入元素数量,在这个空间之后的内存分配给各元素,new[] 的返回值并不是这段动态内存空间的首地址,而是动态内存空间中存放第一个元素的内存地址。

以上说的是 delete[] 需要调用元素析构函数的情况,但是C++的哲学是 Zero-cost Abstraction,所以对于并没有显式定义析构函数的 struct/class 的对象元素来说,并不需要为其产生析构函数的代码,也就不需要在 delete[] 的时候调用元素的析构函数以增加无谓的运算开销,那么, new[] 也就不用存储这个元素数量。还有一种情况就是如 int 等基本类型作为空间元素的时候,也不存在析构函数的调用,所以跟没有显示定义析构函数的对象元素一样:在 new[] 时候不需要存储元素数量,在 delete[] 时候不需要调用析构函数。

综上所述, new[]delete[] 的具体行为受对象元素是否存在必须调用析构函数而有所不同。

一图胜千言,我画了三张图来展现上面说的三种元素情况:

  • int 作为基本类型:

int *ptr = new int[5]

  • 定义了一个 class A ,但是 A 并没有显式定义析构函数:

A *ptr = new A[5]

  • 定义了一个 class B,并且 B 显式定义了析构函数:

B *ptr = new B[5]

可以看出,对于 int *ptr = new int[5]A *ptr = new A[5] ,因为不涉及存储元素数量和对析构函数的调用,所以 deletedelete[] 的操作都仅仅是将传入的地址进行释放而不做其他额外事情。这种情况下,你使用 delete 或者 delete[] 都不会存在任何问题。

但是对于 B *ptr = new B[5] 却一定要使用 delete[] ,因为传过来的并不是真正的动态内存首地址, delete[] 的内部处理就会变成从传入的内存地址往前偏移获取真正的动态内存首地址,从该首地址空间获取到元素的数量,然后通过数量逐个调用元素的析构函数,完了再用得到的内存首地址释放动态内存。但若使用 delete 就会只调用第一个元素的析构函数,并且将第一个元素的地址作为动态内存首地址进行释放,但是释放错误的内存地址(非申请时候动态内存的首地址)将发生严重错误,如在 visual studio 中会直接触发程序异常并崩溃。

接下来思考另一种情况,如果 B *ptr = new B 操作后使用 delete[] 释放呢?这也会产生非常严重的错误,因为它会根据这个内存地址往前偏移获取数量,但是这个数量值是个不确定的值,所以接下来发生的行为就是在指针越界访问的情况下调用了无数次析构函数,而这些内存空间中并不存在有效元素,该行为将发生程序崩溃,即便该过程程序照常执行,接下来用偏移地址释放内存也会崩溃,总之,程序执行到此已经走火入魔了。