C++中new与new[]的区别

 我们知道new是用来产生一个堆区对象的, 而new[]是用来产生一个堆区对象数组的。并且new关键字相当于先调用函数operator new为这个对象分配内存,在调用这个对象的构造函数; 而new[]关键字相当于先调用函数operator new[]为这些对象分配内存, 再依次调用这些对象的构造函数,咋听起来 new Anew A[n](当n=1时)没多大区别。但实际上是有区别的。 (注意我说的是newnew[]有区别, 而事实上operator newoperator 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[]销毁数组时发生了什么吧:

主要做了两件事情:

  1. 如果该类的析构函数是非平凡, 则该数组中所有对象调用其析构函数.

  2. 调用函数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指针中存放的地址), 而不是第一个对象的地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值