我们知道new是用来产生一个堆区对象的, 而new[]是用来产生一个堆区对象数组的。并且new
关键字相当于先调用函数operator new
为这个对象分配内存,在调用这个对象的构造函数; 而new[]
关键字相当于先调用函数operator new[]
为这些对象分配内存, 再依次调用这些对象的构造函数,咋听起来 new A
与 new A[n]
(当n=1时)没多大区别。但实际上是有区别的。 (注意我说的是new
与new[]
有区别, 而事实上operator new
与operator new[]
这两个函数基本没什么区别, 都与malloc
一样只是分配内存的函数).
注意本篇文章中的关于new和delete所做的事情的部分的代码算是伪代码, 只是阐述其逻辑, 忽略其中的UB(undefined behavior)
现在我们有一个类A, 注意A拥有的是平凡析构函数:
class A { public: };
什么是平凡析构函数?
若一个类型满足下列全部条件,那么它的析构函数是平凡的:
- 析构函数不是用户提供的(表示它是隐式声明,或在它的首个声明显式定义为预置(=default)的)
- 析构函数不是虚函数
- 如果该类型有基类的话,其所有基类都拥有平凡析构函数
- 如果该类型有非静态数据成员的话, 其所有非静态数据成员都拥有平凡析构函数
平凡析构函数是不进行任何动作的析构函数。有平凡析构函数的对象不要求 delete 表达式,并可以通过简单地解分配它的存储进行释放。所有与 C 语言兼容的数据类型(POD 类型)都是可以平凡析构的。
需要注意编译器显式(预置)或隐式生成的析构函数不一定是平凡的!
struct X {
std::string str;
~X() = default;
};
由于X的非静态成员str的析构函数是非平凡的,即不满足上述条件的第四条,故X的析构函数不是平凡的
我们来看看A* p = new A;
做了什么:
// new A大致做了以下事情: void* pos = operator new(sizeof(A)); // 分配内存 (A*)pos->A(...); // 调用构造函数 return (A*)pos; // 返回这个对象的内存首地址
再看看A* p = new A[n]
;
// new A[n]大致做了以下事情: A* pos = (A*)operator new[](sizeof(A) * n); // 分配内存 for (size_t i = 0; i < n; ++i) { pos[i].A(...); // 调用构造函数 } return pos; // 返回对象数组的内存首地址
但我们不使用平凡析构函数而是自定义一个析构函数呢?
class A { public: ~A() { ... // do something } };
此时new A[n]
与之前的行为不再相同:
// new A[n]大致做了以下事情: void* pos = (A*)operator new[](sizeof(size_t) + sizeof(A) * n); // 分配内存 *(size_t*)pos = n; A* arr = (A*)((size_t*)pos + 1); for (size_t i = 0; i < n; ++i) { arr[i].A(...); // 调用构造函数 } return arr; // 返回对象数组的内存首地址, 注意这里是返回arr而非pos, 原因在后面
可以看到这里需要额外申请一个size_t
的内存, 为什么会这样呢?
我们先看看用delete[]
销毁数组时发生了什么吧:
主要做了两件事情:
-
如果该类的析构函数是非平凡, 则该数组中所有对象调用其析构函数.
-
调用函数
operator delete[]
释放内存。(这里的operator delete[]
和free
一样, 只是起到释放内存的作用)
显然,如果数组元素的析构函数是平凡的,那么就无需做第1步了,毕竟之前说了平凡析构函数是不做任何操作的函数,执不执行无所谓。在这种情况下,根本不需要知道数组个数,因此无需在数组前面放元素个数n了。
但若元素的析构函数是非平凡的,我们知道析构函数一般与资源释放有关,无论如何我们自己提供的析构函数是有意义的,是需要调用的!所以销毁数组时就必须要知道这个数组元素个数。所以new A[n]时申请内存的大小不是sizeof(A) * n
, 而是sizeof(size_t)+sizeof(A)*n
这片内存的前sizeof(size_t)
个字节存放的是数组元素的数目, 即new A[n]
中的n, 然后才是第一个对象、第二个对象、...
结构如下所示:
n 对象1 对象2 ...
可以看到如果一个类使用的不是平凡析构函数的话, 我们是需要知道这个数组中对象的个数的! 所以此时new[]会额外申请一个sizeof(size_t)
大小的内存来存放该数组的个数, 以便销毁数组时让delete[]知道多少个析构函数。
// new A[n]大致做了以下事情: void* pos = (A*)operator new[](sizeof(size_t) + sizeof(A) * n); // 分配内存 *(size_t*)pos = n; A* arr = (A*)((size_t*)pos + 1); for (size_t i = 0; i < n; ++i) { arr[i].A(...); // 调用构造函数 } return arr; // 返回对象数组的内存首地址, 注意这里是返回arr而非pos, 原因在后面 // delete [] arr(其中A* arr = new A[n])大概做了以下事情: size_t num = *((size_t*)arr - 1); // 获取该数组元素个数, 因为arr指向的是数组中第一个对象, 而数组元素个数存放在该对象前面 //这里的(size_t*)arr - 1 == 上面的pos for (size_t i = num; i > 0; --i) { // 逆序析构 arr[i-1].~A(); // 调用析构函数 } operator delete[] ((size_t*)arr - 1); // 释放内存, 注意: 这片内存首地址是存放元素个数的那个size_t的地址(即new A[n]中的那个pos指针中存放的地址), 而不是第一个对象的地址