条款16:成对使用new和delete时要采用相同形式
Use the same form in corresponding uses of new and delete.
首先,对于以下代码:
std::string* stringArray = new std::string[100];
...
delete stringArray;
虽然乍一看,这段代码并没有什么不妥,确实是成对使用了new和delete,但还是存在错误,因为程序中存在不明确的行为:
- 在stringArray内所含的100个上图日国内对象中,有99不太可能被适当的删除,因为它们的析构函数很有可能没有被调用。
new与delete
当我们使用new关键字时——通过new动态生成一个对象,会发生两件事:
- 内存被分配出来(通过operator new函数)
- 针对此内存会有一个(或者更多)的构造函数被调用。
而当我们使用delete关键字时,也会发生两件事:
- 针对次内存会有一个(或者更多)的析构函数被调用。
- 内存被释放出来(通过operator delete函数)
但是,delete的最大问题在于:
- 即将被删除的内存之中到底有多少个对象?这关系到有多少的析构函数应该被调用。
因此,对于这个问题,可以用更为简单的方式进行理解:即将被删除的指针,所指的到底是单一对象还是对象数组?之所以要分清,是因为一般而言,单一对象的内存布局不同于数组的内存布局。更明确的说 :
- 数组所用的内存还包括“数组大小”的记录,以便delete直到需要调用多少次的析构函数。
虽然数组有这样的记录,但是当我们对着一个指针使用delete时,唯一能够让delete知道内存中存在一个“数组大小记录”的方法是:由我们去告诉它——
- 如果在使用delete时加上中括号,delete就会认定指针指向一个数组,否则它就认定指针指向单一的对象。
std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1; //删除一个对象
delete [] stringPtr2; //删除一个由数组组成的数组
对于上面两种的形式:
1,如果我们对stringPtr1使用delete []的形式,会产生不理想的未定义结果。此时,delete会读取若干的内存并将它们解释为“数组大小”,然后开始多次调用析构函数,殊不知这块内存并不是一个数组,其上也没有应该销毁的对象。
2,如果我们没有对stringPtr2使用delete []的形式,依然会产生不理想的未定义结果。因此此时,可能会导致太少的析构函数被调用。另外,对于没有析构函数的内置类型(如int型),这种操作也是未定义的、甚至有害。
总之:
- 如果调用new时使用[],就必须在对应调用delete时也使用[]。
- 如果调用new时没有使用[],那么也不应该在对应调用delete的位置使用[]。
当我们撰写的class含有一个指针指向动态分配的内存、并提供多个构造函数时,这个规则就非常重要,因为这种情况下必须很小心的在所有构造函数中使用相同形式的new将指针成员初始化。如果没有这样做,就没法在析构函数使用对应形式的delete。
在使用typedef时,这条规则也十分重要,因为在使用new的方式创建该种typedef类型对象时,应该以哪一种delete形式将其删除,必须要表达清楚:
typedef std::string AddressLines[4]; //表示每个人的地址有四行,每行是一个string
由于AddressLines是一个数组,如果这样使用new:
std::string* pal = new AddressLines; //此时,“new AddressLines”返回一个string*
//就像“new string[4]”一样
那么就必须匹配“数组形式”的delete:
delete pal; //未定义的行为!错误!
delete [] pal; //正确的行为
当然,为了避免这样的错误,尽量不要对数组形式做typedef操作。因为C++标准库中含有string,vector等templates,对于上面这个例子中,就可以将AddressLines定义为vector< string >就完事儿了。
最后: