轉載請註明文章出處:https://tlanyan.me/construct-...
問題
爲了提高程序的性能,一個做法是一次性分配足夠多的內存,從而避免多次申請以及數據拷貝。對於c++,有一個問題:如何在已分配好的內存上構造對象?
前文“vector的性能利器:reserve”提到使用reserve
預先分配內存,再push_back
或emplace_back
,存儲過萬個大對象時可極大提升效率。探究其實現原理,會發現分配內存簡單,調用標準庫或者nedmalloc
、tcmalloc
等庫中的函數即可;有了內存,問題同樣變成如何在已分配的內存上構造對象?
方案
有兩種解決方案解決這個問題。
placement new
第一種方案是使用placement new
。其用法過程爲:首先分配足夠大的內存;然後用placement new
語法生成對象:new(ptr) xxx()
,其中ptr
是足夠容納所指對象的指針。
一個使用例子:
class Person {
private:
int age;
std::string name;
public:
// methods
};
int main(int argc, char** argv) {
char mem[sizeof(Person)]; // 或者 auto mem = malloc(sizeof(Person));
auto p = new(mem) Person();
assert((void*)p == (void*)mem); // 兩個指針指向同一塊內存
return 0;
}
使用placement new
有三個注意點:一是要有足夠的內存放置對象,這是必須的;二是指針應該是“對齊”的,例如對於4字節對齊的系統,指針地址應該是4的整數倍;三是你(可能)需要顯式調用析構函數完成對象的銷燬。
operator new
使用new
生成對象實際上執行了三個操作:
- 調用
operator new
分配內存 - 調用類的構造函數
- 返回指針
其中operator new
是可重載的,無論全局還是特定類。其函數原型爲:
void* operator new(size_t sz);
回到把對象在指定內存上構造的問題上,我們可以通過重載operator new
,返回已分配內存的指針。然而由於operator new
函數只接受一個參數,地址指針需要是“全局”變量才能生效。這樣想來,這種方案實用性並不高。
其他
如果你希望像vector中的reserve
先分配內存,然後在其上裝載對象,可以使用allocator
。allocator
定義在<memory>頭文件中,能對指定類型分配合適的內存,並可手動調用對象的構造函數和析構函數。</memory>
用法示例:
int main(int argc, char** argv) {
std::allocator<Person> alloc;
auto p = alloc.allocate(1); // 分配一個Person對象的內存
alloc.construct(p); // 調用Person的構造函數,如果構造函數有參數,參數寫在p之後
// p 現在是一個指向Person的指針,且其指向對象被初始化過
// 對p進行一些操作
// 銷燬對象,但不釋放內存,等同於調用p->~Person()
alloc.destroy(p);
// 釋放內存
alloc.deallocate(p, 1);
return 0;
}
對於可以內部管理的情形,建議使用allocator
而非placement new
。
作用
爲什麼有這個需求呢?個人覺得有三方面的原因:
- 像vector的
reserve
,預先分配內存可大幅提高性能; - 重複利用已分配好的空間,避免內存碎片;
- 細粒度進行內存管理,例如能夠實現許多虛擬機中的將內存數據從一個片區轉移到另一個片區(垃圾回收時觸發)。