18、【C++】智能指针
/ ... 若此处抛出异常,ptr未释放,导致内存泄漏delete ptr;智能指针通过RAII(资源获取即初始化)机制,在对象生命周期结束时自动释放资源,避免内存泄漏。删除器用于指定智能指针释放资源的方式,适用于非new分配的资源(如malloc、文件句柄)。:不可移动的unique_ptr。:侵入式引用计数,对象自带计数成员。
18、【C++】智能指针
目录
- 一、智能指针概述
- 二、std::unique_ptr
- 三、std::shared_ptr
- 四、std::weak_ptr
- 五、自定义删除器(Custom Deleter)
- 六、智能指针的异常安全
- 七、智能指针的应用场景
- 八、常见误区与注意事项
- 九、扩展内容
一、智能指针概述
1.1 为什么需要智能指针(内存泄漏问题)
动态内存管理中,程序员常因忘记释放内存或异常抛出导致内存泄漏:
void leak() {
int* ptr = new int(10);
// ... 若此处抛出异常,ptr未释放,导致内存泄漏
delete ptr;
}
智能指针通过RAII(资源获取即初始化) 机制,在对象生命周期结束时自动释放资源,避免内存泄漏。
1.2 RAII原理
RAII核心思想:将资源的生命周期与对象的生命周期绑定,对象构造时获取资源,析构时释放资源。智能指针是RAII的典型应用:
template <typename T>
class SmartPtr {
public:
SmartPtr(T* ptr) : _ptr(ptr) {} // 获取资源
~SmartPtr() { delete _ptr; } // 释放资源
private:
T* _ptr;
};
void safe() {
SmartPtr<int> ptr(new int(10)); // 资源与ptr绑定
// ... 异常抛出时,ptr析构,资源释放
}
1.3 智能指针的分类
1.3.1 独占所有权(unique_ptr)
- 特性:同一时间只能有一个智能指针拥有资源所有权。
- 不可拷贝:拷贝构造和赋值运算符被删除,仅支持移动。
- 适用场景:独占资源(如动态数组、文件句柄)。
1.3.2 共享所有权(shared_ptr)
- 特性:多个智能指针可共享同一资源,通过引用计数管理资源释放。
- 引用计数:当引用计数为0时,释放资源。
- 适用场景:共享资源(如多线程共享数据)。
1.3.3 弱引用(weak_ptr)
- 特性:不拥有资源所有权,不影响引用计数,用于解决shared_ptr的循环引用。
- 依赖shared_ptr:需从shared_ptr创建,通过lock()获取可用的shared_ptr。
二、std::unique_ptr
2.1 unique_ptr的特性(独占所有权)
- 独占性:同一资源只能被一个unique_ptr拥有。
- 不可拷贝:
unique_ptr(const unique_ptr&) = delete。 - 可移动:支持通过
std::move转移所有权。 - 大小优化:与裸指针大小相同(通常8字节),删除器作为模板参数可能增加大小。
2.2 unique_ptr的基本用法
2.2.1 创建与初始化
#include <memory>
// 创建unique_ptr
std::unique_ptr<int> up1(new int(10)); // C++11
auto up2 = std::make_unique<int>(20); // C++14(推荐,异常安全)
推荐使用std::make_unique:避免资源泄露(如unique_ptr(new int, deleter)在构造时可能抛出异常)。
2.2.2 所有权转移(std::move)
std::unique_ptr<int> up1 = std::make_unique<int>(10);
// std::unique_ptr<int> up2 = up1; // 错误:不可拷贝
std::unique_ptr<int> up2 = std::move(up1); // 正确:转移所有权,up1变为nullptr
2.2.3 释放所有权(release)
std::unique_ptr<int> up = std::make_unique<int>(10);
int* raw_ptr = up.release(); // 释放所有权,up变为nullptr
delete raw_ptr; // 需手动释放资源
2.3 unique_ptr与数组
unique_ptr支持动态数组,自动使用delete[]释放:
// 创建数组
std::unique_ptr<int[]> up(new int[5]); // C++11
auto up_arr = std::make_unique<int[]>(5); // C++14
// 访问元素
up_arr[0] = 1;
up_arr[1] = 2;
2.4 unique_ptr的删除器(Deleter)
unique_ptr可指定删除器(释放资源的函数/对象),用于非new分配的资源:
// 函数指针作为删除器
void file_deleter(FILE* fp) {
fclose(fp);
std::cout << "file closed" << std::endl;
}
// 创建时指定删除器
std::unique_ptr<FILE, decltype(&file_deleter)> up(fopen("data.txt", "r"), file_deleter);
// lambda作为删除器(C++11+)
auto deleter = [](FILE* fp) { fclose(fp); };
std::unique_ptr<FILE, decltype(deleter)> up2(fopen("data.txt", "r"), deleter);
2.5 unique_ptr与auto_ptr的区别
std::auto_ptr是C++98的智能指针,因设计缺陷(拷贝时转移所有权)已被废弃:
std::auto_ptr<int> ap1(new int(10));
std::auto_ptr<int> ap2 = ap1; // ap1失去所有权,访问ap1会UB
unique_ptr通过禁用拷贝解决此问题,更安全。
2.6 unique_ptr的实现原理
unique_ptr的核心是移动语义和删除器封装:
template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
public:
// 构造函数
explicit unique_ptr(T* ptr = nullptr) : _ptr(ptr) {}
// 移动构造
unique_ptr(unique_ptr&& other) noexcept : _ptr(other._ptr) {
other._ptr = nullptr; // 源对象释放所有权
}
// 移动赋值
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
_deleter(_ptr); // 释放当前资源
_ptr = other._ptr;
other._ptr = nullptr;
}
return *this;
}
// 析构函数
~unique_ptr() {
if (_ptr) _deleter(_ptr);
}
// 禁止拷贝
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
private:
T* _ptr;
Deleter _deleter;
};
三、std::shared_ptr
3.1 shared_ptr的特性(共享所有权)
- 共享性:多个shared_ptr可指向同一资源。
- 引用计数:通过引用计数跟踪拥有资源的shared_ptr数量。
- 线程安全:引用计数的增减是原子操作,线程安全。
3.2 引用计数(Reference Counting)
3.2.1 引用计数的变化
- 创建:引用计数=1。
- 拷贝:引用计数+1。
- 析构:引用计数-1,若计数=0,释放资源。
3.2.2 查看引用计数(use_count)
auto sp1 = std::make_shared<int>(10);
std::cout << sp1.use_count() << std::endl; // 1
auto sp2 = sp1; // 拷贝,引用计数+1
std::cout << sp1.use_count() << std::endl; // 2
std::cout << sp2.use_count() << std::endl; // 2
3.3 shared_ptr的基本用法
3.3.1 创建与初始化
// C++11
std::shared_ptr<int> sp1(new int(10));
// C++11(推荐)
auto sp2 = std::make_shared<int>(20); // 效率更高,控制块与对象同分配
std::make_shared优势:一次性分配对象和控制块内存,减少内存碎片。
3.3.2 拷贝与赋值
auto sp1 = std::make_shared<int>(10);
auto sp2 = sp1; // 拷贝,引用计数=2
std::shared_ptr<int> sp3;
sp3 = sp1; // 赋值,引用计数=3
3.3.3 重置与释放(reset)
auto sp = std::make_shared<int>(10);
sp.reset(); // 引用计数-1,若=0则释放资源,sp变为nullptr
sp.reset(new int(20)); // 释放旧资源,管理新资源,引用计数=1
3.4 shared_ptr的控制块(Control Block)
shared_ptr的引用计数存储在控制块中,包含:
- 强引用计数:拥有资源的shared_ptr数量。
- 弱引用计数:引用资源的weak_ptr数量。
- 删除器:释放资源的函数对象。
- 分配器:内存分配相关。
控制块创建时机:
std::make_shared:同时创建对象和控制块。shared_ptr(new T):先创建对象,再创建控制块。
3.5 shared_ptr的线程安全性
- 引用计数:原子操作,线程安全。
- 指向的对象:非线程安全,需额外同步(如互斥锁)。
// 线程安全:引用计数增减
auto sp = std::make_shared<int>(10);
std::thread t1([sp]() { auto sp2 = sp; });
std::thread t2([sp]() { auto sp3 = sp; });
// 线程不安全:对象修改
auto sp = std::make_shared<int>(10);
std::thread t1([sp]() { *sp = 20; });
std::thread t2([sp]() { *sp = 30; }); // 数据竞争
3.6 shared_ptr的循环引用问题
两个shared_ptr相互引用,导致引用计数无法归零,资源泄漏:
struct Node {
std::shared_ptr<Node> next;
};
int main() {
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2; // n1引用n2,n2引用计数=2
n2->next = n1; // n2引用n1,n1引用计数=2
// 析构时n1和n2引用计数=1,无法释放,内存泄漏
return 0;
}
解决:使用std::weak_ptr打破循环引用。
3.7 shared_ptr的实现原理
template <typename T>
class shared_ptr {
public:
// 构造函数
shared_ptr(T* ptr) : _ptr(ptr) {
_control_block = new ControlBlock();
_control_block->strong_count = 1;
}
// 拷贝构造
shared_ptr(const shared_ptr& other) {
_ptr = other._ptr;
_control_block = other._control_block;
_control_block->strong_count++;
}
// 析构函数
~shared_ptr() {
if (--_control_block->strong_count == 0) {
delete _ptr; // 释放资源
if (_control_block->weak_count == 0) {
delete _control_block; // 释放控制块
}
}
}
private:
T* _ptr; // 指向资源
struct ControlBlock {
int strong_count; // 强引用计数
int weak_count; // 弱引用计数
}* _control_block;
};
四、std::weak_ptr
4.1 weak_ptr的特性(弱引用)
- 无所有权:不增加强引用计数,不影响资源释放。
- 依赖shared_ptr:需从shared_ptr创建。
- 解决循环引用:打破shared_ptr的相互引用。
4.2 weak_ptr的基本用法
4.2.1 lock()获取shared_ptr
lock()返回指向资源的shared_ptr,若资源已释放则返回nullptr:
auto sp = std::make_shared<int>(10);
std::weak_ptr<int> wp = sp; // 从shared_ptr创建weak_ptr,强引用计数不变
// 获取shared_ptr
if (auto locked = wp.lock()) { // locked是shared_ptr<int>
std::cout << *locked << std::endl; // 10
} else {
std::cout << "资源已释放" << std::endl;
}
4.2.2 expired()判断对象是否存活
std::weak_ptr<int> wp;
{
auto sp = std::make_shared<int>(10);
wp = sp;
std::cout << wp.expired() << std::endl; // false(资源存活)
}
std::cout << wp.expired() << std::endl; // true(资源已释放)
4.3 weak_ptr解决循环引用
修改3.6节的循环引用示例:
struct Node {
std::weak_ptr<Node> next; // 使用weak_ptr
};
int main() {
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2; // weak_ptr不增加强引用计数
n2->next = n1; // weak_ptr不增加强引用计数
// 析构时n1和n2的强引用计数=1-1=0,资源释放
return 0;
}
4.4 weak_ptr的应用场景
4.4.1 观察者模式
观察者通过weak_ptr观察主题,主题销毁后观察者自动失效:
class Subject;
class Observer {
private:
std::weak_ptr<Subject> _subject; // 弱引用主题
};
class Subject {
private:
std::vector<std::weak_ptr<Observer>> _observers; // 存储弱引用观察者
};
4.4.2 缓存机制
缓存对象通过weak_ptr管理,当对象不被使用时自动释放:
std::unordered_map<Key, std::weak_ptr<Value>> cache;
std::shared_ptr<Value> get_value(const Key& key) {
if (auto cached = cache[key].lock()) {
return cached; // 返回缓存对象
}
auto value = std::make_shared<Value>();
cache[key] = value; // 缓存weak_ptr
return value;
}
五、自定义删除器(Custom Deleter)
删除器用于指定智能指针释放资源的方式,适用于非new分配的资源(如malloc、文件句柄)。
5.1 删除器的作用
- 释放资源:调用自定义函数释放资源(如
fclose、free)。 - 数组支持:
std::default_delete[]用于数组释放。
5.2 函数对象作为删除器
struct FileDeleter {
void operator()(FILE* fp) const {
if (fp) {
fclose(fp);
std::cout << "file closed" << std::endl;
}
}
};
std::unique_ptr<FILE, FileDeleter> up(fopen("data.txt", "r"));
5.3 lambda表达式作为删除器
auto deleter = [](int* p) {
std::cout << "deleting int: " << *p << std::endl;
delete p;
};
std::unique_ptr<int, decltype(deleter)> up(new int(10), deleter);
std::shared_ptr<int> sp(new int(20), deleter); // shared_ptr删除器不影响类型
5.4 数组删除器(std::default_delete[])
智能指针默认使用std::default_delete(调用delete),数组需指定std::default_delete[]:
std::unique_ptr<int[], std::default_delete<int[]>> up(new int[5]); // 数组删除器
auto sp = std::shared_ptr<int>(new int[5], std::default_delete<int[]>());
六、智能指针的异常安全
智能指针确保异常抛出时资源释放:
void safe_operation() {
auto sp = std::make_shared<int>(10);
risky_operation(); // 抛出异常时,sp析构,资源释放
}
与try-catch配合:
try {
auto sp = std::make_shared<Resource>();
throw std::runtime_error("error");
} catch (const std::exception& e) {
// sp已析构,资源释放
}
七、智能指针的应用场景
7.1 unique_ptr的应用
- 独占资源:动态数组、文件句柄、网络连接。
- 工厂函数返回值:返回对象所有权。
std::unique_ptr<Product> create_product() {
return std::make_unique<ConcreteProduct>();
}
7.2 shared_ptr的应用
- 共享资源:多线程共享数据、大型对象共享访问。
- 容器存储:存储动态对象,自动管理生命周期。
std::vector<std::shared_ptr<Shape>> shapes;
shapes.push_back(std::make_shared<Circle>());
shapes.push_back(std::make_shared<Square>());
7.3 weak_ptr的应用
- 解决循环引用:观察者模式、树结构父节点引用子节点。
- 临时访问共享资源:不影响资源生命周期的临时访问。
八、常见误区与注意事项
8.1 shared_ptr的循环引用
问题:两个shared_ptr相互引用,导致资源泄漏。
解决:使用weak_ptr打破循环。
8.2 将this指针托管给shared_ptr
问题:对象未被shared_ptr管理时,将this托管会导致二次释放。
解决:继承std::enable_shared_from_this:
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
std::shared_ptr<MyClass> get_shared_ptr() {
return shared_from_this(); // 安全获取this的shared_ptr
}
};
8.3 删除器不匹配
问题:使用错误删除器导致未定义行为(如用delete释放new[]数组)。
解决:数组使用std::default_delete[]。
8.4 过度使用shared_ptr
问题:所有场景都用shared_ptr,导致不必要的引用计数开销。
解决:优先使用unique_ptr,必要时才用shared_ptr。
九、扩展内容
9.1 std::auto_ptr的缺陷(已废弃)
- 拷贝转移所有权:导致悬挂指针。
- 不支持数组:删除数组时调用
delete而非delete[]。
9.2 boost智能指针简介
Boost库提供更多智能指针:
boost::scoped_ptr:不可移动的unique_ptr。boost::intrusive_ptr:侵入式引用计数,对象自带计数成员。
9.3 自定义智能指针(简易实现)
实现一个简化的unique_ptr:
template <typename T>
class MyUniquePtr {
public:
MyUniquePtr(T* ptr = nullptr) : _ptr(ptr) {}
~MyUniquePtr() { delete _ptr; }
MyUniquePtr(MyUniquePtr&& other) noexcept : _ptr(other._ptr) {
other._ptr = nullptr;
}
MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
if (this != &other) {
delete _ptr;
_ptr = other._ptr;
other._ptr = nullptr;
}
return *this;
}
T& operator*() const { return *_ptr; }
T* operator->() const { return _ptr; }
MyUniquePtr(const MyUniquePtr&) = delete;
MyUniquePtr& operator=(const MyUniquePtr&) = delete;
private:
T* _ptr;
};
以上内容,全面覆盖了C++智能指针的核心知识点,包括unique_ptr、shared_ptr、weak_ptr的特性、用法、实现原理及应用场景。合理使用智能指针可显著提高代码的安全性和可读性,减少内存泄漏。
更多推荐



所有评论(0)