以下代码有什么问题?
std::string *stringArray = new std::string[100];
//...
delete stringArray;
每件事情看起来都井然有序。使用了new
,也搭配了对应的delete
。但还是有一点完全错误:程序的行为是未定义的。在最好的情况下,stringArray
所包含的100个string
对象中的99个不太可能被正确地回收,因为它们的析构函数很可能没有被调用。
当使用了new
的时候,有两件事情发生:
- 调用
operator new
分配内存。 - 对象的构造函数被调用。
当使用了delete
的时候,也有两件事情发生:
- 对象的析构函数被调用。
- 调用
operator delete
释放内存。
在delete
的过程当中,最大的问题是:即将被回收的内存中究竟存有多少个对象?这个问题的回答决定了有多少个析构函数必须被调用起来。
对于我们而言,实际上这个问题可以更简单些:即将被回收的那个指针,所指的是一个单一的对象,还是一个对象的数组?这是一个不可或缺的问题,因为单一对象的内存布局一般而言不同于数组的内存布局。更明确地说,数组所用的内存通常还包括“数组大小”的记录,以便于delete
知道需要调用多少次析构函数;而单一对象的内存则没有这笔记录。
可以简单地将两种内存排列方式理解如下(这是一种假设,用于帮助理解,但是很多编译器确实也是这么做的):
new
的内存排布
new[]
的内存排布
当我们对着一个指针使用delete
的时候,唯一能够让delete
知道内存中是否存在一个“数组大小记录”的方法就是:我们自己告诉它。也就是说,如果在delete
操作中添加了[]
,delete
就会认定这个指针指向了一个数组,否则就是单一的一个对象。
std::string *ptr1 = new std::string;
std::string *ptr2 = new std::string[100];
//...
delete ptr1; //删除单一的一个对象。
delete ptr2; //删除一个由对象构成的数组。
现在假设我们对ptr1
使用delete[]
,会发生什么呢?delete
可能会读取若干字节的内存并将它解释为“数组大小”,然后从下一个地址开始连续调用析构函数,浑然不知它所处理的那块内存不但不是个数组,也或许并未持有那种类型的对象。
如果对ptr2
使用delete
又会发生什么事呢?未有定义,但是可以预见析构函数至多会被调用一次;更有甚者,对内置类型如int
来说,即便它没有析构函数,仍然是有害的。
现在,规则很清晰了:如果在new
的时候使用了[]
,那么delete
的时候也应该使用[]
;如果在new
的时候没有使用[]
,那么delete
的时候也不应该使用[]
。
其实,为了避免类似的错误,最好不要使用数组这种元素,因为STL
当中包含有string
,vector
等等的模板类,我们对原生数组的需求几乎被降至了0。
【注意】
如果在new
的时候使用了[]
,那么delete
的时候也应该使用[]
;如果在new
的时候没有使用[]
,那么delete
的时候也不应该使用[]
。