C++ new表达式、operator new和placement new
300 Words | Read in about 2 Min | View times
Overview
C++中new
关键字和delete
关键字我们肯定都使用过,它们是对堆中的内存进行申请和释放的操作,这两个操作是不能被重载的。有的同学可能会被问到,如何实现自定义内存分配行为。搞懂这个问题之前,我们需要先了解new
表达式、operator new
和placement 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步执行:
-
调用一个名为
operator new
的标准库函数,该函数从内存堆上分配一个足够大的、原始的、未命名的内存空间。若成功则返回该段内存地址,若失败则调用一个new_handler
,然后继续前面的过程。 -
调用一个
placement new
构造对象,并为其传入初始值。 -
对象被分配了空间并构造完成,返回一个指向该对象的指针。
事实上,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 new
和operator 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 new
和operator 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
可以对一块内存进行反复构造,这样节省了中间多次请求分配内存带来的开销。