好的,各位观众老爷们,欢迎来到今天的C++内存分配脱口秀!今天咱们要聊的是一个听起来高端大气上档次,但实际上…嗯…也确实有点高端的东西:自定义std::allocator
。
开场白:内存,谁说了算?
咱们写C++,容器是家常便饭。std::vector
、std::list
、std::map
…哪个不是天天见?但你有没有想过,这些容器背后的内存,是谁在默默奉献?没错,就是std::allocator
!
默认情况下,容器们会使用std::allocator<T>
,这个老兄会调用::operator new
和::operator delete
来分配和释放内存。换句话说,它基本上就是个封装了全局new
和delete
的壳子。
但问题来了,全局new
和delete
虽然好用,但有时候不够灵活。比如:
- 性能问题: 全局
new
和delete
可能会有锁竞争,在大并发场景下会成为瓶颈。 - 内存碎片: 频繁分配和释放小块内存会导致内存碎片,降低内存利用率。
- 定制需求: 你可能想使用特定的内存池,或者在特定的地址分配内存。
- 诊断与调试: 你可能想追踪内存分配情况,检测内存泄漏。
这时候,自定义std::allocator
就闪亮登场了!它可以让你对容器的内存分配进行细粒度控制,就像给容器配了个私人管家,想怎么花钱(内存)都由你说了算。
正文:手把手教你打造私人管家
要自定义std::allocator
,你需要定义一个类,并满足一些特定的要求。别怕,其实没那么复杂。
1. allocator
类的基本结构
一个最基本的allocator
类看起来像这样:
template <typename T>
class MyAllocator {
public:
using value_type = T;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
// 默认构造函数、复制构造函数、移动构造函数、析构函数(通常不需要自定义)
MyAllocator() noexcept = default;
template <typename U> MyAllocator(const MyAllocator<U>&) noexcept {}
~MyAllocator() noexcept = default;
// 分配内存
pointer allocate(size_type n);
// 释放内存
void deallocate(pointer p, size_type n);
};
value_type
: 分配器管理的类型。pointer
、const_pointer
、reference
、const_reference
: 这些类型定义了指针和引用的类型,通常直接使用T*
、const T*
、T&
、const T&
就足够了。size_type
、difference_type
: 大小和差值的类型,通常使用std::size_t
和std::ptrdiff_t
。- 构造函数、析构函数: 默认的就挺好,一般不用动。
allocate(size_type n)
: 分配n
个T
类型的对象的内存。这是最核心的函数之一。deallocate(pointer p, size_type n)
: 释放p
指向的n
个T
类型的对象的内存。这也是最核心的函数之一。
2. 实现allocate
和deallocate
接下来,我们来实现allocate
和deallocate
函数。这里我们先用最简单的new
和delete
来演示:
template <typename T>
typename MyAllocator<T>::pointer MyAllocator<T>::allocate(size_type n) {
if (n > std::numeric_limits<size_type>::max() / sizeof(T)) {
throw std::bad_alloc(); // 防止整数溢出
}
pointer p = static_cast<pointer>(::operator new(n * sizeof(T)));
if (!p) {
throw std::bad_alloc(); // 分配失败
}
return p;
}
template <typename T>
void MyAllocator<T>::deallocate(pointer p, size_type n) {
::operator delete(p);
}
- 整数溢出检查:
allocate
函数首先要检查n * sizeof(T)
是否会溢出,避免分配过小的内存。 ::operator new
: 使用全局new
来分配内存。注意,这里使用::operator new
而不是new T[n]
,因为new T[n]
会调用构造函数,而allocator
只负责分配原始内存。::operator delete
: 使用全局delete
来释放内存。同样,这里使用::operator delete
而不是delete[] p
,因为delete[] p
会调用析构函数。- 异常处理: 如果分配失败,抛出
std::bad_alloc
异常。
3. rebind
(重要!)
allocator
还需要一个rebind
特性。这个特性允许你从一个allocator<T>
创建出allocator<U>
,这在某些容器(比如std::map
)中是必需的。
template <typename T>
struct MyAllocator {
// ... (前面的代码)
template <typename U>
struct rebind {
using other = MyAllocator<U>;
};
};
rebind
是一个嵌套的模板类,它定义了一个名为other
的类型,该类型是MyAllocator<U>
。
4. operator==
和operator!=
allocator
还需要定义operator==
和operator!=
,用于比较两个allocator
是否相等。通常情况下,只要两个allocator
的类型相同,就认为它们相等。
template <typename T, typename U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) noexcept {
return true;
}
template <typename T, typename U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) noexcept {
return false;
}
5. 完整代码
把上面的代码片段拼起来,一个最基本的自定义allocator
就完成了:
#include <iostream>
#include <vector>
#include <limits>
template <typename T>
class MyAllocator {
public:
using value_type = T;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
MyAllocator() noexcept = default;
template <typename U> MyAllocator(const MyAllocator<U>&) noexcept {}
~MyAllocator() noexcept = default;
pointer allocate(size_type n);
void deallocate(pointer p, size_type n);
template <typename U>
struct rebind {
using other = MyAllocator<U>;
};
};
template <typename T>
typename MyAllocator<T>::pointer MyAllocator<T>::allocate(size_type n) {
if (n > std::numeric_limits<size_type>::max() / sizeof(T)) {
throw std::bad_alloc();
}
pointer p = static_cast<pointer>(::operator new(n * sizeof(T)));
if (!p) {
throw std::bad_alloc();
}
std::cout << "Allocated " << n * sizeof(T) << " bytes at " << p << std::endl;
return p;
}
template <typename T>
void MyAllocator<T>::deallocate(pointer p, size_type n) {
std::cout << "Deallocated memory at " << p << std::endl;
::operator delete(p);
}
template <typename T, typename U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) noexcept {
return true;
}
template <typename T, typename U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) noexcept {
return false;
}
int main() {
std::vector<int, MyAllocator<int>> vec;
vec.reserve(10); // 预分配10个int的内存
for (int i = 0; i < 10; ++i) {
vec.push_back(i);
}
return 0;
}
运行上面的代码,你会看到allocate
和deallocate
函数被调用,并且输出了分配和释放的地址。
进阶:定制你的私人管家
上面的MyAllocator
只是个简单的例子,它实际上和std::allocator
没什么区别。下面我们来玩点更刺激的,定制你的私人管家!
1. 内存池分配器
内存池是一种预先分配一大块内存,然后从中分配小块内存的技术。它可以减少内存碎片,提高分配速度。
#include <iostream>
#include <vector>
#include <limits>
#include <memory> // std::align
template <typename T>
class PoolAllocator {
private:
T* pool_ = nullptr;
size_t pool_size_ = 0;
T* current_ = nullptr;
public:
using value_type = T;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
PoolAllocator(size_t pool_size) : pool_size_(pool_size) {
pool_ = static_cast<T*>(::operator new(pool_size_ * sizeof(T)));
current_ = pool_;
std::cout << "PoolAllocator: Initialized pool of " << pool_size_ * sizeof(T) << " bytes at " << pool_ << std::endl;
}
template <typename U>
PoolAllocator(const PoolAllocator<U>& other) : pool_size_(other.pool_size_) {
pool_ = static_cast<T*>(::operator new(pool_size_ * sizeof(T)));
current_ = pool_;
std::cout << "PoolAllocator: Copy Initialized pool of " << pool_size_ * sizeof(T) << " bytes at " << pool_ << std::endl;
}
~PoolAllocator() {
std::cout << "PoolAllocator: Destroying pool at " << pool_ << std::endl;
::operator delete(pool_);
}
pointer allocate(size_type n) {
if (n > 1) {
throw std::bad_alloc(); // 只能分配单个对象
}
if (current_ + n > pool_ + pool_size_) {
throw std::bad_alloc(); // 内存池已满
}
pointer p = current_;
current_ += n;
std::cout << "PoolAllocator: Allocated " << sizeof(T) << " bytes at " << p << std::endl;
return p;
}
void deallocate(pointer p, size_type n) {
// 不做实际释放,等待整个内存池销毁
std::cout << "PoolAllocator: Deallocate called (no actual deallocation)" << std::endl;
}
template <typename U>
struct rebind {
using other = PoolAllocator<U>;
};
};
template <typename T, typename U>
bool operator==(const PoolAllocator<T>&, const PoolAllocator<U>&) noexcept {
return true;
}
template <typename T, typename U>
bool operator!=(const PoolAllocator<T>&, const PoolAllocator<U>&) noexcept {
return false;
}
int main() {
PoolAllocator<int> allocator(10); // 创建一个大小为10的int内存池
std::vector<int, PoolAllocator<int>> vec(allocator);
for (int i = 0; i < 5; ++i) {
vec.push_back(i);
}
return 0;
}
pool_
、pool_size_
、current_
:pool_
指向内存池的起始地址,pool_size_
是内存池的大小,current_
指向下一个可分配的地址。- 构造函数: 在构造函数中分配内存池,并初始化
current_
。 allocate
: 从current_
指向的位置分配内存,并将current_
向后移动。如果内存池已满,抛出std::bad_alloc
异常。deallocate
: 不进行实际的内存释放。因为内存池通常在整个容器销毁时才释放。- 注意: 这个简单的内存池分配器只能分配单个对象,如果需要分配多个对象,需要进行修改。
2. 追踪内存分配的分配器
如果你想追踪内存分配情况,可以创建一个追踪内存分配的分配器。
#include <iostream>
#include <vector>
#include <limits>
#include <map>
#include <mutex>
template <typename T>
class TrackingAllocator {
private:
std::map<void*, size_t> allocations_;
std::mutex mutex_;
public:
using value_type = T;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
TrackingAllocator() noexcept = default;
template <typename U> TrackingAllocator(const TrackingAllocator<U>&) noexcept {}
~TrackingAllocator() {
std::lock_guard<std::mutex> lock(mutex_);
if (!allocations_.empty()) {
std::cerr << "Memory leak detected!" << std::endl;
for (const auto& [ptr, size] : allocations_) {
std::cerr << " Address: " << ptr << ", Size: " << size << " bytes" << std::endl;
}
}
}
pointer allocate(size_type n) {
if (n > std::numeric_limits<size_type>::max() / sizeof(T)) {
throw std::bad_alloc();
}
pointer p = static_cast<pointer>(::operator new(n * sizeof(T)));
if (!p) {
throw std::bad_alloc();
}
std::lock_guard<std::mutex> lock(mutex_);
allocations_[p] = n * sizeof(T);
std::cout << "TrackingAllocator: Allocated " << n * sizeof(T) << " bytes at " << p << std::endl;
return p;
}
void deallocate(pointer p, size_type n) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = allocations_.find(p);
if (it != allocations_.end()) {
allocations_.erase(it);
} else {
std::cerr << "Warning: Attempting to deallocate untracked memory at " << p << std::endl;
}
std::cout << "TrackingAllocator: Deallocated memory at " << p << std::endl;
::operator delete(p);
}
template <typename U>
struct rebind {
using other = TrackingAllocator<U>;
};
};
template <typename T, typename U>
bool operator==(const TrackingAllocator<T>&, const TrackingAllocator<U>&) noexcept {
return true;
}
template <typename T, typename U>
bool operator!=(const TrackingAllocator<T>&, const TrackingAllocator<U>&) noexcept {
return false;
}
int main() {
TrackingAllocator<int> allocator;
{
std::vector<int, TrackingAllocator<int>> vec(allocator);
for (int i = 0; i < 5; ++i) {
vec.push_back(i);
}
} // vec goes out of scope, memory is deallocated
// If there was a memory leak, the destructor of TrackingAllocator will report it.
return 0;
}
allocations_
: 一个std::map
,用于存储已分配的内存地址和大小。mutex_
: 一个互斥锁,用于保护allocations_
的线程安全。allocate
: 在分配内存后,将地址和大小添加到allocations_
中。deallocate
: 在释放内存后,从allocations_
中移除地址。- 析构函数: 在析构函数中检查
allocations_
是否为空。如果不为空,说明存在内存泄漏。
总结:内存分配,尽在掌握
自定义std::allocator
是一个强大的工具,它可以让你对容器的内存分配进行细粒度控制。虽然实现一个功能完善的allocator
需要一定的技巧,但掌握了基本原理后,你就可以根据自己的需求定制各种各样的allocator
,从而提高程序的性能、降低内存碎片、方便调试。
希望今天的脱口秀能让你对自定义std::allocator
有一个更深入的了解。记住,内存分配,尽在掌握!下次再见!