C++11 智能指针:从原理到实现
文章目录
- 一、引言
- 二、智能指针的核心原理:RAII
- 三、智能指针的基本使用
- 1. `std::unique_ptr`
- 2. `std::shared_ptr`
- 3. `std::weak_ptr`
- 三、智能指针的实现原理
- 1. `std::unique_ptr` 的实现
- 2. `std::shared_ptr` 的实现
- 3. `std::weak_ptr` 的实现
- 四、`shared_ptr`的线程安全问题
- 1. 引用计数的线程安全
- 2. 指针本身的线程安全
- 3. 解决方案
一、引言
在传统 C++ 中,动态内存管理一直是个头疼的问题。程序员需要手动使用 new
和 delete
来分配和释放内存,稍有不慎就会导致内存泄漏、悬空指针等问题。特别是在异常处理的场景中,资源释放的逻辑很容易被忽略。
C++11 引入的智能指针(Smart Pointer)通过 RAII(Resource Acquisition Is Initialization)技术,彻底改变了这一局面。它让内存管理变得自动化、安全化,大大降低了程序出错的概率。本文将深入探讨 C++11 中三种主要的智能指针:unique_ptr
、shared_ptr
和 weak_ptr
,并通过代码示例展示它们的实现原理。
二、智能指针的核心原理:RAII
RAII 是 C++ 中管理资源的一种重要编程范式,其核心思想是:资源在对象构造时获取,在对象析构时释放。智能指针正是基于这一思想,将动态内存的生命周期与对象的生命周期绑定在一起。
当我们使用智能指针管理内存时,只需要在构造时传入一个原始指针,智能指针会在其生命周期结束时自动释放该内存。即使在异常发生的情况下,栈展开(Stack Unwinding)过程也会确保智能指针的析构函数被调用,从而避免内存泄漏。
三、智能指针的基本使用
1. std::unique_ptr
unique_ptr
是一种独占式智能指针,确保同一时间只有一个智能指针指向该对象。它禁止拷贝,但支持移动语义。
创建与基本操作:
#include <memory>
#include <iostream>void unique_ptr_demo() {// 创建 unique_ptrstd::unique_ptr<int> ptr1 = std::make_unique<int>(42); // C++14 推荐写法std::unique_ptr<int> ptr2(new int(100)); // 传统写法// 使用解引用和箭头操作符std::cout << *ptr1 << std::endl; // 输出: 42// 移动所有权std::unique_ptr<int> ptr3 = std::move(ptr1); // ptr1 变为空if (!ptr1) {std::cout << "ptr1 is empty" << std::endl;}// 获取原始指针(谨慎使用)int* raw_ptr = ptr3.get();// 释放所有权int* released = ptr3.release();delete released; // 需要手动释放// 重置指针ptr2.reset(); // 释放内存,ptr2 变为空
}
管理动态数组:
std::unique_ptr<int[]> arr = std::make_unique<int[]>(5);
for (int i = 0; i < 5; ++i) {arr[i] = i;
}
2. std::shared_ptr
shared_ptr
是一种共享式智能指针,使用引用计数来管理对象的生命周期。多个 shared_ptr
可以指向同一个对象,当最后一个 shared_ptr
被销毁时,对象才会被释放。
创建与引用计数:
void shared_ptr_demo() {// 创建 shared_ptrstd::shared_ptr<int> sp1 = std::make_shared<int>(10); // 推荐写法std::shared_ptr<int> sp2(new int(20)); // 传统写法// 拷贝构造和赋值std::shared_ptr<int> sp3 = sp1; // 引用计数+1sp2 = sp1; // sp2 原来的对象被释放,引用计数+1// 获取引用计数std::cout << "Use count: " << sp1.use_count() << std::endl; // 输出: 3// 自定义删除器(例如释放文件句柄)std::shared_ptr<FILE> file(fopen("test.txt", "r"), [](FILE* f) {if (f) fclose(f);});
} // 所有 shared_ptr 离开作用域,对象被释放
3. std::weak_ptr
weak_ptr
是一种弱引用智能指针,不控制对象的生命周期,用于解决 shared_ptr
的循环引用问题。
基本使用:
void weak_ptr_demo() {std::shared_ptr<int> sp = std::make_shared<int>(100);std::weak_ptr<int> wp = sp; // 不增加引用计数// 检查对象是否存在if (auto locked = wp.lock()) { // 转换为 shared_ptrstd::cout << *locked << std::endl; // 对象存在} else {std::cout << "Object expired" << std::endl;}// 查看引用计数std::cout << "Use count: " << wp.use_count() << std::endl; // 输出: 1
} // sp 离开作用域,对象被释放
解决循环引用:
class B;class A {
public:std::shared_ptr<B> b_ptr;~A() { std::cout << "A destroyed" << std::endl; }
};class B {
public:std::weak_ptr<A> a_ptr; // 使用 weak_ptr 避免循环引用~B() { std::cout << "B destroyed" << std::endl; }
};void test_cycle() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();a->b_ptr = b;b->a_ptr = a; // 不会导致循环引用
} // a 和 b 都能正确释放
三、智能指针的实现原理
1. std::unique_ptr
的实现
unique_ptr
的核心是禁止拷贝,只允许移动语义,并在析构时释放资源。
所以在实现 unique_ptr
时,必须禁用拷贝构造和拷贝赋值,并显式实现移动构造、移动赋值和资源管理,以确保独占资源的正确性和安全性。
template<class T>class unique_ptr {public:unique_ptr(T* ptr = nullptr) : _ptr(ptr) {}unique_ptr(unique_ptr&& other) : _ptr(other._ptr) {other._ptr = nullptr;}~unique_ptr() {delete _ptr;}unique_ptr(const unique_ptr&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;unique_ptr& operator=(unique_ptr&& other) {if (this != &other) {delete _ptr;_ptr = other._ptr;other._ptr = nullptr;}return *this;}T& operator*() const {return *_ptr;}T* operator->() const {return _ptr;}T* get() const {return _ptr;}T* release() {T* temp = _ptr;_ptr = nullptr;return temp;}void reset(T* ptr = nullptr) {if (_ptr) {delete _ptr;}_ptr = ptr;}private:T* _ptr;};
2. std::shared_ptr
的实现
shared_ptr
的核心是引用计数。
主要就是当执行拷贝构造、拷贝赋值的时候让计数器++
,而当析构的时候让计数器--
。如果计数器为 0 就释放掉资源。
template<class T>class shared_ptr {public:shared_ptr(T* ptr): _ptr(ptr), _pcount(new int(1)){}template<class D>shared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new int(1)), _del(del){}~shared_ptr() {if (--(*_pcount) == 0) {_del(_ptr);delete _pcount;}}shared_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}shared_ptr<T>& operator=(const shared_ptr<T>& sp) {if (_ptr != sp._ptr) {if (--(*_pcount) == 0) {_del(_ptr);delete _pcount;}_pcount = sp._pcount;_ptr = sp._ptr;++(*_pcount);}return *this;}T& operator*() {return *_ptr;}T* operator->() {return _ptr;}int use_count() const {return *_pcount;}private:T* _ptr;int* _pcount;std::function<void(T*)> _del = [](T* ptr) { delete ptr; };};
3. std::weak_ptr
的实现
weak_ptr
的核心是弱引用机制,通过独立于 shared_ptr
的引用计数(弱计数)实现对资源的非拥有性访问,主要用于解决循环引用问题。
下面的代码现在看不懂没关系,船到桥头自然直。
template<class T>class weak_ptr {private:T* _ptr;std::atomic<int>* _pcount;std::atomic<int>* _wcount;public:weak_ptr() : _ptr(nullptr), _pcount(nullptr), _wcount(nullptr) {}weak_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pcount(sp._pcount), _wcount(new std::atomic<int>(1)){}weak_ptr(const weak_ptr& wp): _ptr(wp._ptr), _pcount(wp._pcount), _wcount(wp._wcount){if (_wcount) {(*_wcount)++;}}~weak_ptr() {if (_wcount && --(*_wcount) == 0) {if (_pcount && *_pcount == 0) {delete _wcount;}}}weak_ptr& operator=(const shared_ptr<T>& sp) {if (_wcount && --(*_wcount) == 0) {if (_pcount && *_pcount == 0) {delete _wcount;}}_ptr = sp._ptr;_pcount = sp._pcount;_wcount = new std::atomic<int>(1);return *this;}weak_ptr& operator=(const weak_ptr& wp) {if (this != &wp) {if (_wcount && --(*_wcount) == 0) {if (_pcount && *_pcount == 0) {delete _wcount;}}_ptr = wp._ptr;_pcount = wp._pcount;_wcount = wp._wcount;if (_wcount) {(*_wcount)++;}}return *this;}shared_ptr<T> lock() const {if (expired()) {return shared_ptr<T>(nullptr);}return shared_ptr<T>(_ptr);}bool expired() const {return !_pcount || *_pcount == 0;}int use_count() const {return _pcount ? *_pcount : 0;}};
四、shared_ptr
的线程安全问题
std::shared_ptr
的线程安全问题需要从 引用计数的原子性 和 指针本身的访问语义 两方面分析。
1. 引用计数的线程安全
shared_ptr
的核心机制是通过 引用计数 管理资源生命周期,其引用计数的操作具有 原子性(由 std::atomic
保证)。具体规则如下:
- 原子性操作
- 引用计数的递增 / 递减(如拷贝构造、赋值、析构)是 原子操作,多线程中无需额外同步。
- 引用计数的查询(如 use_count())是 非原子操作,可能读取到中间状态(但不会导致程序崩溃,仅可能返回过时值)。
- 安全场景
- 多个线程同时读取
shared_ptr
对象(如调用use_count()
、解引用*ptr
)是安全的,不会导致数据竞争。 - 一个线程修改
shared_ptr
(如赋值、析构),其他线程读取:由于引用计数操作是原子的,不会导致shared_ptr
对象本身的不一致(但需注意管理对象的线程安全)。
- 多个线程同时读取
2. 指针本身的线程安全
shared_ptr
管理的 目标对象(*_ptr
)的访问 不保证线程安全,需开发者自行处理同步:
- 数据竞争风险
如果多个线程通过shared_ptr
访问同一对象的 可变状态,且未加同步机制,会导致数据竞争:
std::shared_ptr<MyClass> ptr;// 线程 A
ptr->x = 10; // 危险:未加锁,若线程 B 同时修改 x,发生数据竞争// 线程 B
int val = ptr->x; // 危险:与线程 A 竞争
3. 解决方案
库里面的 std::shared_ptr
是保证了多个线程同时修改引用计数是原子的,不会导致数据竞争;引用计数的读取操作是安全的,但非原子,可能会读到中间状态。
我们上面实现的 shared_ptr
的 _pcount
的类型是 int*
,所以它的引用计数的读取不是线程安全的,可以将它改成 atomic<int>*
类型,用于保证引用计数的线程安全。当然,既然是多线程,那么使用互斥枷锁解决线程安全问题也可以(具体的等后面多线程部分再讲)。
// 定义的时候:std::atomic<int>* _pcount;// 构造初始化的时候shared_ptr(T* ptr): _ptr(ptr), _pcount(new std::atomic<int>(1)){}