目录
0.为什么需要智能指针?
1.智能指针的使用及原理
RAII:
智能指针的原理:
2.智能指针有哪些?
std::auto_ptr
std::unique_ptr
std::weak_ptr
0.为什么需要智能指针?
想要回答这个问题,首先要来看一个没有智能指针存在的场景:
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
思考:如果p1这里new 抛异常会如何?如果p2这里new 抛异常会如何?如果div调用又会抛异常会如何?毫无疑问,如果new开辟空间时抛出异常亦或是div调用时候抛出异常都会导致p1p2就没有及时释放,造成内存泄露!但如果我们依次在抛出异常前手动释放掉资源又会显得繁琐,这时就需要智能指针!
1.智能指针的使用及原理
先来介绍一下智能指针运用到了什么原理:
RAII:
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
private:
T* _ptr;
};
上面就是根据RAII思想大致设计出的智能指针,指针内部存储的是对象的地址,这样就相当于把资源的地址交给一个类对象管理,当这个类销毁时调用析构函数就不需要手动释放了!
智能指针的原理:
上面其实与真正的智能指针的框架大差不差,但是因为还要具备指针的行为,因此需要重载一下operator*以及operator->两个操作符函数:
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
T* _ptr;
};
这样就有了一个完整智能指针的雏形了~
2.智能指针有哪些?
std::auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针,让我们先来看一下关于auto_ptr的一些介绍:
但其实auto_ptr是许多程序员不喜欢使用的智能指针,因此存在指针悬空的问题:
void test()
{
//c++98中提供auto_ptr
auto_ptr<A> ap1(new A(1));
auto_ptr<A> ap2(new A(2));
auto_ptr<A> ap3(ap1);
//这里用sp1去构造sp3就会出现指针悬空的问题
(ap1->a)++;//sp1被置为空后再去访问就会报错
(ap2->a)++;
}
在上面的场景中就出现了指针悬空的问题,这里进行了管理权转移,ap1将管理权交给了ap3,那么ap3构造的时候会将ap1置为nullptr,如果此时再去对ap1进行访问就属于非法的!
下面来简单实现一下auto_ptr,以便我们明了具体结构:
思路:
---1--- 其实大致框架已经在前面给出了,完成了operator->以及operator*函数的重写,这里需要完成的是拷贝构造函数
---2--- 在拷贝构造函数中,需要把地址传给成员变量,将参数的地址置为nullptr
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{ }
auto_ptr(auto_ptr<T>& ptr)
:_ptr(ptr._ptr)
{
ptr._ptr = nullptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
如果不想要这种危险的情况发生,可以使用std::unique_ptr:
std::unique_ptr
顾名思义,unique则代表的是唯一,也就是说此智能指针不能拷贝构造一个新的同类型对象,先让我们看看介绍:
那么如果你试图用一个unique_ptr对象去拷贝构造一个新对象,这种行为在编译时就会报错:
void test()
{
unique_ptr<A> up1(new A(1));
unique_ptr<A> up2(new A(2));
//unique_ptr直接进行了反拷贝操作,禁止了这种指针拷贝行为
unique_ptr<A> up3(up1);
}
为什么会有删除函数这样的报错,是因为unique_ptr的底层实现了反拷贝:
我们来模拟实现一下unique_ptr:
思路:
---1--- 大致框架还是相同,不同的是这样要如何实现反拷贝?
---2--- 如果你使用C++98那么反拷贝应该将拷贝构造函数以及赋值函数只声明不实现
---3--- 如果你使用C++11那么只需将这两个函数设置为=delete即可,这也是为什么报错显示函数已经被删除
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
//unique这里用到了反拷贝,c++98中也可以只声明不实现,将声明放到private中
unique_ptr(unique_ptr<T>& ptr) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
T* _ptr;
};
那么如果我就是要实现可以拷贝构造和赋值构造的智能指针呢?登场的是std::shared_ptr:
std::shared_ptr
顾名思义,shared就是可以共享,那么就代表此类型的智能指针可以实现多个指针指向同一块内存,先来看看介绍:
那么如果你使用shared_ptr去构造和赋值,都是可以通过编译的:
void test()
{
bit::shared_ptr<A> sp1(new A(1));
bit::shared_ptr<A> sp2(new A(2));
bit::shared_ptr<A> sp3(sp1);//这里支持拷贝,是因为实现了引用计数
sp2 = sp3;
}
话不多说,来看看底层实现:
思路:
---1--- 框架没有变化,但是如果要实现shared_ptr的拷贝构造和赋值构造就要一点难度了:
---2--- 为什么shared_ptr可以支持多个对象指向同一内存?这里运用了引用计数,每个对象销毁时计数-1,直到减为0时,这块内存发生析构释放,这里最适合来进行技术的是int*类型的对象,为什么不是static或者是int,static只会实例化一份,无法实现多个对象的引用计数,int又会导致每个对象的引用计数都是拷贝,无法从根本修改计数
---3--- 默认构造:先new int,初始化为1即可
---4--- 拷贝构造:将指针赋值,将计数+1,并把地址赋值一下
---5--- 赋值构造:不仅仅是计数的增加以及指针的赋值,还要考虑原指针指向的内存是否应该释放
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr=nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
{
++(*(sp._pcount));
_pcount = sp._pcount;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//在改变被赋值对象的指针之前,先要考虑是否要释放被赋值对象管理的内存资源
//也就是说改变指针之前要对原理管理资源的pcount--,如果减到0就需要释放
//首先要考虑指向通过一块内存的两个指针相互赋值,如果不同名只是麻烦一点,如果同名那么就会造成释放后再去访问的问题
if (sp._ptr == _ptr)
{
return *this;
}
//对于被赋值的要记得减去引用计数来决定是否释放内存,防止造成内存泄漏
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
++*(sp._pcount);
_pcount = sp._pcount;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()const//返回指针
{
return _ptr;
}
size_t use_count()const//返回引用计数的个数
{
return *_pcount;
}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
}
}
private:
T* _ptr;
//shared_ptr可以支持多个指针指向同一块空间,怎么实现?
//引用计数,如何实现?使用static int不可行,因为多个对象只能有一个static,
//但是每一块资源都需要一个计数
int* _pcount;
};
但是,shared_ptr也会产生相应的问题,那就是引用循环:
引用循环(Reference Cycle),也被称为循环引用,它会引发内存泄漏等问题。
定义:引用循环指两个或多个对象之间相互持有对方的引用,形成一个闭环引用结构,导致这些对象无法被正常释放,造成内存资源的浪费。例如,对象 A 持有对象 B 的引用,而对象 B 又持有对象 A 的引用,这样就形成了引用循环。
假设存在两个类A
和B
,它们的定义如下:
#include <memory>
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() {
std::cout << "A destroyed" << std::endl;
}
};
class B {
public:
std::shared_ptr<A> a_ptr;
~B() {
std::cout << "B destroyed" << std::endl;
}
};
void test() {
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;
}
在test
函数结束时,a
和b
所指向的对象本应被释放,但由于它们相互引用,引用计数都不会降为 0,这里值得细说一下:a_ptr由b管理,当b析构时a_ptr发生析构,但是b_ptr又管理b,当b_ptr发生析构,b才会析构,那么b_ptr由谁管理呢?a,当a发生析构b_ptr才会析构,但是a又被a_ptr管理着,这就构成了死循环,最后引用计数由2->1,会导致内存泄漏。
如何解决?weak_ptr登场:不过在这之前先要补充定制删除器:如果让智能指针管理开辟的数组,该如何释放呢?shared_ptr提供了这样的接口:
这里的D可以传入函数指针,仿函数亦或是lambda表达式:
void test_sp4()
{
//如果new了一个数组,又该如何释放呢?
//了解定制删除器:new A[10]
//template <class U, class D> shared_ptr (U* p, D del);
//template <class D> shared_ptr(nullptr_t p, D del);
//库中用了模板D来控制,其实是仿函数,同时也可以接收lameda表达式
std::shared_ptr<A> sp1(new A[10], [](const A* ptr) { delete[] ptr; });
std::shared_ptr<A> sp2((A*)malloc(sizeof(A)*10), Destroy<A>());
std::shared_ptr<FILE> sp3(fopen("SmartPtr.hpp", "r"), [](FILE* ptr) {return fclose(ptr); });
//ps:这里的A是作者自定义的类
}
这是怎么做到的呢?其实是用到了U实例化后的包装器来接收传入的函数指针,仿函数亦或是lambda表达式,并使用模板来处理,这样在析构函数的时候使用包装器传入要释放资源的指针即可:(了解一下,理解即可)
template<class T>
class shared_ptr
{
public:
template<class D>
shared_ptr(T* ptr,D del)
:_ptr(ptr)
,_pcount(new int(1))
,_del(del)
{}
function<void(T*)> _del;//使用包装器来解决模板参数无法涉及析构函数的问题
};
std::weak_ptr
weak_ptr可以理解为是专门用于处理循环引用的问题的指针,但请注意并没有采用RAII思想来搭建,所以属于非常特殊的智能指针:
可以看到,weak_ptr并不支持传参构造,通常用shared_ptr去构造weak_ptr,那么如果这样使用weak_ptr就可以正常析构:
#include <memory>
class B;
class A {
public:
std::weak_ptr<B> b_ptr;//这里改成了weak_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() {
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;
}
原理是什么呢?其实weak_ptr并没有处理shared_ptr中的引用计数部分,但内部存在强弱引用计数,负责了指针的拷贝,并且使用了控制块,这才使得循环引用得以解决,请注意weak_ptr与RAII没有任何关联,可以参与资源的访问,但是不参与资源的释放!!!
模拟实现一下:
思路:
---1--- 简单实现一个简易版,其实很简单,只用实现shared_ptr构造和默认构造即可,直接给出:
template<class T>
class weak_ptr
{
public:
weak_ptr()//不支持传参构造
:_ptr(nullptr)
{
}
weak_ptr(const shared_ptr<T>& sp)//支持sharedptr构造
:_ptr(sp._ptr)
{ }
weak_ptr<T>& operator=(const weak_ptr<T>& wp)
{
_ptr = wp._ptr;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
补充:weak_ptr的底层实现:(了解即可)
控制块(Control Block):强 / 弱计数的载体
当 shared_ptr
首次指向动态对象时,会创建一个控制块(内存中与对象分离),包含:
- 强引用计数(strong count):记录当前活跃的
shared_ptr
数量(决定对象生命周期)。 - 弱引用计数(weak count):记录指向该对象的
weak_ptr
数量(决定控制块的生命周期)。 - 自定义删除器、分配器(若有)。
weak_ptr
的核心实现逻辑:
1. 绑定 shared_ptr
时:
weak_ptr
不增加强计数,仅增加弱计数。- 控制块通过原子操作(如
std::atomic<int>
)保证线程安全。
2. 对象销毁时机(强计数为 0 时)
- 控制块自身内存被释放(若通过
make_shared
创建,控制块与对象内存合并,优化缓存)。