一、智能指针的引入
我们知道,栈是系统开辟并且系统进行释放的,而堆是程序员手动开辟,手动释放的。那么如果程序员忘记手动释放就会造成内存泄露,或者由于程序逻辑运行出现异常,导致代码过早返回,没有执行到free或者delete。那么如何避免这种错误呢,所以引入了智能指针(手动开辟,系统回收)
智能指针是怎么防止内存泄露的,如下代码:
void func()
{
shared_ptr<int> p1(new int(10));
*p1 = 20;
}
看这个 func 函数,在函数局部使用了 new 开辟了堆内存,此时 func 函数不管是正常运行结束,或者是代码抛出异常,那么在出作用域之前,智能指针对象 p1 会进行析构,在析构函数里面它就会把管理的堆内存给释放掉,有效的防止了内存泄露。
在C++98标准,只有一个智能指针 auto_ptr
在C++11标准,智能指针有unique_ptr、shared_ptr 、weak_ptr 不用auto_ptr(有缺陷)借鉴boost库引用的
二、智能指针的实现原理
智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
什么是智能指针? 智能的管理指针所指向的动态资源的释放。它是一个类,有类似指针的功能。
三、四种智能指针
<1> auto_ptr --- 所有权唯一,但所有权可以转移
模拟实现:
#include<iostream>
using namespace std;
//在实现的时候不知道管理什么样的内存块,所以用模板来实现
template<typename T>
class Auto_Ptr
{
public:
//构造函数
Auto_Ptr(T* ptr)
:mptr(ptr)
{}
//拷贝构造
Auto_Ptr(Auto_Ptr<T>& rhs)
{
mptr = rhs.Release();
}
//赋值运算符重载
Auto_Ptr<T>& operator=(Auto_Ptr<T>& rhs)
{
if (this != &rhs)//自赋值的判断
{
delete mptr;
mptr = rhs.Release();//转让权限,取消了旧指针的所有权
}
return *this;
}
//析构函数
~Auto_Ptr()
{
delete mptr;//Auto_Ptr不能用来管理数组
mptr = NULL;
}
//解引用
T& operator*()
{
return *mptr;
}
//调用
T* operator->()
{
return mptr;//return (this->mptr);
}
private:
//这个函数只是把智能指针赋值为空,但是它原来指向的内存并没有被释放,相当于它只是释放了对资源的所有权
T* Release()//转让权限
{
T* tmp = mptr;
mptr = NULL;
return tmp;
}
T* mptr;
};
缺陷:
1.一个指针变量指向的空间不能由两个auto_ptr管理,不然会析构两次,使程序崩溃 --- 所有权唯一,不能共享内存块
//错误
int *ap = new int;
auto_ptr<int> a1(ap);
auto_ptr<int> a2(ap);
2、赋值或拷贝构造将原指针的所有权转移给新指针,会使得原指针悬空,直接编译不会出错,但解引用是会出现很多问题
基于这个原因,应该避免把auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作(出错了也不易发现),这样会使容器中很多元素被置为NULL。
auto_ptr<int> ap1(new int);
auto_ptr<int> ap2(ap1);
//写上面两句不会报错,但是如果对旧指针进行操作,运行出错
*ap1 = 20;//出错
ap2剥夺了ap1的所有权,当程序运行时访问ap1将会报错。
3、auto_ptr不能用来管理数组,析构函数中用的是delete
<2> unique_ptr --- 所有权唯一且不能转移
以将构造和拷贝构造放到私有下,来保证他的所有权唯一,所有权不能能转移
它是( C++11引入的,前身是scoped_ptr,scoped_ptr是boost库里的),也不支持拷贝构造和赋值,但比auto_ptr好,直接赋值会编译出错(与auto_ptr最大的不同就是类内私有的声明了拷贝构造函数和赋值运算符重载,是针对auto_ptr的缺点而出现的)
模拟实现:
#include<iostream>
using namespace std;
template<typename T>
class Unique_Ptr
{
public:
Unique_Ptr(T* ptr)
:mptr(ptr)
{}
~Unique_Ptr()
{
delete mptr;
mptr = NULL;
}
T& operator*()
{
return *mptr;
}
T* operator->()
{
return mptr;
}
private:
//将赋值和拷贝构造写到私有,禁止所有权转移
Unique_Ptr(Unique_Ptr<T>& _Right);
Unique_Ptr<T>& operator=(Unique_Ptr<T>& _Right);
T* mptr;
};
因为构造和拷贝构造放到私有下,所以再进行拷贝构造,编译就会出错,保证了他所以有权唯一
缺陷:
1、所有权唯一,不能数据共享
设计层面保证所有权唯一了 ,但是外部控制也可以让他指向同一块内存--->程序崩溃
//错误
int* p = new int;
unique_ptr<int> up1(p);
unique_ptr<int> up2(p);
<3> shared_ptr --- 所有权不唯一,基于引用计数
资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
shared_ptr是强引用指针 ,会有智能指针相互引用的问题。比如上图的双向链表
解决这种状况的办法就是将两个类中的一个成员变量改为weak_ptr
对象,因为weak_ptr
不会增加引用计数,使得引用形不成环,最后就可以正常的释放内部的对象,不会造成内存泄漏
<4> weak_ptr --- 和shared_ptr搭配使用
引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr。weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。
weak_ptr并没有重载operator->和operator *操作符,因此不可直接通过weak_ptr使用对象,典型的用法是调用其lock函数来获得shared_ptr示例,进而访问原始对象。
weak_ptr也维护了一个引用计数,跟shared_ptr维护的引用计数或互不干扰,或相互协同。weak_ptr的指针会在weak_ptr维护的引用计数上加一,而shared_ptr会在shared_ptr维护的引用计数上加一,这样在循环引用时,就会因为对不同引用的判断的不同,使最终决定是否释放空间的结果也不相同。
有一个规定,就是创建对象的时候,持有它的强智能指针,当其他地方想使用这个对象的时候,应该持有该对象的弱智能指针,
Q:如何判断weak_ptr的对象是否失效?
A:1、expired():检查被引用的对象是否已删除。
2、lock()会返回shared指针,判断该指针是否为空。
3、use_count()也可以得到shared引用的个数,但速度较慢。
(1)weak_ptr虽然是一个模板类,但是不能用来直接定义指向原始指针的对象。
(2)weak_ptr接受shared_ptr类型的变量赋值,但是反过来是行不通的,需要使用lock函数。
(3)weak_ptr设计之初就是为了服务于shared_ptr的,所以不增加引用计数就是它的核心功能。
(4)由于不知道什么之后weak_ptr所指向的对象就会被析构掉,所以使用之前请先使用expired函数检测一下。
参考博客:C++智能指针——探究六个常见的智能指针的使用及原理
智能指针 auto_ptr、scoped_ptr、shared_ptr、weak_ptr