好的,下面是一篇关于C++自定义智能指针,包含特有资源管理、引用计数和线程安全的讲座式技术文章。
C++ 自定义智能指针:特有资源管理、引用计数与线程安全
大家好!今天我们来深入探讨C++中自定义智能指针的实现,重点关注如何进行特有资源管理、实现引用计数以及确保线程安全。标准库的智能指针(std::unique_ptr、std::shared_ptr和std::weak_ptr)已经提供了强大的功能,但在某些特定场景下,我们需要根据实际需求定制智能指针的行为。
1. 智能指针的基础:资源管理与RAII
智能指针的核心思想是RAII (Resource Acquisition Is Initialization),即资源获取即初始化。这意味着在对象构造时获取资源,在对象析构时释放资源。智能指针通过将原始指针封装在类中,并在类的析构函数中释放资源,从而自动管理内存,避免内存泄漏。
2. 为什么需要自定义智能指针?
虽然 std::unique_ptr 和 std::shared_ptr 已经非常有用,但它们并不能覆盖所有情况。以下是一些需要自定义智能指针的常见场景:
- 非
new/delete分配的资源: 例如,使用malloc/free分配的内存,或者通过其他API(例如文件句柄、网络socket等)获取的资源。 - 自定义的释放逻辑: 例如,需要调用特定的API来释放资源,而不是简单的
delete。 - 需要更精细的控制: 例如,需要跟踪资源的使用情况,或者在释放资源之前执行一些额外的操作。
- 特有的线程安全需求:标准库的
std::shared_ptr的引用计数是线程安全的,但对其指向的对象的访问并不是。在多线程环境中,如果需要对智能指针指向的对象进行并发访问,则需要额外的同步机制。
3. 实现一个简单的引用计数智能指针
首先,我们从一个简单的引用计数智能指针开始,逐步添加功能。
#include <iostream>
#include <atomic>
template <typename T>
class SimpleSharedPtr {
private:
T* ptr;
std::atomic<int>* count;
public:
// 构造函数
SimpleSharedPtr(T* p = nullptr) : ptr(p), count(nullptr) {
if (ptr) {
count = new std::atomic<int>(1);
}
}
// 拷贝构造函数
SimpleSharedPtr(const SimpleSharedPtr& other) : ptr(other.ptr), count(other.count) {
if (ptr) {
count->fetch_add(1, std::memory_order_relaxed); // increment reference count
}
}
// 赋值运算符
SimpleSharedPtr& operator=(const SimpleSharedPtr& other) {
if (this != &other) {
// 释放当前拥有的资源
release();
// 复制新的资源
ptr = other.ptr;
count = other.count;
if (ptr) {
count->fetch_add(1, std::memory_order_relaxed); // increment reference count
}
}
return *this;
}
// 析构函数
~SimpleSharedPtr() {
release();
}
// 解引用运算符
T& operator*() const {
return *ptr;
}
// 箭头运算符
T* operator->() const {
return ptr;
}
// 获取原始指针
T* get() const {
return ptr;
}
private:
void release() {
if (ptr && count->fetch_sub(1, std::memory_order_release) == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
delete ptr;
delete count;
ptr = nullptr;
count = nullptr;
}
}
};
int main() {
SimpleSharedPtr<int> ptr1(new int(10));
SimpleSharedPtr<int> ptr2 = ptr1;
SimpleSharedPtr<int> ptr3;
ptr3 = ptr1;
std::cout << *ptr1 << std::endl;
std::cout << *ptr2 << std::endl;
std::cout << *ptr3 << std::endl;
return 0;
}
这个例子展示了一个基本的引用计数智能指针。count 是一个 std::atomic<int>,用于线程安全地跟踪引用计数。拷贝构造函数和赋值运算符会增加引用计数,析构函数会减少引用计数并在计数变为零时释放资源。fetch_add、fetch_sub、memory_order_relaxed、memory_order_release和memory_order_acquire确保了引用计数的线程安全。
4. 处理自定义释放逻辑
如果我们需要使用自定义的释放逻辑,可以在智能指针中引入一个删除器 (deleter)。删除器是一个函数对象,用于释放智能指针管理的资源。
#include <iostream>
#include <atomic>
template <typename T, typename Deleter = std::default_delete<T>>
class CustomSharedPtr {
private:
T* ptr;
std::atomic<int>* count;
Deleter deleter;
public:
// 构造函数
CustomSharedPtr(T* p = nullptr, Deleter d = Deleter()) : ptr(p), count(nullptr), deleter(d) {
if (ptr) {
count = new std::atomic<int>(1);
}
}
// 拷贝构造函数
CustomSharedPtr(const CustomSharedPtr& other) : ptr(other.ptr), count(other.count), deleter(other.deleter) {
if (ptr) {
count->fetch_add(1, std::memory_order_relaxed); // increment reference count
}
}
// 赋值运算符
CustomSharedPtr& operator=(const CustomSharedPtr& other) {
if (this != &other) {
// 释放当前拥有的资源
release();
// 复制新的资源
ptr = other.ptr;
count = other.count;
deleter = other.deleter; // Copy the deleter
if (ptr) {
count->fetch_add(1, std::memory_order_relaxed); // increment reference count
}
}
return *this;
}
// 析构函数
~CustomSharedPtr() {
release();
}
// 解引用运算符
T& operator*() const {
return *ptr;
}
// 箭头运算符
T* operator->() const {
return ptr;
}
// 获取原始指针
T* get() const {
return ptr;
}
private:
void release() {
if (ptr && count->fetch_sub(1, std::memory_order_release) == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
deleter(ptr); // Use the custom deleter
delete count;
ptr = nullptr;
count = nullptr;
}
}
};
// 自定义删除器
struct FreeDeleter {
void operator()(void* ptr) {
std::free(ptr);
}
};
int main() {
// 使用 malloc 分配内存
int* data = (int*)std::malloc(sizeof(int));
*data = 20;
// 使用自定义删除器来释放内存
CustomSharedPtr<int, FreeDeleter> ptr(data, FreeDeleter());
std::cout << *ptr << std::endl;
return 0;
}
在这个例子中,CustomSharedPtr 接受一个模板参数 Deleter,默认为 std::default_delete<T>。在析构函数中,我们调用 deleter(ptr) 来释放资源。 FreeDeleter 是一个自定义的删除器,用于释放使用 malloc 分配的内存。
5. 更复杂的资源管理:文件句柄
现在,我们来看一个更复杂的例子:管理文件句柄。
#include <iostream>
#include <fstream>
#include <atomic>
class FileDeleter {
public:
void operator()(std::fstream* file) {
if (file->is_open()) {
file->close();
}
delete file;
std::cout << "File closed." << std::endl;
}
};
template <typename T, typename Deleter>
class GenericSharedPtr {
private:
T* ptr;
std::atomic<int>* count;
Deleter deleter;
public:
// 构造函数
GenericSharedPtr(T* p = nullptr, Deleter d = Deleter()) : ptr(p), count(nullptr), deleter(d) {
if (ptr) {
count = new std::atomic<int>(1);
}
}
// 拷贝构造函数
GenericSharedPtr(const GenericSharedPtr& other) : ptr(other.ptr), count(other.count), deleter(other.deleter) {
if (ptr) {
count->fetch_add(1, std::memory_order_relaxed); // increment reference count
}
}
// 赋值运算符
GenericSharedPtr& operator=(const GenericSharedPtr& other) {
if (this != &other) {
// 释放当前拥有的资源
release();
// 复制新的资源
ptr = other.ptr;
count = other.count;
deleter = other.deleter;
if (ptr) {
count->fetch_add(1, std::memory_order_relaxed); // increment reference count
}
}
return *this;
}
// 析构函数
~GenericSharedPtr() {
release();
}
// 解引用运算符
T& operator*() const {
return *ptr;
}
// 箭头运算符
T* operator->() const {
return ptr;
}
// 获取原始指针
T* get() const {
return ptr;
}
private:
void release() {
if (ptr && count->fetch_sub(1, std::memory_order_release) == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
deleter(ptr);
delete count;
ptr = nullptr;
count = nullptr;
}
}
};
int main() {
// 打开文件
std::fstream* file = new std::fstream("example.txt", std::ios::out);
if (file->is_open()) {
*file << "Hello, world!" << std::endl;
}
// 使用自定义删除器来管理文件句柄
GenericSharedPtr<std::fstream, FileDeleter> filePtr(file, FileDeleter());
//不需要手动关闭文件句柄, filePtr 析构时会自动关闭文件。
return 0;
}
在这个例子中,FileDeleter 负责关闭文件句柄。当 filePtr 析构时,FileDeleter 会被调用,确保文件被正确关闭。
6. 线程安全:深入探讨
虽然 std::atomic<int> 保证了引用计数的线程安全,但这并不意味着智能指针指向的对象也是线程安全的。如果多个线程同时访问智能指针指向的对象,仍然需要额外的同步机制,例如互斥锁。
我们可以将互斥锁集成到智能指针中,以提供更高级别的线程安全。
#include <iostream>
#include <atomic>
#include <mutex>
template <typename T>
class ThreadSafeSharedPtr {
private:
T* ptr;
std::atomic<int>* count;
std::mutex mutex; // 保护 ptr 指向的对象
public:
// 构造函数
ThreadSafeSharedPtr(T* p = nullptr) : ptr(p), count(nullptr) {
if (ptr) {
count = new std::atomic<int>(1);
}
}
// 拷贝构造函数
ThreadSafeSharedPtr(const ThreadSafeSharedPtr& other) : ptr(other.ptr), count(other.count) {
if (ptr) {
count->fetch_add(1, std::memory_order_relaxed); // increment reference count
}
}
// 赋值运算符
ThreadSafeSharedPtr& operator=(const ThreadSafeSharedPtr& other) {
if (this != &other) {
// 释放当前拥有的资源
release();
// 复制新的资源
ptr = other.ptr;
count = other.count;
if (ptr) {
count->fetch_add(1, std::memory_order_relaxed); // increment reference count
}
}
return *this;
}
// 析构函数
~ThreadSafeSharedPtr() {
release();
}
// 解引用运算符
T& operator*() const {
std::lock_guard<std::mutex> lock(mutex); // 加锁
return *ptr;
}
// 箭头运算符
T* operator->() const {
std::lock_guard<std::mutex> lock(mutex); // 加锁
return ptr;
}
// 获取原始指针
T* get() const {
return ptr;
}
private:
void release() {
if (ptr && count->fetch_sub(1, std::memory_order_release) == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
delete ptr;
delete count;
ptr = nullptr;
count = nullptr;
}
}
// 加锁
void lock() {
mutex.lock();
}
// 解锁
void unlock() {
mutex.unlock();
}
};
int main() {
ThreadSafeSharedPtr<int> ptr(new int(30));
// 模拟多线程访问
std::thread t1([&]() {
*ptr = 40;
});
std::thread t2([&]() {
std::cout << *ptr << std::endl;
});
t1.join();
t2.join();
return 0;
}
在这个例子中,ThreadSafeSharedPtr 包含一个 std::mutex,用于保护 ptr 指向的对象。operator*() 和 operator->() 会在访问对象之前加锁,确保线程安全。
7. 完整示例:自定义资源和线程安全
最后,我们将自定义释放逻辑和线程安全结合起来。
#include <iostream>
#include <atomic>
#include <mutex>
// 自定义资源:简单的计数器
class Counter {
private:
int value;
public:
Counter(int initialValue = 0) : value(initialValue) {}
int getValue() const {
return value;
}
void increment() {
value++;
}
};
// 自定义删除器
class CounterDeleter {
private:
std::mutex& mutex; // 引用外部互斥锁
public:
CounterDeleter(std::mutex& m) : mutex(m) {}
void operator()(Counter* counter) {
std::lock_guard<std::mutex> lock(mutex); // 加锁,确保线程安全
std::cout << "Deleting counter with value: " << counter->getValue() << std::endl;
delete counter;
}
};
// 线程安全的智能指针
template <typename T, typename Deleter>
class SafeSharedPtr {
private:
T* ptr;
std::atomic<int>* count;
Deleter deleter;
public:
// 构造函数
SafeSharedPtr(T* p, Deleter d) : ptr(p), count(nullptr), deleter(d) {
if (ptr) {
count = new std::atomic<int>(1);
}
}
// 拷贝构造函数
SafeSharedPtr(const SafeSharedPtr& other) : ptr(other.ptr), count(other.count), deleter(other.deleter) {
if (ptr) {
count->fetch_add(1, std::memory_order_relaxed); // increment reference count
}
}
// 赋值运算符
SafeSharedPtr& operator=(const SafeSharedPtr& other) {
if (this != &other) {
// 释放当前拥有的资源
release();
// 复制新的资源
ptr = other.ptr;
count = other.count;
deleter = other.deleter;
if (ptr) {
count->fetch_add(1, std::memory_order_relaxed); // increment reference count
}
}
return *this;
}
// 析构函数
~SafeSharedPtr() {
release();
}
// 解引用运算符
T& operator*() const {
return *ptr;
}
// 箭头运算符
T* operator->() const {
return ptr;
}
// 获取原始指针
T* get() const {
return ptr;
}
private:
void release() {
if (ptr && count->fetch_sub(1, std::memory_order_release) == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
deleter(ptr);
delete count;
ptr = nullptr;
count = nullptr;
}
}
};
int main() {
std::mutex mtx; // 外部互斥锁
Counter* counter = new Counter(100);
// 创建智能指针,使用自定义删除器和外部互斥锁
SafeSharedPtr<Counter, CounterDeleter> sharedCounter(counter, CounterDeleter(mtx));
// 模拟多线程操作
std::thread t1([&]() {
std::lock_guard<std::mutex> lock(mtx); // 加锁
sharedCounter->increment();
std::cout << "Thread 1: Value = " << sharedCounter->getValue() << std::endl;
});
std::thread t2([&]() {
std::lock_guard<std::mutex> lock(mtx); // 加锁
std::cout << "Thread 2: Value = " << sharedCounter->getValue() << std::endl;
});
t1.join();
t2.join();
return 0;
}
在这个例子中:
Counter是一个简单的自定义类,代表一个计数器。CounterDeleter是一个自定义删除器,在删除Counter对象之前会加锁,确保线程安全。它接收一个外部的互斥锁引用,并在删除对象时使用它。SafeSharedPtr是线程安全的智能指针,使用自定义删除器和外部互斥锁来管理Counter对象。- 主函数创建了一个
Counter对象,并使用SafeSharedPtr来管理它。两个线程同时访问和修改Counter对象,使用外部互斥锁来确保线程安全。
8. 总结:针对特定场景定制智能指针
通过以上示例,我们了解了如何自定义智能指针以满足特定的资源管理和线程安全需求。关键在于理解RAII原则,使用删除器处理自定义释放逻辑,以及使用 std::atomic 和互斥锁来确保线程安全。在实际开发中,应根据具体情况选择合适的智能指针实现方式,以提高代码的健壮性和可维护性。
智能指针的定制,是为了更好地管理资源,并提供线程安全保障,使得资源在多线程环境下也能被安全地共享和释放。
好的,下面是一些关于 C++ 自定义智能指针的表格,以更清晰地展示其特性和用法。
表格 1:智能指针类型比较
| 特性 | std::unique_ptr |
std::shared_ptr |
自定义智能指针 |
|---|---|---|---|
| 所有权 | 独占 | 共享 | 可以独占或共享,取决于实现 |
| 资源管理 | delete 或自定义删除器 |
引用计数 | 可以使用 delete、自定义删除器或其他资源释放方式 |
| 适用场景 | 独占所有权,避免内存泄漏 | 多个对象共享资源 | 特殊的资源管理需求,需要自定义行为 |
| 线程安全 | 不适用 | 引用计数线程安全 | 可以通过互斥锁等机制实现线程安全 |
| 额外开销 | 无 | 引用计数 | 可能有,取决于实现 |
| 实现复杂度 | 简单 | 较复杂 | 取决于自定义需求 |
表格 2:自定义删除器的使用场景
| 场景 | 描述 | 示例 |
|---|---|---|
非 new/delete 分配的内存 |
使用 malloc/free 分配的内存。 |
CustomSharedPtr<int, FreeDeleter> ptr(malloc(sizeof(int)), FreeDeleter()); |
| 需要调用特定 API 释放资源 | 例如,释放文件句柄、网络 socket 等。 | GenericSharedPtr<std::fstream, FileDeleter> filePtr(new std::fstream("example.txt"), FileDeleter()); |
| 在释放资源之前需要执行额外操作 | 例如,记录日志、发送通知等。 | CustomSharedPtr<T, LoggingDeleter<T>> ptr(new T(), LoggingDeleter<T>()); |
| 需要清理多个相关联的资源 | 例如,一个对象包含多个指针,需要在析构时一起释放。 | (需要在 Deleter 中处理多个资源的释放) |
| 使用C风格API,需要特殊的资源释放方式 | 例如,某些C API需要特定的函数来释放资源,而不是简单的delete或者free。 | (需要根据C API的要求定制Deleter) |
表格 3:线程安全智能指针的实现方式
| 实现方式 | 描述 | 优点 | 缺点 |
|---|---|---|---|
std::atomic |
使用 std::atomic 保证引用计数的线程安全。 |
简单易用,性能较好。 | 只能保证引用计数的线程安全,不能保护智能指针指向的对象。 |
| 互斥锁 | 使用 std::mutex 保护智能指针指向的对象。 |
可以保护智能指针指向的对象,提供更高级别的线程安全。 | 可能会引入死锁等问题,性能开销较大。 |
| 读写锁 | 使用 std::shared_mutex 或 std::shared_timed_mutex 实现读写分离。 |
允许多个线程同时读取对象,提高并发性能。 | 实现较复杂,需要仔细考虑读写锁的正确使用。 |
| 无锁数据结构 | 使用无锁数据结构来管理智能指针指向的对象。 | 避免了锁的开销,性能更高。 | 实现非常复杂,容易出错,需要深入理解无锁编程的原理。 |
| CAS原子操作 | 使用Compare-and-Swap(CAS)原子操作来更新资源。 | 避免了锁的开销,性能更高,可以实现无锁编程。 | 实现复杂,需要对原子操作有深入理解。 |
表格 4:自定义智能指针的设计原则
| 原则 | 描述 | 示例 |
|---|---|---|
| RAII | 资源获取即初始化,在对象构造时获取资源,在对象析构时释放资源。 | 智能指针在构造函数中获取资源,在析构函数中释放资源。 |
| 单一职责 | 智能指针只负责资源管理,不应该承担其他职责。 | 避免在智能指针中添加与资源管理无关的功能。 |
| 异常安全 | 智能指针应该保证在发生异常时,资源能够被正确释放。 | 在析构函数中使用 try-catch 块捕获异常,确保资源被释放。 |
| 避免循环引用 | 使用 std::weak_ptr 打破循环引用。 |
在对象之间存在循环引用时,使用 std::weak_ptr 观察对象,而不是使用 std::shared_ptr。 |
| 考虑线程安全 | 在多线程环境下,需要考虑智能指针的线程安全问题。 | 使用 std::atomic 保证引用计数的线程安全,使用互斥锁保护智能指针指向的对象。 |
| 避免资源泄露 | 确保在任何情况下,资源都能够被正确释放。 | 仔细检查代码,确保所有资源都能够被智能指针管理,避免手动释放资源。 |
| 选择合适的删除器 | 根据资源类型选择合适的删除器。 | 对于使用 malloc 分配的内存,使用 FreeDeleter 删除器。 |
这些表格提供了一个更结构化的方式来理解自定义智能指针的不同方面。
资源管理是核心,线程安全是保障
自定义智能指针的目的是为了更灵活地管理资源,并根据特定需求提供线程安全保障。 通过自定义删除器,我们可以处理各种类型的资源,并确保它们在不再需要时被正确释放。线程安全机制则保证了在多线程环境下,资源能够被安全地共享和访问。
更多IT精英技术系列讲座,到智猿学院