所有代码均运行在visual studio2019、x64环境下

delete用于释放由new创建的单个对象,delete[]用于释放由new创建的数组对象,一般都是配对使用。

但是为什么delete不用于释放数组对象,delete[]为什么不用于释放单个对象,从两者的原理说起。

delete在使用时会经历两步:

  1. 调用指针所指向的对象的析构函数
  2. 调用free函数回收指针所指向的内存

delete[]也是两步:

  1. 调用指针所指向数组中每个对象的析构函数
  2. 调用free函数回收指针所指向的内存

两者在第一步都需要调用对象的析构函数,只是一次或是多次的区别,也因此对于数组对象,需要使用delete[],否则除了第一个数组中第一个对象析构函数被调用,之后的对象都不会调用析构函数,在对象持有指针或是一些系统资源如文件句柄,Socket等时,如果不在析构函数中进行释放,将造成内存泄漏。注意到了吗,上文中有个前提是需要调用析构函数,如果对象不显式存在析构函数,如基本数据类型int,char或是自定义数据类型中不显式定义析构函数,这时候delete和delete[]就没有区别,因为不需要调用析构函数。

class TestA
{
public:
    TestA() { }
    virtual ~TestA() { cout << "~A" << endl; }
    int i;
};

class TestB
{
public:
    TestB() { }
    //virtual ~TestB() { cout << "~B" << endl; }
    int i;
};


int main() {
    int* arr = new int[10];
    delete[] arr;

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

    TestB* b = new TestB[10];
    delete[] b;

    TestB* bb = new TestB[10];
    delete bb;

    TestA* a = new TestA[10];
    delete[] a;

    TestA* aa = new TestA[10];
	delete aa;		//在这里出错
}
~A
~A
~A
~A
~A
~A
~A
~A
~A
~A
~A

一共调用了11次析构函数,在最后一次程序报错。

但在这里还没有解决问题,为什么delete aa会报错呢,按理来说虽然只调用了一次析构函数,之后的对象都没有调用,但也无所谓啊,最多就是对象中的一些资源没有被释放,但分配的内存还是可以由free函数回收掉的。关于这一点我们先思考一个问题,对于delete[]而言,他是如何知道要调用多少个对象的析构函数的,当我们在new[]时,向操作系统申请一块内存,然后调用构造函数,申请的这块内存由操作系统进行管理,它记录内存首地址和长度,这样在调用free时传入首地址进行回收,试想一下,我们知道对象的大小,比如int四个字节,此时操作系统记录了分配内存的总长度,是不是就可以知道有多少个对象了,但可惜的是操作系统并没有提供访问 内存长度的接口,也因此无法知道内存长度。所有编译器在分配时,会在数组首地址之前再申请一块空间用于记录数组个数(如果没有析构函数就不需要知道个数,编译器也就不会多申请这块空间),对于x64,这个大小存储在数组首地址的前八个字节,对于x86则是前4个字节

class TestA
{
public:
    TestA() { }
    virtual ~TestA() { cout << "~A" << endl; }
    int i;
};

int main() {
    int* arr = new int[10];
    cout << *((long long*)arr - 1) << endl;
    delete[] arr;


    TestA* a = new TestA[10];
    cout << *((long long*)a - 1) << endl;
    delete[] a;

}
-144680349937434461
10
~A
~A
~A
~A
~A
~A
~A
~A
~A
~A

对于int数组,由于没有析构函数所有编译器就不会存储数组大小,所有取前8个字节内容是不确定的,而对于a则记录大小为10。此时我们解决一个问题,在分配含有析构函数的自定义对象时,会多申请8个字节用于记录数组大小,方便调用析构函数时知道要调用多少次,所以在使用delete去释放一个数组对象时,由于传入的是数组首地址,但是申请的内存应该是数组首地址再往前8个字节的位置,数组首地址操作系统并未记录,所以会出错,同样的,使用delete[]去释放单个对象,由于他会访问前8个字节取得大小从而决定调用多少次析构函数,这时候行为将会不确定,却决于前8个字节会是什么值,可能会调用很多次析构函数,但在最后free时会出错,因为new对象时记录的是首地址,而不是首地址-8。

可以使用该代码测试操作系统记录的地址

class TestA
{
public:
    TestA() { }
    ~TestA() { cout << "~A" << endl; }
    int i;
};

int main() {
    TestA* a = new TestA[10];
    free((long long*)a - 1);
    //free(a);	//会出错,因为会多分配8个字节,首地址不为a
}
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐