delete 和 delete []

每一本 C++ 教材上都会告诉你,使用new,new [],delete 和 delete [] 的时候必须要配对使用,否则会造成严重的后果。那么,到底会有什么严重的后果呢?
先说一下 C/C++ 中的内存释放的机制。当 C++ 程序从空余内存块中找出分配出 size 大小的内存并且使用完之后,在释放这块内存的时候,程序如何得知你当初申请了多大的内存呢?事实上,当使用 malloc/new/new[] 申请了一块内存的时候,编译器实际上会在这块内存的头部保存一个 size_t 信息,记录了这块内存的大小。这个size信息是需要占用额外的空间的,也就是说:

int * p = new int[10];

这行语句实际上从系统空闲内存中索取了sizeof(int) * 10 + sizeof (size_t) 大小的空间,用图形表示的话大概是这个样子:

delete 和 delete []_第1张图片
系统的内存分布

在对 p 指针进行free/delete/delete []操作的时候,实际上会先通过 *(p-sizeof(size_t))来获得这块内存的大小,然后将内存归还给系统。从这一步上来说,free,delete,delete [] 没有任何区别——也就是说,对于下面的两行代码:

int * p = new int[10];
delete p;

虽然没有使用delete [],但内存是可以正确地被归还的,不会引起内存泄漏之类的后果(但是,某些比较 powerful 的编译器可能会报错)。恰好相反,如果你觉得delete 没有 delete [] 强劲,试图通过一个循环归还内存的话:

int * p = new int[10];
for (int i = 0; i < 10; ++i) {
    delete p+i;
}

则会引发不可预知的后果,因为:1,这个时候整个数组都已经归还了,无需再次归还;2,delete试图读取要删除的指针头部的size信息,对于p[1] 及之后的指针来说,前面的一块内存里存放了什么样的值完全是无法预料的。同样地,企图删除 new [] 申请的数组中的某一个元素也是非法的:

int * p = new int[10];
delete p + 5;  // WRONG!

既然 delete 和 delete [] 都能正确地归还内存,那么这两者又有什么区别呢?为什么 new/delete、new []/delete [] 必须配对使用?
实际上是,对于C++程序,delete 不仅肩负着归还内存的任务,还肩负着正确地析构对象的任务。所以,如果用伪代码表示的话,delete [] 的行为类似于:

for (int i = 0 ; i < memory_size; i = i + sizeof(int)) {
    *((void * )p + i)->~int();  // 这只是个示例,很多编译器会针对POD对象进行优化,把这一步省略掉
}
free(p);

简而言之,先循环地对数组中的对象都调用一次析构函数,最后再把内存归还给系统。所以,如果对象存在析构函数并且析构函数肩负着比较重要的任务,例如释放资源句柄之类的,则必须要使用 delete [];使用 delete 的话,虽然这块内存会正确地归还,但只会针对数组内的第一个对象执行析构函数,后续的对象的析构函数都无法被执行。

以上是 delete 和 delete [] 的区别,下面是一些奇技淫巧。

我们知道 C++ 中存在着类继承,对于一个继承体系下的类的对象,delete 和 delete [] 是怎么执行的呢?

class Base {
    private:
        int a;
    public:
        virtual ~Base() {
            cout << "Good bye, Base!" << endl;
        }
};

class Derived : public Base {
    private:
        int b;
    public:
        virtual ~Derived() {
            cout << "Good bye, Derived!" << endl;
        }
};

对于下面的两行代码:

Base * p = new Derived();
delete p;

对虚函数有了解的人都会知道,在delete p;的时候,会执行Derived类的析构函数,即使p指针是一个Base类型的指针,这是由C++的虚函数机制来保证的。那么问题来了:下面两行代码,执行结果是什么?

Base * p = new Derived[10];
delete [] p;

即使new []和 delete [] 正确地配对使用了,这段程序的运行依然是不正确的,会出现内存访问错误。问题就在于,在执行 delete [] p的时候,实际上是这么执行的:

for (int i = 0; i < memory_size; i = i + sizeof(Base)) {
    (Base *)((void * ) p + i)->~();
}
free(p);

实际上,在对数组中的第一个对象执行析构操作的时候,结果是正常的:通过虚表指针找到了虚表,再通过虚表找到了派生类的虚析构函数并且执行了这个虚析构函数;但是在对第二个对象执行析构操作的时候,由于Base对象和Derived对象的大小并不相同,(Base *)((void * ) p + i)并不能找到虚表的地址——这个指针实际指向的并不是第二个Derived对象的开头,而是第一个Derived对象结尾的几个字节!程序会错误地把这末尾的几个字节的内容当做虚表的地址,所以会引发内存访问错误。
C++允许程序员完全地掌控你所写的程序(甚至允许你嵌入汇编!),但是你必须搞清楚你所写的是什么。任何一个不恰当地操作都可能引发灾难性的后果,不会有 exception 让你去 catch,而是直接 down 掉整个程序。这门语言迷人和迷惑人的地方就在这儿——从语言本身的特性来讲,C++大概真的是复杂度最高的编程语言了。

你可能感兴趣的:(delete 和 delete [])