智能指针
文章目录
小辉:小黑,智能指针是什么呀?
它的作用是什么呢?
小黑:智能指针是基于一种RAII思想
实现的类,其功能和指针相似,可以说是
你自己实现的指针
一:智能指针的使用和原理
1.RAII
小辉:小黑,你提到的RAII思想是什么啊?
小黑:RAII是一种利用对象生命周期来控制程序资源
的简单技术.在对象构造时获取资源,接着控制
对资源的访问使之在对象的生命周期内始终保
持有效,最后在对象析构的时候释放资源
-
RAII(Resource Acquisition Is Initialization)思想的优点
-
不需要显式地释放资源。
-
采用这种方式,对象所需的资源在其生命期内始终保持有效。
1.1使用实例
#include<iostream>
#include<memory>
using namespace std;
template <class T>
class SmartPtr{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{
}
~SmartPtr(){
if (_ptr){
delete _ptr;
_ptr = nullptr;
cout << "~SmartPtr()" << endl;
}
}
T& operator *(){
return *_ptr;
}
T* operator ->(){
return _ptr;
}
private:
T* _ptr;
};
struct Date{
int _year = 1997;
int _month = 8;
int _day = 1;
~Date(){
cout << "~Date()" << endl;
}
};
int main(){
//int *pa = new int(1);
//SmartPtr<int> sp(pa);
//*pa=8;
//这种方式起不到资源申请立刻初始化的作用
//不符合RAII的思想,如下申请
SmartPtr<int> sp(new int(1));
*sp = 10;//像指针一样使用,和 pa指向同一片空间
SmartPtr<Date> spd(new Date);
spd->_year = 1998;
spd->_month = 1;
spd->_day = 28;
SmartPtr<Date> spd2 = spd;
//调用拷贝构造(浅拷贝)
spd2->_year = 1997;
//继续往后调试程序奔溃,原因为资源的二次释放(需要进行深拷贝)
return 0;
}
智能指针:要像指针一样使用
- 监视结果
小黑:由监视结果可以看到指针sp与sp2指向
同一片内存,在调用析构函数的时候会出
现二次释放的问题导致程序奔溃
小辉:【总结一下智能指针的原理】
1.RAII特性
2.重载operator*和opertaor->,
具有像指针一样的行为
3.对象所需的资源在其生
命期内始终保持有效
【问题提出】
- 可以看出这种方式存在不可拷贝,赋值的问题(拷贝或者赋值会导致对象在生命周期结束后调用析构函数时造成二次释放的问题)
- 拷贝/赋值会引发二次释放的问题
二:智能指针的种类
1.std::auto_ptr
1.1:std::auto_ptr使用实例
#include<iostream>
#include<memory>
using namespace std;
struct Date{
int _year = 1997;
int _month = 8;
int _day = 1;
~Date(){
cout << "~Date()" << endl;
}
};
int main(){
auto_ptr<Date> d1(new Date);
//d1里面是空的,调试看看
auto_ptr<Date> d2 = d1;
//此处d2指向这片空间(d1中的_ptr没有了),而d1为空
d2->_year = 1997;
(*d2)._year = 1998;
return 0;
//程序结束的时候,调用一次析构函数
//原因为此时只有智能指针d2指向这片空间,d1被架空
}
gcc中编辑的时候提示警告,不推荐使用
- 调试结果
小黑:从调试结果中可以看到,将 d1 赋值给
d2 后 d1 就被置空了
小辉:虽然atuo_ptr解决了上述提出的
二次释放问题,但是也出现了原指针
不可再用的情况
我们可以看到使用auto_ptr虽然可以解决二次释放的问题.但是也引来了新的问题,通过赋值或者拷贝的方式导致了原来的指针所指的资源被赋值给了新的指针,而且也将原指针置为了空.使得原指针没有了使用的价值.
1.2:std::auto_ptr的模拟实现
#include<iostream>
#include<memory>
using namespace std;
struct Date{
int _year = 1997;
int _month = 8;
int _day = 1;
~Date(){
cout << "~Date()" << endl;
}
};
template <class T>
class Autoptr{
public:
Autoptr(T* ptr)
:_ptr(ptr)
{
}
//管理权转移 ,解决二次释放的问题
Autoptr(Autoptr<T> & ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
//之前的对象已经被置空
}
~Autoptr(){
if (_ptr){
delete _ptr;
_ptr = nullptr;
}
}
T& operator * (){
return *_ptr;
}
T* operator ->(){
return _ptr;
}
private:
T* _ptr;
};
int main(){
Autoptr<Date> d1(new Date);
//管理权转移,指针悬空,c++98:auto_ptr 禁止使用
Autoptr<Date> d2 = d1;
d2->_year= 1997;
(*d2)._year = 1998;
return 0;
}
【std::auto_ptr总结】
- 1.解决了两个指针指向同一片空间二次释放的问题
- 2.将原来的对象置空,以达到解决二次释放的问题
【缺点】
- 使用 auto_ptr 之后赋值前的对象失去意义
2.std::unique_ptr
小辉:小黑,这款智能指针怎么样?
有没有解决拷贝问题啊?
小黑:这款智能指针比较靠谱,解决了
拷贝赋值的问题,但是他也有自己的
不足之处奥
2.1:std::unique_ptr使用实例
#include<iostream>
#include<memory>
using namespace std;
struct Date{
int _year = 1997;
int _month = 8;
int _day = 1;
~Date(){
cout << "~Date()" << endl;
}
};
int main(){
unique_ptr<int> uq(new int);
*uq = 10;
unique_ptr<Date> uq1(new Date);
//unique_ptr<Date> uq2 = uq1;
//unique_ptr<Date> uq3 (uq1);
return 0;
}
- 拷贝与赋值的结果
小辉:原来你说缺点是这样的啊
- unique_ptr的实现原理:简单粗暴的防拷贝.
2.2:std::unique_ptr的模拟实现
//为了解决auto_ptr的拷贝问题,引入了unique_ptr
#include<iostream>
using namespace std;
struct Date{
int _year = 1997;
int _month = 8;
int _day = 1;
~Date(){
cout << "~Date()" << endl;
}
};
template <class T>
class UniquePtr{
public:
UniquePtr(T* ptr)
:_ptr(ptr)
{
}
~UniquePtr()
{
if (_ptr){
delete _ptr;
_ptr = nullptr;
}
}
T& operator* (){
return *_ptr;
}
T* operator->(){
return _ptr;
}
private:
//防拷贝:两种方式都可以
UniquePtr(const UniquePtr<T>& up);
UniquePtr<T>& operator=(const UniquePtr<T> &up);
//UniquePtr(const UniquePtr<T> up)=delete;
//UniquePtr<T>& operator=(const UniquePtr<T> &up)=delete;
T* _ptr;
};
int main(){
UniquePtr<Date> up(new Date);
//不可拷贝,不可赋值
//UniquePtr<Date> upcopy(up);//错误的拷贝方式(不可以拷贝)
up->_year = 1990;
return 0;
}
//靠谱
//实现了RAII +指针
//缺点:不能拷贝,不能赋值
【unique_ptr总结】
- unique_ptr 在pirvate:权限下采用将拷贝构造函数和赋值运算符重载函数delete/只声明不实现的方式解决了auto_ptr的指针置空问题和二次释放问题
【缺点】
- 该类型的对象不能再采用拷贝和赋值的方式给新对象赋值
3.std::shared_ptr
小辉:看这个只能指针的名字就觉得
这个指针很有奉献精神(share)
小黑:不能那么说奥,这款智能指针可不是
用来分享的.它的介绍如下
3.1:std::shared_ptr实用实例
- shared_ptr的原理
是通过引用计数的方式来实现多个shared_ptr来实现多个shared_ptr对象之间共享资源
- 1.shared_ptr在其内部,给每一份资源都维护着一份计数,用来记录该份资源被几个对象共享
- 2.在对象被销毁时(调用析构函数),就将对象的计数减一
- 3.如果计数为0,就说明自己是最后一个使用该资源的对象,必须释放资源
- 4.若计数不为0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源
//解决了拷贝和赋值的问题
#include<iostream>
#include<memory>
using namespace std;
struct Date{
int _year = 1997;
int _month = 8;
int _day = 1;
~Date(){
cout << "~Date()" << endl;
}
};
int main(){
//shared_ptr可拷贝,可赋值
shared_ptr<Date> sp(new Date);
shared_ptr<Date> spcopy(sp);
shared_ptr<Date> sp2(new Date);
sp2 = sp;
cout << sp.use_count() << endl;
//引用计数:记录管理的指针;
return 0;
}
- 监视结果
- 运行结果
小辉:由监视结果可以看到,智能指针 sp
和 spcopy 指向同一片内存,由运行
结果来看该程序调用了两次析构函数,
原因是申请了两次资源
3.2:shared_ptr的模拟实现
//模拟实现shared_ptr
#include<iostream>
using namespace std;
struct Date
{
int _year = 1997;
int _month = 8;
int _day = 1;
~Date(){
cout << "~Date()" << endl;
}
};
template <class T>
class SharedPtr{
public:
//构造函数在第一次申请资源后调用
SharedPtr(T* ptr)
:_ptr(ptr)
, _useCount(new int(1))//第一次申请资源的时候指向这片资源的指针个数为1
{}
//拷贝构造的使用
SharedPtr(const SharedPtr<T> & sp)
:_ptr(sp._ptr)
, _useCount(sp._useCount)
{
//当前指向资源的引用计数+1;
++(*_useCount);
}
//赋值运算符重载
SharedPtr<T> &operator=(const SharedPtr<T> & sp)
{
//对于两个智能指针管理同一片空间的情况,不需要赋值
if (_ptr != sp._ptr){//检查是否为自己给自己赋值
if (--(*_useCount) == 0)
//检查是否是最后的一个指针指向这片空间
{
delete _ptr;
delete _useCount;
//若是最后一个指针则将这片空间释放并且将计数器指针也释放掉
}
_ptr = sp._ptr;
_useCount = sp._useCount;
++(*_useCount);
}
return *this;
}
//析构函数的使用
~SharedPtr()
{
if (--(*_useCount)==0){
delete _ptr;
delete _useCount;
_ptr = nullptr;
_useCount = nullptr;
}
}
T& operator*(){
return *_ptr;
}
T* operator ->(){
return _ptr;
}
int useCount(){
return *_useCount;
}
private:
T* _ptr;
int* _useCount;
};
int main(){
//Date* dt = new Date;
SharedPtr<Date> sp(new Date);
SharedPtr<Date> spcopy(sp);
SharedPtr<Date> sp2(new Date);
sp2 = sp;
return 0;
}
【std::shared_ptr总结】
- 解决了赋值和拷贝的问题
- 引入了计数指针(记录管理内存区域的指针数目)
3.3:shared_ptr的线程安全问题
shared_ptr的线程安全分为两方面
- 1.智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,因为这个操作不是原子的所以就会导致引用计数出现错误,从而导致资源未释放或者程序奔溃的问题.因此智能指针中引用计数++/–的时候就需要加锁了.也就是说引用计数的操作是线程安全的
- 2.智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题
void SharePtrFunc(SharedPtr<Date>& sp, size_t n) {
cout << sp.Get() << endl;
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
SharedPtr<Date> copy(sp);
// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
copy->_year++;
copy->_month++;
copy->_day++;
}
}
int main()
{
SharedPtr<Date> p(new Date);
cout << p.Get() << endl;
const size_t n = 100;
thread t1(SharePtrFunc, p, n);
thread t2(SharePtrFunc, p, n);
t1.join();
t2.join();
cout << p->_year << endl;
cout << p->_month << endl;
cout << p->_day << endl;
return 0;
}
3.4:shared_ptr的循环引用
#include<iostream>
#include<memory>
using namespace std;
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode(){
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
- 运行结果
从运行结果我们看出,在程序结束的时候.Dat对象的析构函数并没有被触发.
那么这是什么回事呢?
原来这里发生了shared_ptr的循环引用
- 循环引用分析
1.起初node1和node2两个指针指向两个结点,我们不需要手动释放
2.node1的next指向node2,node2的_prev指向node1,引用计数变成2
3.node1和node2析构,引用计数变成1,但是_next仍然指向下一个结点,_prev还指向上一个结点.
4.也就是说_next析构了,node2就释放了._prev析构了,node1就释放了
5.但是_next是node的成员.node1释放了,_next才会析构.而node1由_prev管理,_prev属于node2成员,这种现象就叫做循环引用.谁也不会释放
- 解决办法:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
3.5:weak_ptr的引入
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode(){
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
- 运行结果
从运行结果可以看到,引入了weak_ptr之后程序在生命周期结束的时候调用了析构函数
其原理就是:node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
三:RAII扩展学习
- RAII思想除了可以用来设计智能指针,还可以用来设计守卫锁,防止异常安全导致的死锁问题。
#include <thread>
#include <mutex>
// C++11的库中也有一个lock_guard,下面的LockGuard造轮子其实就是为了学习他的原理
template<class Mutex>
class LockGuard
{
public:
LockGuard(Mutex& mtx)
:_mutex(mtx)
{
_mutex.lock();
}
~LockGuard()
{
_mutex.unlock();
}
LockGuard(const LockGuard<Mutex>&) = delete;
private:
// 注意这里必须使用引用,否则锁的就不是一个互斥量对象
Mutex& _mutex;
};
mutex mtx;
static int n = 0;
void Func()
{
for (size_t i = 0; i < 1000000; ++i)
{
LockGuard<mutex> lock(mtx);
++n;
}
}
int main()
{
int begin = clock();
thread t1(Func);
thread t2(Func);
t1.join();
t2.join();
int end = clock();
cout << n << endl;
cout <<"cost time:" <<end - begin << endl;
return 0;
}
三.智能指针总结
-
智能指针:RAII+指针
-
RAII:资源获取立马初始化,根据对象的生命周期控制资源,通过构造函数管理资源,析构函数释放资源
-
1.auto_ptr:拷贝发生管理权限转移,导致之前的auto_ptr悬空
-
2.unique_ptr:防拷贝
-
3.shared_ptr:引用计数所有的智能指针指向同一份资源,资源被引用的数量,记录在引用计数变量中,引用计数变量类型:int*–>与资源对应,线程安全:mutex8–>与资源对应
-
4.守卫锁:利用RAII思想解决线程死锁,构造中加锁,析构中解锁,防止拷贝.
小黑,小辉:一起来学习C++吧,智能指针是
不是很有趣啊?期待下期再见奥.