C++中的std::allocator
文章目錄
- C++中的std::allocator
- 1.`std::allocator`
- 1.1C++中的placement new 和`operator new`
- 1.2一個custom allocator的實現
- 1.3使用`std::allocator_traits`實現allocator
1.std::allocator
C++中的std::allocator
默默工作在C++STL中的所有容器的內存分配上,很多內存池是按照std::allocator
的標準來實現的,甚至很多開源的內存儲項目可以和大多數STL容器兼容,在很多場景下,內存池是std::allocator
的優化。
在C++中,傳統new
操作符將內存分配(operator new
,這里的operator new
是C++的內存分配原語,默認調用C語言中的malloc
,只是進行內存分配)和對象構造(構造函數)耦合。即new
運算符需要同時完成內存分配和對象構造兩個操作。
std::allocator
將解耦內存分配和對象構造這兩個操作,按照C++11的標準,實現一個std::allocator
需要包含以下的元素和方法:
value_type
:將模板的參數類型T
定義為value_type
,如using value_type = T;
或者typedef T value_type;
allocate()
:僅分配原始內存,功能就類似opeartor new
construct()
:在預分配的內存上構造對象(通過使用C++中的placement new機制)destroy()
:析構對象但不釋放內存deallocate()
:釋放原始內存(類似于operator delete
)
注釋: https://cplusplus.com/reference/memory/allocator/
1.1C++中的placement new 和operator new
placement new 是C++中一種特使的內存分配的對象構造機制,它允許在已分配的內存上直接構造對象,而不是通過傳統的new
操作符同時分配內存和構造對象。
placement new的語法形式為:
new (pointer) Type(constructor_arguments);
其中:
pointer
是指向已分配內存的指針Type
是要構造的對象constructor_arguments
是構造函數的參數
placement new的工作原理是,不調用operator new
來分配內存,而是在給定的內存地址上直接調用構造函數,最后返回傳入的指針(將指針類型轉換為目標類型)。placement new由C++標準庫提供默認實現,不可重載:
// 標準庫中的 placement new 聲明(不可重載)
void* operator new(size_t, void* ptr) noexcept {return ptr; // 直接返回傳入的指針
}
乍一看,這個placement new的實現什么都沒干,是如何完成對象的構造呢?其實是依靠語法來進行創建的:
new (pointer) Type(constructor_arguments);
這里仍然調用了Type(constructor_arguments)
,即調用了對象的構造函數,在pointer
指定的內存上進行構造,舉個例子:
#include <iostream>
struct Example {int value;Example(int val) : value(val) {std::cout << "Constructed at " << this << " with value " << value << std::endl;}~Example() {std::cout << "Destructed at " << this << std::endl;}
};
int main() {// 手動分配一塊內存void* buffer = operator new(sizeof(Example));// 使用placement new在這塊內存上構造對象Example* obj = new (buffer) Example(42);// 顯式調用析構函數(這很重要!)obj->~Example();// 釋放內存operator delete(buffer);return 0;
}
輸出為:
Constructed at 0x7fec4c400030 with value 42
Destructed at 0x7fec4c400030
operator new
是C++的內存分配原語,默認調用malloc
進行內存分配,返回void*
,指向未初始化的原始內存,可以重載operator new
以自定義其內存分配行為:
// 自定義全局 operator new
void* operator new(size_t size){std::cout << "Allocating " << size << " bytes\n";return malloc(size);
}
使用opeartor new
和placement new的典型場景如下:
// 僅分配內存,不構造對象
void* raw_mem = operator new(sizeof(MyClass));// 需要手動構造對象(例如通過 placement new)
MyClass* obj = new (raw_mem) MyClass(); // 調用構造函數// 必須手動析構和釋放
obj->~MyClass();
operator delete(raw_mem);
核心區別:
特性 | operator new | placement new |
---|---|---|
作用 | 僅分配原始內存(不構造對象) | 在已分配的內存上構造對象 |
是否調用構造函數 | 否 | 是 |
內存來源 | 通常來自于堆(可通過重載自定義) | 由程序員預先提供 |
語法 | void* p = operator new(size) | new (ptr) Type(args...) |
是否可重載 | 可重載全局或類特定的operator new | 不能重載,已經有固定實現 |
1.2一個custom allocator的實現
一個自定義的allocator
需要實現以下的方法:
方法 | 描述 | 等效操作 |
---|---|---|
allocate(n) | 分配n* sizeof(T) 字節 | operator new |
deallocate(p, n) | 釋放從p 開始的n 個元素 | operator delete |
construct(p, args) | 在p 構造對象(C++17已棄用) | new(p) T(args...) |
destroy(p) | 析構p 處對象(C++17已棄用) | p->~T() |
注釋:C++17 后推薦通過
std::allocator_traits
訪問接口,以支持自定義分配器的可選方法。
按照C++11的標準實現一個allocator
:
#include <iostream>
#include <vector>
template<typename T>
class TrackingAllocator {
public:using value_type = T;TrackingAllocator() = default;// 支持 Rebinding(重新綁定)template<typename U>TrackingAllocator(const TrackingAllocator<U>&) {}T* allocate(size_t n) {size_t bytes = n * sizeof(T);std::cout << "Allocating " << bytes << " bytes\n";return static_cast<T*>(::operator new(bytes));}void deallocate(T* p, size_t n) {::operator delete(p);std::cout << "Deallocating " << n * sizeof(T) << " bytes\n";}// 支持同類型分配器比較(無狀態)bool operator==(const TrackingAllocator&) { return true; }bool operator!=(const TrackingAllocator&) { return false; }
};// 使用示例
int main() {// 使用自定義分配器std::vector<int, TrackingAllocator<int>> vec;vec.push_back(42); // 輸出分配信息vec.push_back(13); // 輸出分配信息// 清空向量vec.clear(); // 輸出釋放信息return 0;
}
輸出:
Allocating 4 bytes
Allocating 8 bytes
Deallocating 4 bytes
Deallocating 8 bytes
1.3使用std::allocator_traits
實現allocator
在 C++17 及之后版本中,推薦通過 std::allocator_traits
訪問分配器接口,而非直接調用分配器的方法。這是因為 allocator_traits
提供了一種統一且安全的方式來與分配器交互,即使自定義分配器沒有實現某些可選方法,也能通過默認實現正常工作。
- 兼容性:即使自定義分配器未實現某些方法(如
construct
/destroy
),allocator_traits
會提供默認實現。 - 靈活性:允許分配器僅實現必要的接口,其余由
allocator_traits
補充。 - 標準化:所有標準庫容器(如
std::vector
、std::list
)內部都使用allocator_traits
而非直接調用分配器。
注釋:https://cplusplus.com/reference/memory/allocator_traits/
關鍵接口對比(使用C++11標準 vs. C++17標準)
操作 | C++11,直接調用分配器alloc | C++17,通過allocator_traits(std::allocator_traits<Alloc>) |
---|---|---|
分配內存 | alloc.allocate(n) | allocator_traits<Alloc>::allocate(alloc, n) |
釋放內存 | alloc.deallocate(p, n) | allocator_traits<Alloc>::deallocate(alloc, p, n) |
構造對象 | alloc.construct(p, args) | allocator_traits<Alloc>::construct(alloc, p, args...) |
析構對象 | alloc.destroy(p) | allocator_traits<Alloc>::destroy(alloc, p) |
獲取最大大小 | alloc.max_size() | allocator_traits<Alloc>::max_size(alloc) |
重新綁定分配器類型 | alloc.rebind<U>::other | allocator_traits<Alloc>::rebind_alloc<U> |
注釋:C++17 后
construct
和destroy
被廢棄,推薦直接使用std::allocator_traits
或 placement new/顯式析構。
舉個極簡分配器的例子:
#include <iostream>
#include <memory> // std::allocator_traitstemplate <typename T>
struct SimpleAllocator {using value_type = T;// 必須提供 allocate 和 deallocateT* allocate(size_t n) {return static_cast<T*>(::operator new(n * sizeof(T)));}void deallocate(T* p, size_t n) {::operator delete(p);}// 不提供 construct/destroy,由 allocator_traits 提供默認實現
};struct Widget {int id;Widget(int i) : id(i) { std::cout << "Construct Widget " << id << "\n"; }~Widget() { std::cout << "Destroy Widget " << id << "\n"; }
};int main() {using Alloc = SimpleAllocator<Widget>;Alloc alloc;// 1. 分配內存(通過 allocator_traits)auto p = std::allocator_traits<Alloc>::allocate(alloc, 1);// 2. 構造對象(即使 SimpleAllocator 沒有 construct 方法!)std::allocator_traits<Alloc>::construct(alloc, p, 42); // 調用 Widget(42)// 3. 析構對象(即使 SimpleAllocator 沒有 destroy 方法!)std::allocator_traits<Alloc>::destroy(alloc, p);// 4. 釋放內存std::allocator_traits<Alloc>::deallocate(alloc, p, 1);return 0;
}
輸出:
Construct Widget 42
Destroy Widget 42
一個更復雜的自定義分配器示例(帶狀態)
#include <iostream>
#include <memory> // std::allocator_traitstemplate <typename T>
class TrackingAllocator {size_t total_allocated = 0;
public:using value_type = T;T* allocate(size_t n) {total_allocated += n * sizeof(T);std::cout << "Allocated " << n * sizeof(T) << " bytes (Total: " << total_allocated << ")\n";return static_cast<T*>(::operator new(n * sizeof(T)));}void deallocate(T* p, size_t n) {total_allocated -= n * sizeof(T);std::cout << "Deallocated " << n * sizeof(T) << " bytes (Remaining: " << total_allocated << ")\n";::operator delete(p);}// 支持比較(相同類型的 TrackingAllocator 才等價)bool operator==(const TrackingAllocator& other) const {return false; // 有狀態,不同實例不能混用}bool operator!=(const TrackingAllocator& other) const {return true;}
};int main() {using Alloc = TrackingAllocator<int>;Alloc alloc1, alloc2;auto p1 = std::allocator_traits<Alloc>::allocate(alloc1, 2);auto p2 = std::allocator_traits<Alloc>::allocate(alloc2, 3);// 必須用相同的 allocator 實例釋放!std::allocator_traits<Alloc>::deallocate(alloc1, p1, 2);std::allocator_traits<Alloc>::deallocate(alloc2, p2, 3);return 0;
}
輸出:
Allocated 8 bytes (Total: 8)
Allocated 12 bytes (Total: 12)
Deallocated 8 bytes (Remaining: 0)
Deallocated 12 bytes (Remaining: 0)