1. new和new[], delete和delete[]
首先,我们引入一个平凡析构函数的概念,所谓平凡,就是析构函数调用与不调用并没有区别,也就是析构函数是空的。一个类的析构函数是平凡的,需要满足以下条件:
- 我们没有显式定义析构函数(如果我们定义了,编译器会认为析构函数是必须调用的,尽管我们自定义的析构函数是空的)
- 析构函数不是虚函数
- 类类型非静态成员变量的析构函数也是平凡的
- 基类的析构函数也是平凡的
那么析构函数平不平凡与动态内存的申请和释放又有什么关联呢?先简单解答一下这个问题,如果析构函数是平凡的,那么我们使用MyClass *p = new MyClass[3];
时,在堆上只会分配大小为sizeof(MyClass) * 3
的内存;如果析构函数是不平凡的,在堆上分配的内存大小为sizeof(unsigned long) + sizeof(MyClass) * 3
,就是在实际存在对象的内存会前,会用8个字节记录申请的对象个数,当使用delete[]
释放内存时,会往前读取8个字节,以确定要调用的析构函数次数。
注:测试过程在linux系统上进行,可能windows系统只需要4个字节用于记录个数。
我们先定义一个类,这个类中我们显式定义了析构函数,尽管这个析构函数没有任何代码,但是编译器仍把这个类认为为是一个不平凡的类。
class MyClass {
private:
int a;
int b;
public:
MyClass() {
this->a = 10;
this->b = 5;
cout << "无参构造" << endl;
};
~MyClass() {
// cout << "析构函数" << endl;
}
};
接下来实现一下测试代码
int main() {
// 获取一个MyClass对象的占用的字节数
cout << "sizeof(MyClass) = " << sizeof(MyClass) << endl;
// 堆上申请3个MyClass对象
MyClass *p = new MyClass[3];
int tmp1 = 1;
float tmp2 = 2.0f;
double tmp3 = 3.0;
bool tmp4 = true;
short tmp5 = 4;
printf("&p = %p, sizeof(p) = %ld\n", &p, sizeof(p));
printf("(p - 1) = %p, *(p - 1) = %ld\n", (p - 1), *((unsigned long *)(p - 1)));
printf("&p[0] = %p, (p + 0) = %p, sizeof(p[0]) = %ld\n", &p[0], (p + 0), sizeof(p[0]));
printf("&p[1] = %p, (p + 1) = %p, sizeof(p[1]) = %ld\n", &p[1], (p + 1), sizeof(p[1]));
printf("&p[2] = %p, (p + 2) = %p, sizeof(p[2]) = %ld\n", &p[2], (p + 2), sizeof(p[2]));
printf("第一个对象p[0]的地址 %p\n", &p[0]);
printf("第一个对象p[0]的地址 %p\n", &p[0]);
printf("&tmp1 = %p, sizeof(tmp1) = %ld\n", &tmp1, sizeof(tmp1));
printf("&tmp2 = %p, sizeof(tmp2) = %ld\n", &tmp2, sizeof(tmp2));
printf("&tmp3 = %p, sizeof(tmp3) = %ld\n", &tmp3, sizeof(tmp3));
printf("&tmp4 = %p, sizeof(tmp4) = %ld\n", &tmp4, sizeof(tmp4));
printf("&tmp5 = %p, sizeof(tmp5) = %ld\n", &tmp5, sizeof(tmp5));
return 0;
}
输出结果为
sizeof(MyClass) = 8
无参构造
无参构造
无参构造
&p = 0x7ffe00f2d260, sizeof(p) = 8
(p - 1) = 0x5c42792722c0, *(p - 1) = 3
&p[0] = 0x5c42792722c8, (p + 0) = 0x5c42792722c8, sizeof(p[0]) = 8
&p[1] = 0x5c42792722d0, (p + 1) = 0x5c42792722d0, sizeof(p[1]) = 8
&p[2] = 0x5c42792722d8, (p + 2) = 0x5c42792722d8, sizeof(p[2]) = 8
第一个对象p[0]的地址 0x5c42792722c8
第一个对象p[0]的地址 0x5c42792722c8
&tmp1 = 0x7ffe00f2d258, sizeof(tmp1) = 4
&tmp2 = 0x7ffe00f2d25c, sizeof(tmp2) = 4
&tmp3 = 0x7ffe00f2d268, sizeof(tmp3) = 8
&tmp4 = 0x7ffe00f2d255, sizeof(tmp4) = 1
&tmp5 = 0x7ffe00f2d256, sizeof(tmp5) = 2
直接看上面的输出结果,可能比较难以理解,根据上面的输出结果,我绘制了如下的堆栈信息图。
指针p指向的是第一个MyClass对象的地址,我们通过往前移动8个字节,并以unsigned long
的类型输出其中的内容,确实是为3,这也就验证了我们前面的说法,对于不平凡的类,new[]
会多使用8个字节记录对象的个数。
从上图中,我们还可以发现,栈中变量存储的顺序并不是我们定义的顺序,而是占用空间较大的变量会被存储在栈底,占用空间较小的变量会被存储在栈顶(栈的增长方向是从上往下)。
接下来,我们需要判断对于不平凡的类,是否会记录对象的个数。我们修改一下MyClass的定义,修改结果如下。
class MyClass {
public:
int a;
int b;
public:
// MyClass() = default;
MyClass() {
this->a = 10;
this->b = 5;
cout << "无参构造" << endl;
};
// 不显示声明析构函数
// ~MyClass() {
// cout << "析构函数" << endl;
// }
};
不改变测试代码,运行结果为
sizeof(MyClass) = 8
无参构造
无参构造
无参构造
&p = 0x7fff57894ef8, sizeof(p) = 8
(p - 1) = 0x5786ba7e02b8, *(p - 1) = 33 # 输出的值不再是对象的个数
&p[0] = 0x5786ba7e02c0, (p + 0) = 0x5786ba7e02c0, sizeof(p[0]) = 8
&p[1] = 0x5786ba7e02c8, (p + 1) = 0x5786ba7e02c8, sizeof(p[1]) = 8
&p[2] = 0x5786ba7e02d0, (p + 2) = 0x5786ba7e02d0, sizeof(p[2]) = 8
第一个对象p[0]的地址 0x5786ba7e02c0
第一个对象p[0]的地址 0x5786ba7e02c0
&tmp1 = 0x7fff57894ef0, sizeof(tmp1) = 4
&tmp2 = 0x7fff57894ef4, sizeof(tmp2) = 4
&tmp3 = 0x7fff57894f00, sizeof(tmp3) = 8
&tmp4 = 0x7fff57894eed, sizeof(tmp4) = 1
&tmp5 = 0x7fff57894eee, sizeof(tmp5) = 2
我们发现第6行的输出结果不再是对象的个数。
对于基本数据类型以及平凡的类,使用new[]或new申请的内存,使用delete[]或delete释放并没有区别。但对于不平凡的类,使用new[]申请内存时,会在第一个元素的前面记录元素的个数,当使用delete[]释放内存时,会先读取元素的个数,并调用对应次数的析构函数,最后释放内存;而使用delete释放内存时,只会调用一次析构函数(如果需要),最后释放内存。
根据个人测试结果推测,内容可能存在错误!!! 若有错误,感谢指正~