0%

[Effective C++ 笔记]条款 16. 成对使用 new 和 delete 时要采取相同形式

简介

  • 在调用 delete 时使用和当初构造时使用的 new 相同的形式

引子

考虑以下例子:

1
2
std::string* stringArray = new std::string[100];
delete stringArray;

虽然我们成对的使用了 newdelete,但很遗憾这段代码还是完全错误的。因为我们动态分配了一个长度为 100 的数组,但是在清理内存时我们只使用了 delete,这会导致不明确行为。(在我的机器下,编译和运行结果如下,完整代码见 ex1

1
2
3
4
5
6
7
8
9
10
11
# 编译会发出警告,但不会报错
main.cpp:21:5: warning: 'delete' applied to a pointer that was allocated with 'new[]'; did you mean 'delete[]'? [-Wmismatched-new-delete]
delete stringArray;
^
[]
main.cpp:20:32: note: allocated with 'new[]' here
std::string* stringArray = new std::string[100];
# 运行时调用 delete 时会报错
main.out(11825,0x11c75fdc0) malloc: *** error for object 0x7fed58808808: pointer being freed was not allocated
main.out(11825,0x11c75fdc0) malloc: *** set a breakpoint in malloc_error_break to debug
[1] 11825 abort ./main.out

使用 new 和 delete 时发生的操作

当我们通过 new 来动态生成一个对象时,通常会发生以下两件事情:

  • 要求空间大小的内存通过 operator new 被分配出来
  • 针对被分配出来的部分内存,一个或多个构造函数被调用

而当我们通过 delete 删除一个对象时,相应地也有两种事情发生:

  • 一个或多个析构函数被调用
  • 相应的内存通过 operator delete 被释放

因此,这里我们可以看到一个问题,在调用 new 的时候,我们很自然可以知道有多少个构造函数会被调用(如果生成的一个对象则调用一次,如果生成数组则按照数组长度调用)。但是在我们调用 delete 时,我们(或者说程序)怎么知道需要调用多少次析构函数呢?通常来说,假如我们知道要对一个数组进行删除的话,我们是可以知道这个数组内包含多少个对象的,因为数组和单一对象的内存布局有些许不同,下面是一种可能的布局方式。

  • 单一对象:

|object|

  • 数组:

|n|object1|object2|...|objectn|

相对于单一对象而言,数组除了存储了相应数量的对象之外,还会有额外的参数存储数组中对象的个数。所以如果我们让 delete 知道操作的对象是数组,它就可以去读取数组中存储对象个数的变量从而知道调用多少次析构函数。而这么做的方法是在对数组进行清理时,调用 delete[] 而非 delete

那么假如我们对一个单一对象调用 delete[] 呢?同样会导致未定义行为。(编译和运行的情况和上例类似,完整代码见 ex2),考虑上面提到的内存布局,如果我们对单一对象调用 delete[] 程序会尝试读取一定字节作为数组长度变量,因此可能会有不好的后果(对一片不属于对象的空间调用析构函数等等)。因此,我们需要在调用 delete 时采用和调用 new 相同的类型。

需要额外注意的情况

  • 对于在类中进行动态分配时,我们需要对所有构造函数采取相同形式的 new 方法,否则在析构时,我们没办法知道应该调用哪种 delete
  • 应该尽量避免对数组使用 typedef 否则,在调用 delete 时很容易出现混淆(本该调用 delete[] 却调用了 delete),如果可以的话采用 std::vector 等标准容器。

结论