C++ std::shared_ptr 的自定义分配器
理解 std::shared_ptr
在C++中,std::shared_ptr
是智能指针家族的一员,它提供了一种自动管理动态分配对象生命周期的机制。std::shared_ptr
通过引用计数来跟踪有多少个std::shared_ptr
实例指向同一个对象。当引用计数降为0时,对象会被自动释放,从而避免了内存泄漏。
例如,下面是一个简单使用std::shared_ptr
的例子:
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> ptr = std::make_shared<int>(42);
std::cout << "Value: " << *ptr << std::endl;
return 0;
}
在上述代码中,std::make_shared<int>(42)
创建了一个指向int
类型对象(值为42)的std::shared_ptr
。std::cout
通过解引用ptr
来输出对象的值。
std::shared_ptr
的默认分配器
std::shared_ptr
在默认情况下使用std::allocator
来分配和释放内存。std::allocator
是C++标准库提供的默认内存分配器,它使用operator new
和operator delete
来进行内存的分配与释放。
例如,当我们使用std::make_shared
创建std::shared_ptr
时,std::make_shared
会使用默认的分配器来分配对象的内存:
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor" << std::endl; }
~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};
int main() {
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
return 0;
}
在这个例子中,std::make_shared<MyClass>()
使用默认分配器分配MyClass
对象的内存。当ptr
超出作用域时,MyClass
对象会被释放,调用其析构函数。
为什么需要自定义分配器
虽然默认分配器在大多数情况下工作得很好,但在某些特定场景下,我们可能需要使用自定义分配器:
- 内存池:在需要频繁分配和释放小块内存的场景中,使用内存池可以减少内存碎片,提高内存分配效率。自定义分配器可以从内存池中分配和释放内存。
- 特定的内存需求:例如,需要在特定的内存区域(如共享内存、显存等)分配内存,默认分配器无法满足这种需求,需要自定义分配器。
- 性能优化:在某些对性能要求极高的应用中,自定义分配器可以针对应用的特点进行优化,提高内存分配和释放的速度。
实现自定义分配器
自定义分配器的基本结构
自定义分配器需要满足一定的接口要求,它是一个类模板,至少需要实现以下成员类型和成员函数:
- 成员类型:
pointer
:指向分配对象类型的指针。const_pointer
:指向常量分配对象类型的指针。value_type
:分配对象的类型。size_type
:用于表示内存大小的无符号整数类型。difference_type
:用于表示两个指针之间距离的有符号整数类型。rebind<U>
:用于将分配器适配到不同类型的模板。
- 成员函数:
pointer allocate(size_type n)
:分配n
个value_type
对象所需的内存。void deallocate(pointer p, size_type n)
:释放由allocate
分配的内存,p
是指向要释放内存的指针,n
是对象的数量。template <class... Args> void construct(pointer p, Args&&... args)
:在p
指向的内存位置构造对象。void destroy(pointer p)
:销毁p
指向的对象。
下面是一个简单的自定义分配器示例,它只是简单地包装了std::malloc
和std::free
:
#include <iostream>
#include <memory>
template <typename T>
class MyAllocator {
public:
using pointer = T*;
using const_pointer = const T*;
using value_type = T;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
template <typename U>
struct rebind {
using other = MyAllocator<U>;
};
pointer allocate(size_type n) {
return static_cast<pointer>(std::malloc(n * sizeof(T)));
}
void deallocate(pointer p, size_type n) {
std::free(p);
}
template <class... Args>
void construct(pointer p, Args&&... args) {
new (p) T(std::forward<Args>(args)...);
}
void destroy(pointer p) {
p->~T();
}
};
使用自定义分配器与 std::shared_ptr
要使用自定义分配器与std::shared_ptr
,我们可以使用std::shared_ptr
的构造函数,它接受一个分配器对象作为参数。
#include <iostream>
#include <memory>
template <typename T>
class MyAllocator {
public:
using pointer = T*;
using const_pointer = const T*;
using value_type = T;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
template <typename U>
struct rebind {
using other = MyAllocator<U>;
};
pointer allocate(size_type n) {
return static_cast<pointer>(std::malloc(n * sizeof(T)));
}
void deallocate(pointer p, size_type n) {
std::free(p);
}
template <class... Args>
void construct(pointer p, Args&&... args) {
new (p) T(std::forward<Args>(args)...);
}
void destroy(pointer p) {
p->~T();
}
};
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor" << std::endl; }
~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};
int main() {
MyAllocator<MyClass> alloc;
std::shared_ptr<MyClass> ptr(alloc.allocate(1), [&alloc](MyClass* p) {
alloc.destroy(p);
alloc.deallocate(p, 1);
});
alloc.construct(ptr.get());
return 0;
}
在上述代码中,我们首先定义了MyAllocator
。然后在main
函数中,我们创建了一个MyAllocator<MyClass>
对象alloc
。接着,我们使用std::shared_ptr
的构造函数,第一个参数是通过alloc.allocate(1)
分配的内存,第二个参数是一个自定义的删除器,它调用alloc
的destroy
和deallocate
函数来释放资源。最后,我们使用alloc.construct
在分配的内存上构造MyClass
对象。
内存池分配器示例
内存池的概念
内存池是一种内存管理技术,它预先分配一块较大的内存块,然后将其划分成多个小块供程序使用。当程序需要分配内存时,直接从内存池中获取小块内存,而不是调用系统的内存分配函数(如malloc
)。当程序释放内存时,将小块内存返回给内存池,而不是释放回系统。这样可以减少内存碎片,提高内存分配效率。
实现内存池分配器
下面是一个简单的内存池分配器的实现:
#include <iostream>
#include <memory>
#include <vector>
class MemoryPool {
private:
std::vector<char> pool;
std::vector<bool> used;
size_t blockSize;
size_t numBlocks;
public:
MemoryPool(size_t blockSize, size_t numBlocks)
: blockSize(blockSize), numBlocks(numBlocks) {
pool.resize(blockSize * numBlocks);
used.resize(numBlocks, false);
}
void* allocate() {
for (size_t i = 0; i < numBlocks; ++i) {
if (!used[i]) {
used[i] = true;
return &pool[i * blockSize];
}
}
return nullptr;
}
void deallocate(void* p) {
size_t index = static_cast<char*>(p) - &pool[0];
index /= blockSize;
if (index < numBlocks) {
used[index] = false;
}
}
};
template <typename T>
class PoolAllocator {
private:
MemoryPool& pool;
public:
using pointer = T*;
using const_pointer = const T*;
using value_type = T;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
template <typename U>
struct rebind {
using other = PoolAllocator<U>;
};
PoolAllocator(MemoryPool& pool) : pool(pool) {}
PoolAllocator(const PoolAllocator& other) : pool(other.pool) {}
template <typename U>
PoolAllocator(const PoolAllocator<U>& other) : pool(other.pool) {}
pointer allocate(size_type n) {
if (n != 1) {
throw std::bad_alloc();
}
return static_cast<pointer>(pool.allocate());
}
void deallocate(pointer p, size_type n) {
if (n != 1) {
throw std::logic_error("Deallocation of multiple blocks not supported");
}
pool.deallocate(p);
}
template <class... Args>
void construct(pointer p, Args&&... args) {
new (p) T(std::forward<Args>(args)...);
}
void destroy(pointer p) {
p->~T();
}
};
使用内存池分配器与 std::shared_ptr
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor" << std::endl; }
~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};
int main() {
MemoryPool pool(sizeof(MyClass), 10);
PoolAllocator<MyClass> alloc(pool);
std::shared_ptr<MyClass> ptr(alloc.allocate(1), [&alloc](MyClass* p) {
alloc.destroy(p);
alloc.deallocate(p, 1);
});
alloc.construct(ptr.get());
return 0;
}
在上述代码中,我们首先定义了MemoryPool
类来管理内存池。然后定义了PoolAllocator
,它使用MemoryPool
进行内存分配和释放。在main
函数中,我们创建了一个MemoryPool
对象pool
,并基于此创建了PoolAllocator<MyClass>
对象alloc
。最后,我们使用std::shared_ptr
结合alloc
来管理MyClass
对象的生命周期。
自定义分配器与线程安全
多线程环境下的问题
在多线程环境中,使用自定义分配器需要特别注意线程安全问题。如果多个线程同时访问自定义分配器的共享资源(如内存池),可能会导致数据竞争和未定义行为。
例如,在前面的内存池分配器中,如果多个线程同时调用allocate
或deallocate
函数,used
向量和pool
向量可能会被同时修改,导致数据不一致。
解决线程安全问题
- 互斥锁:可以使用
std::mutex
来保护共享资源。在访问共享资源前,先锁定互斥锁,访问完成后解锁互斥锁。
#include <iostream>
#include <memory>
#include <vector>
#include <mutex>
class MemoryPool {
private:
std::vector<char> pool;
std::vector<bool> used;
size_t blockSize;
size_t numBlocks;
std::mutex mtx;
public:
MemoryPool(size_t blockSize, size_t numBlocks)
: blockSize(blockSize), numBlocks(numBlocks) {
pool.resize(blockSize * numBlocks);
used.resize(numBlocks, false);
}
void* allocate() {
std::lock_guard<std::mutex> lock(mtx);
for (size_t i = 0; i < numBlocks; ++i) {
if (!used[i]) {
used[i] = true;
return &pool[i * blockSize];
}
}
return nullptr;
}
void deallocate(void* p) {
std::lock_guard<std::mutex> lock(mtx);
size_t index = static_cast<char*>(p) - &pool[0];
index /= blockSize;
if (index < numBlocks) {
used[index] = false;
}
}
};
- 无锁数据结构:使用无锁数据结构来避免锁的开销。例如,使用无锁队列或无锁链表来管理内存池中的空闲块。这需要更复杂的实现,但可以提高多线程环境下的性能。
自定义分配器的局限性
- 可移植性:某些自定义分配器可能依赖于特定的操作系统或硬件平台,导致代码的可移植性较差。例如,在特定平台上使用特殊的内存映射技术实现的分配器,在其他平台上可能无法工作。
- 兼容性:自定义分配器需要与标准库容器和其他使用分配器的组件兼容。如果自定义分配器没有正确实现分配器接口,可能会导致与标准库组件的不兼容,从而出现难以调试的错误。
- 维护成本:实现和维护自定义分配器需要更多的精力。由于自定义分配器通常针对特定的应用场景进行优化,当应用场景发生变化时,可能需要对分配器进行大量的修改。
总结
自定义分配器为C++开发者提供了在内存管理方面的强大灵活性。通过理解std::shared_ptr
的默认分配器机制,以及掌握自定义分配器的实现方法,开发者可以根据应用的具体需求优化内存管理,提高程序的性能和效率。在多线程环境下,要特别注意自定义分配器的线程安全问题,选择合适的同步机制或无锁数据结构来保证数据的一致性。同时,也要清楚自定义分配器的局限性,权衡其带来的好处与可能增加的维护成本和兼容性问题。在实际应用中,应根据具体情况谨慎选择是否使用自定义分配器以及如何实现它。