C++ new表达式、operator new和placement new

Share on:
300 Words | Read in about 2 Min | View times

Overview

C++中new关键字和delete关键字我们肯定都使用过,它们是对堆中的内存进行申请和释放的操作,这两个操作是不能被重载的。有的同学可能会被问到,如何实现自定义内存分配行为。搞懂这个问题之前,我们需要先了解new表达式、operator newplacement new之间的关系。

本系列文章将包括以下领域:

本章其他内容请见 《现代C++》

new和operator new

我们通常使用关键字new的方式如下:

1Foo* p = new Foo();

这是一个new表达式,实际上等价于以下代码:

1Foo* p;
2void* raw = operator new(sizeof(Foo)); //operator new
3
4try {
5    p = new(raw) Foo(); //placement new
6} catch(...) {
7    operator delete(raw);
8    throw;
9}

new表达式实际上分为3步执行:

  1. 调用一个名为operator new的标准库函数,该函数从内存堆上分配一个足够大的、原始的、未命名的内存空间。若成功则返回该段内存地址,若失败则调用一个new_handler,然后继续前面的过程。

  2. 调用一个placement new构造对象,并为其传入初始值。

  3. 对象被分配了空间并构造完成,返回一个指向该对象的指针。

事实上,operator new是允许重载的,如果在类中重载了operator new,则调用的是void* Foo::operator new(size_t size);如果没有重载,则调用的是全局的void* ::operator new(size_t size)

重载operator new时可以添加新的参数:

1class Test {
2public:
3    //重载时可以增加额外参数
4    void* operator new(size_t size, std::string param) {
5        std::cout << "Call override operator new with param " << param << std::endl;
6        return ::operator new(size);
7    }
8};

operator new有一个特殊的版本不可以被重载,即后面我们要提到的placement new

重载全局的::operator new比较少见,一旦重载,所有new的行为将被改变,内部就只能使用malloc来分配内存了:

1void* operator new(size_t size) {
2    //...
3    return malloc(size);
4}

所以,要实现不同的内存分配行为,应该重载operator new,而不是new

C++标准库定义了以下8个版本的operator newoperator delete,其中前4个可能抛出bad_alloc异常,而后4个版本不会抛出异常:

 1//以下版本可能抛出异常
 2void* operator new(size_t);
 3void* operator new[](size_t);
 4void* operator delete(void*) noexcept;
 5void* operator new[](void*) noexcept;
 6
 7//以下版本不会抛出异常
 8void* operator new(size_t, nothrow_t&) noexcept;
 9void* operator new[](size_t, nothrow_t&) noexcept;
10void* operator delete(void*, nothrow_t&) noexcept;
11void* operator new[](void*, nothrow_t&) noexcept;

尽管new表达式和delete表达式默认都会调用operator newoperator delete,但是它们都是标准库函数,普通代码也是可以直接调用的。

new和::new

默认情况下编译器会将关键字new翻译成全局::operator new和相应的构造函数。

operator new在类中被重载,而有些地方又想使用默认的operator new,就应该写成::new,即调用全局的::operator new

new和placement new

placement new允许我们在一个特定的、已分配好的内存空间上直接构造对象,且这块内存空间可以暂存,反复用来构造对象,节省new时大量的系统调用开销。

placement new的调用原型如下:

1new (place_addr) T;
2new (place_addr) T(initializer);
3new (place_addr) T[size];
4new (place_addr) T[size] {initializer list}

其中place_addr必须是一个指针,同时initializer中提供了一个以逗号分隔的初始值列表,该初始值列表用于构造新分配的对象。

其实,placement new本质上只是operator new的一个特殊重载版本:

1void* operator new(size_t, void* p) throw() { return p; }

可以看到,placement new的执行忽略了size_t参数,只返回第二个参数,其结果是允许用户把一个对象放到一个特定的地方,达到调用构造函数的效果。我们来看一个具体例子:

 1char* ptr = new char[sizeof(T)]; //分配内存
 2
 3T* t = new(ptr) T; //placement new构造对象
 4
 5t->~T(); //析构对象t,注意此时不释放内存;使用placement new构造的对象,只能手动调用析构函数
 6
 7T* t2 = new(ptr) T; //复用内存,placement new构造新对象
 8
 9t2->~T(); //析构对象t2,注意此时不释放内存
10
11delete[] ptr; //真正释放内存
12

总之,利用placement new可以对一块内存进行反复构造,这样节省了中间多次请求分配内存带来的开销。

Prev Post: 『C++仿函数』
Next Post: 『C++左值&右值,左值引用&右值引用』