标题:[C++] 智能指针 进阶
水墨不写bug
在很久之前我们探讨了智能指针
的浅显认识,接下来会更加深入,从源码角度认识智能指针,从而了解智能指针的设计原理,并应用到以后的工作项目中。
本文将会按照C++智能指针的发展历史,回顾智能指针的各个版本的设计。后半部分将会模拟实现最常考的shared_ptr的功能。
一、C++智能指针的发展历程
1. C++98时代:auto_ptr
的尝试与缺陷
背景:C++早期依赖手动new
/delete
,稍微有管理不当比如忘记释放
,或者有异常抛出
(退出调用的函数栈,但是堆区的指针却找不到了),就可能导致内存泄漏。C++98引入auto_ptr
,首次尝试自动化资源管理。
机制:基于RAII(资源获取即初始化),在构造时获取内存,在析构时自动释放内存
。
问题:
- 隐式所有权转移:复制
auto_ptr
时,原指针变为nullptr
,导致难以追踪的悬空指针。 - 不兼容容器:因拷贝语义异常,无法安全用于STL容器。
示例:
auto_ptr<int> p1(new int(32));
auto_ptr<int> p2 = p1; // p1变为nullptr,后续使用p1导致未定义行为
结局:最终C++11弃用,C++17移除。因此为了代码的逻辑性和健壮性,以及可移植性,非常不建议使用auto_ptr
。
2. Boost库的智能指针
背景:C++社区通过Boost库探索更健壮的解决方案,后来这些方案被C++委员会收录到了C++11
。
关键类型:
scoped_ptr
:禁止拷贝,严格独占所有权,轻量且安全。shared_ptr
:引用计数实现共享所有权,解决多所有者场景。weak_ptr
:打破shared_ptr
循环引用,防止内存泄漏。
影响:直接为C++11智能指针奠定基础。
3. C++11
:C++划时代的进步
核心类型:
unique_ptr
:取代auto_ptr
,独占所有权,支持移动语义,禁止拷贝,可管理数组(unique_ptr<T[]>
)。unique_ptr<int> p1(new int(10)); unique_ptr<int> p2 = std::move(p1); // 显式所有权转移
shared_ptr
:引用计数共享资源,线程安全但性能有开销。shared_ptr<A> a = make_shared<A>(); shared_ptr<A> b = a; // 引用计数增至2
weak_ptr
:可通过其获取对应的shared_ptr
资源,不增加计数,需通过lock()
获取临时shared_ptr
。weak_ptr<A> w = a; if (shared_ptr<A> tmp = w.lock()) { /* 使用tmp */ }
改进:
- 弃用
auto_ptr
,推荐unique_ptr
。 - 引入
make_shared
:合并控制块与对象内存分配,提升性能与异常安全。
4. C++14:完善与便利性增强
make_unique
:填补make_shared
的对称性,安全构造unique_ptr
。auto p = make_unique<int>(20); // 避免显式new
shared_ptr
增强:支持自定义删除器与分配器,更灵活的资源管理。
后期的C++17用法不多常见,在此就不再赘述了。
二、智能指针的模拟实现
1、unique_ptr
template<class T>
class UniquePtr
{UniquePtr<T>& operator=(const UniquePtr<T>&) = delete;//删除拷贝构造和赋值重载UniquePtr(const UniquePtr<T>&) = delete;//这是unique_ptr的标志性的特点
public:UniquePtr(T* ptr):_ptr(ptr)//浅拷贝//这就要求我们需要在外部new出堆区的空间,然后传递给智能指针来管理{}~UniquePtr(){if(_ptr)//析构delete _ptr;}T& operator*()//重载*和->让智能指针(类)像普通指针(自定义类型)一样使用{return *_ptr;}T* operator->(){return ptr;}
private:T* _ptr;
};
2、shared_ptr
template<class T>
class SharedPtr
{
public:SharedPtr(T* ptr):_ptr(ptr)//要求从外部传一个指向堆区的指针,_refcount(new int{1})//开辟在堆上,便于所有的管理同一资源的智能指针对象维护,_pmtx(new std::mutex)//同样的原因{}SharedPtr(const SharedPtr<T>& obj):_ptr(obj._ptr)//只需要浅拷贝,然后引用计数++,_refcount(obj._refcount),_pmtx(obj._pmtx){AddRef();}SharedPtr<T>& operator=(const SharedPtr<T>& obj){if(obj._ptr != _ptr)//不能给自身赋值{Release();//释放掉被赋值的智能指针对象的原有资源//浅拷贝_ptr = obj._ptr;//两个指针指向同一个T*对象_refcount = obj._refcount;//两个指针指向同一个int*_pmtx = obj._pmtx;//两个指针指向同一个mutex*的锁AddRef();}}void AddRef(){//增加引用计数需要加锁_pmtx->lock();(*_refcount)++;_pmtx->unlock();}void Release(){_pmtx->lock();bool f = false;if(--(*_refcount) == 0 && _ptr){f = true;delete _ptr;delete _refcount;}_pmtx->unlock();//判断是否资源已经被释放,资源已经被释放,则释放锁if(f == true){delete _pmtx;}}~SharedPtr(){Release();}int UseCount(){return *_refcount;}T& operator*()//重载运算符{return *_ptr;}T* operator->(){return _ptr;}T* GetPtr(){return _ptr;}
private:T* _ptr;//指向堆区资源的裸指针int* _refcount;//开辟在堆区的引用计数std::mutex* _pmtx;//堆区的锁,考虑线程安全//如果设置在栈上,那么每一个对象都有一个独立的引用计数//每一次自增自减都需要对同一引用计数的所有对象维护,非常难以维护
};
3、weak_ptr
库中的wear_ptr
的实现需要结合起来shared_ptr
,shared_ptr内部含有两个引用计数
,一个是记录管理资源的shared_ptr(占用引用计数)的个数,一个记录指向资源的weak_ptr(不占用引用计数)的个数。
在这里,为了简便而言,我们实现一个简化版本的weak_ptr,对上面的知识理解即可。
// 简化版本的weak_ptr实现
template<class T>
class WeakPtr
{
public:WeakPtr():_ptr(nullptr){}WeakPtr(const SharedPtr<T>& sp):_ptr(sp.get()){}WeakPtr<T>& operator=(const SharedPtr<T>& sp){_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};
~完
转载请注明出处