内置类型和简单类型为何可以不用配对?
当使用简单类型(没有默认析构)
比如如下代码:
void test()
{
int* lis = new int[100];
delete lis;
}
或者只有简单数据的结构体:
struct node
{
int a;
int b;
int *c;
};
void test()
{
node* lis = new node[10];
delete lis;
}
可以看到。
程序都是正常运行,并且没有内存泄漏,这就很奇怪,欸,为什么呢?
你不是说数组的指针前面加了n才导致new
和delete
必须配对吗?
对,我是这样说过,但是对于简单类型来说,new[]
出来的内存空间前面并没有使用n。下面我将会进行验证。
虽然在前面所说的文章已经说的很清楚了,但是我这次想用另一种方式重新讲一讲为什么这样的操作也可以。
整体思路如下:
当
new
一个对象或者数组的时候,其先分配一块未初始化的内存,然后执行对象的默认构造函数(如果有的话)在分配的内存空间上一个一个(如果有多个)地进行构造然后编译器通过一种方式,保存new出来的空间的大小的数据信息(比如100字节之类的大小的信息)
在
delete
的时候,如果是复杂类型(含有显式析构函数)的话,则会执行其析构函数之后再根据记录下的大小信息将所有的内存全部释放,如果为简单类型,则直接根据大小信息释放同样大小的内存那么为何需要有
delete[]
的操作呢?想象一下,如果一个对象A,其内部含有一个指针,指向一块自己new
出来的处于堆上的内存空间,其析构函数中存在释放这个指针的操作。用new[]
分配出来一个A对象数组,这个对象数组的内存空间前面有4个字节存放数组长度。但是却由delete
操作进行释放的时候,虽然这一整块数组空间也被成功的释放掉了,但是,这个A的析构函数却没有执行,同时,数组前面的4个字节也没有被释放。导致了内存泄漏。所以编译器把这种不配对的对复杂类型的
new
和delete
行为视为错误行为(未定义行为)
所以我们可以知道如下两点:
1、 复杂类型new[]
出来的空间前面存在4个字节存储长度信息
2、只有用delete[]
配对new[]
对复杂类型进行空间管理时才会同时释放前面的4个字节的保存大小信息的内存空间 。如果面对复杂类型不配对使用两者,就会多释放、或者少释放内存空间
以下是验证
首先,我们构建一个类A:
class A
{
private:
int value;
public:
A(int v):value(v) {};
A() {
cout << "creat A" << endl;
}
~A()
{
cout << "delete A" << endl;
};
};
这个类A含有默认构造函数,还有显式的析构函数(注意只有含有默认构造函数的类才可以使用new[]
形式进行内存分配)
我们试着用如下的方式new和delete 进行内存的分配和释放:
void test()
{
A* lis = new A[10];
delete lis;
}
编译正常,但是运行出错。
我们注释掉析构函数再试试:
class A
{
private:
int value;
public:
A(int v):value(v) {};
A()
{
cout << "creat A" << endl;
}
/*~A()
{
cout << "delete A" << endl;
};*/
};
再次运行:
A* lis = new A[10];
delete lis;
OK了,正确运行,没有错误。
我们试着输出一下分配的空间前面的4字节的信息,都执行这样的操作:
A* lis = new A[3];
int *p = (int*)lis;
cout << *(p - 1) << endl;
有析构函数的对象数组,输出如下:
creat A
creat A
creat A
3
没有析构函数的对象数组,输出如下:
creat A
creat A
creat A
-33686019
可以看到,只有拥有显式析构函数的对象,在执行new[]
的时候才会被装载数量信息,然后在delete[]
的时候,将存储数量信息的内存空间、复杂对象数组的内存空间和数组对象管理的内存空间都释放掉,保证没有内存泄漏。
底层的实现到底是不是如此,我也不太确定,也许这些信息储存的操作都是在malloc()
中实现的,能力有限。只能分析到如此了,希望以后可以更加深入地去探究一下。