C++【智能指针】
创始人
2024-02-10 03:04:26

文章目录

  • 一、什么是智能指针
    • RAII思想
    • std::auto_ptr
  • 二、智能指针的拷贝问题(C++98)
    • 1.unique_ptr
    • 2.shared_ptr
      • shared_ptr的问题
      • 循环引用的问题
    • 3.weak_ptr
    • 内存泄漏的危害

一、什么是智能指针

#includeusing namespace std;
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
// 如果出了异常,会直接跳转到捕获的地方去了。int* p1 = new int;int* p2 = new int;cout << div() << endl;//如果是p1 抛异常需要释放p2和div//如果是p2 抛异常需要释放p1和div//如果是div抛异常,需要释放p1和p2delete p1;delete p2;
}
int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

RAII思想

RAII(Resource Acquisition Is Initialization)(资源请求即初始化,也就是获取到资源,马上就初始化)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
①不需要显式地释放资源。
②采用这种方式,对象所需的资源在其生命期内始终保持有效

#includeusing namespace std;//利用RAII思想设计的delete资源的类
template
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout<<"delete"<int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{SmartPtr  sp1(new int);SmartPtr  sp2(new int);cout << div() << endl;cout<<"释放资源"<try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

在这里插入图片描述
不管是正方func结束,还是抛异常,sp1和sp2都会调用析构函数,释放资源。

但是我们这里就不能通过解引用的方式获取到资源了,这里我们就需要通过运算符重载来获取到我们的资源。

#includeusing namespace std;//利用RAII思想设计的delete资源的类
template
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout<<"delete"<return *_ptr;}T&operator->(){return *_ptr;}
private:T* _ptr;
};
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{SmartPtr  sp1(new int);SmartPtr  sp2(new int);cout << div() << endl;*sp1=1;*sp2=2;cout<<*sp1<try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

1.利用RAII思想设计delete类
2.重载*和->运算符

std::auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。

#include
#include 
using namespace std;
class A
{
public:~A(){cout<<"~A()"<auto_ptrap1(new A);return 0;
}

在这里插入图片描述

#include
#include 
using namespace std;
class A
{
public:~A(){cout<<"~A()"<auto_ptrap1(new A);ap1->_a1++;ap1->_a2++;return 0;
}

在这里插入图片描述

二、智能指针的拷贝问题(C++98)

因为智能指针在拷贝的时候,只有一个内置类型的拷贝,所以发生的是浅拷贝,会析构同一块空间,而二次析构内存就会报错,也就是我们的浅拷贝问题

#include
#include 
using namespace std;template
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;
};class A
{
public:~A(){cout<<"~A()"<SmartPtrsp1(new A);sp1->_a1++;sp1->_a2++;SmartPtrsp2(sp1);return 0;
}

在这里插入图片描述

解决方案
1.深拷贝?不能,因为违背了功能需求。智能指针只是像指针一样,帮你托管空间。
但是像下面这种情况我们就需要的是浅拷贝。

list lt;
auto it=lt.begin();

为什么迭代器浅拷贝没有问题呢?
因为迭代器并不负责资源的管理!迭代器只是为了封装底层的细节,以统一的方式来遍历资源。它不管迭代器中资源的释放。

#include
#include 
using namespace std;class A
{
public:~A(){cout<<"~A()"<auto_ptrsp1(new A);sp1->_a1++;sp1->_a2++;auto_ptrsp2(sp1);return 0;
}

库里面的是没有问题的。
因为库里面的行为就是将sp1的资源转移给了sp2
是资源管理权转移,不负责任地拷贝,会导致被拷贝对象的悬空问题
在这里插入图片描述
在这里插入图片描述

手动实现一个auto_ptr,了解上述的C++98的智能指针的底层

#include 
#include 
using namespace std;
namespace zhuyuan
{templateclass auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}~auto_ptr(){cout<<"delete"<&ap):_ptr(ap._ptr){ap._ptr=nullptr;}auto_ptr& operator=(auto_ptr& ap){if(this!=&ap){if(_ptr){delete _ptr;}_ptr=ap._ptr;ap._ptr= nullptr;}return *this;}T&operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};}class A
{
public:~A(){cout<<"~A()"<zhuyuan::auto_ptr ap1(new A);ap1->_a1++;ap1->_a2++;cout<_a1<<" "<_a2< ap2(ap1);return 0;
}

在这里插入图片描述

这个时候如果我们再去调用sp1指针,我们的程序就会发生崩溃。
很多公司明确要求不能使用C++98的智能指针

boost库
智能指针首先从boost社区中发展起来的。
scoped_ptr
shared_ptr
weak_ptr

C++11
unique_ptr
shared_ptr
weak_ptr

1.unique_ptr

不允许拷贝,只要有拷贝就会报错

void test_unique_ptr()
{std::unique_ptr up2(new A);std::unique_ptr up1(up2);
}

在这里插入图片描述

手动实现

namespace zhuyuan
{templateclass unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}~ unique_ptr(){cout<<"delete"<&ap)=delete;unique_ptr& operator=( unique_ptr& ap)=delete;//防止拷贝C++98//只声明,不实现T&operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};}
void test_unique_ptr()
{zhuyuan::unique_ptr up2(new A);zhuyuan::unique_ptr up1(up2);up1->_a1++;up1->_a2++;zhuyuan::unique_ptrup3=up2;
}

在这里插入图片描述

unique_ptr:简单粗暴,不让拷贝,只适用于不需要拷贝的一些场景

那如果我们就是需要拷贝呢?

2.shared_ptr

void test_shared_ptr()
{std::shared_ptr up2(new A);std::shared_ptr up1(up2);up1->_a1++;up1->_a2++;cout<_a1<<" "<_a2<up3=up2;
}

在这里插入图片描述

也就是说,它们共同管理了同一份数据。也就是三个指针指向的都是up2中的资源。
想要实现的话,我们就需要引用计数。
因为多个对象管理同一份资源,析构的时候就会出问题。但是,如果我们这时引入一个计数,也就是表示当前有多少个对象正在管理这份资源。当对象被析构的时候,我们就将这个计数–,当有新的对象引用这份资源的时候,我们就将这个计数++,当最后一个析构的对象释放时,释放这份资源。也就是说只需要析构一次就可以了。

这里使用静态计数对象是不可以滴。
因为一个资源就需要配一个计数,多个智能指针对象共管。
如果是静态对象的话,是所有资源都是有一个计数,因为静态成员属于整个类,类的所有对象。

每个资源需要管理时,会给构造函数,构造new一个计数。

namespace zhuyuan
{templateclass shared_ptr{public:void Release(){//如果这里的引用计数先--,然后等于0了之后,也就是说没有对象使用这份资源了//并且我们的要析构的资源的指针不为nullptr的时候//我们就进行析构。//同时清空我们这里的计数和我们的指向的资源。if (--(*_pCount) == 0 && _ptr){cout << "delete" << _ptr << endl;delete _ptr;_ptr = nullptr;delete _pCount;_pCount = nullptr;}}// RAII思想shared_ptr(T* ptr)//使用初始化列表进行初始化//初始化我们这里的资源:_ptr(ptr)//初始化我们的计数//创建一个类对象的时候,默认计数就是1, _pCount(new int(1)){}~shared_ptr(){//调用release进行析构Release();}//拷贝构造//sp1(sp2)shared_ptr(const shared_ptr& sp)//将指针和计数都进行拷贝:_ptr(sp._ptr), _pCount(sp._pCount){//将计数++,表示又有一个对象对其进行了引用(*_pCount)++;}//赋值// sp1 = sp3shared_ptr& operator=(const shared_ptr& sp){//if (this != &sp)//防止自己给自己赋值if (_ptr != sp._ptr){//这里我们需要先把我们sp1的对象的资源的计数给释放掉//然后再将sp1拷贝给sp3//否则我们sp1所指向的资源的计数就会比真实的多出一份//就会导致内存泄漏的问题!!Release();//将指针赋值_ptr = sp._ptr;//将计数也进行赋值_pCount = sp._pCount;//将计数++++(*_pCount);}return *this;}// 像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get(){return _ptr;}private://指向的资源的地址T* _ptr;//引用计数int* _pCount;};
}class A
{
public:~A(){cout<<"~A()"<
void test_shared_ptr()
{zhuyuan::shared_ptr up2(new A);zhuyuan::shared_ptr up1(up2);up1->_a1++;up1->_a2++;cout<_a1<<" "<_a2<up3=up2;zhuyuan::shared_ptrup4(new int);zhuyuan::shared_ptr sp5(new A);zhuyuan::shared_ptr sp6(sp5);}

在这里插入图片描述
在这里插入图片描述
重点注意
这里的赋值问题

// sp1 = sp3shared_ptr& operator=(const shared_ptr& sp){//防止自己给自己赋值//如果自己给自己赋值,首先就会释放自己的资源,然后再进行赋值//就会崩溃。//if (this != &sp)//采用下面的方法更好,这样我们知道如果我们拷贝的对象和我们被拷贝的对象//所持有的是同一块空间的话,我们就不会进行拷贝。//比方说我们的sp1和sp2指向的是同一块资源//然后我们这里运行的时候,知道了sp1的ptr和sp2的ptr指向的是同一块空间//就不再会进行拷贝。if (_ptr != sp._ptr){// 我们这里拿sp1 = sp3距离//因为我们的sp1之后不再持有原来的资源了,而是指向sp3的资源//这里我们需要先把我们sp1的对象的资源的计数给释放掉//然后再将sp3拷贝给sp1//否则我们原先sp1所指向的资源的计数就会比真实的多出一份//就会导致内存泄漏的问题!!Release();//共同管理新的资源,++计数//将指针赋值_ptr = sp._ptr;//将计数也进行赋值_pCount = sp._pCount;//将计数++++(*_pCount);}return *this;}void Release(){//--被赋值对象的计数,如果是最后一个对象,需要释放资源if (--(*_pCount) == 0 && _ptr){cout << "delete" << _ptr << endl;delete _ptr;_ptr = nullptr;delete _pCount;_pCount = nullptr;}}

shared_ptr的问题

如果两个智能指针指向同一块资源给多线程用的时候,里面可能会存在线程安全的风险。

循环引用的问题

struct Node
{int _val;//自定义类型的对象不能赋值给原生指针,//我们需要将其转换成智能指针std::shared_ptr _next;std::shared_ptr _prev;~Node(){cout<<"~Node"<//不支持隐式类型转换,所以我们需要使用()来进行赋值std::shared_ptr n1(new Node);std::shared_ptr n2(new Node);//循环引用问题n1->_next=n2;n2->_prev=n1;
}
int main()
{test_shared_ptr2();return 0;
}

没有打印析构的信息,我们的资源没有被正确释放,这就出现了内存泄漏

在这里插入图片描述

如何理解这个问题?

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
当这个函数(test_shared_ptr2)结束了之后n2先析构,n1再析构,因为n2后定义,先析构
在这里插入图片描述
在这里插入图片描述
然后就结束了。
我们的两个结点都没有正常释放!!

进一步分析
n1的_next管着右边节点的内存块,n2的_prev管着左边结点的内存块。
所以n1的_next析构,右边的结点就释放了(delete),
n2的_next析构,左边的结点就释放了(delete)。

那么_next什么时候释放呢?_prev什么时候释放呢?
右边结点什么时候delete呢?
左边的结点被delete,调用析构函数,_next作为成员才会析构
左边结点什么时候delete呢?
右边的结点被delete,调用析构函数,_prev作为成员才会析构
这就是一个循环等待对方释放的过程!

share_ptr内部无法解决这个问题!所以我们需要使用weak_ptr!

3.weak_ptr

weak_ptr不是常规智能指针,没有RAII,不支持直接管理资源
weak_ptr主要用shared_ptr构造,用来解决shared_ptr的循环引用的问题
在这里插入图片描述
可以使用shared_ptr进行构造,为了不增加引用计数,也就是不参与资源管理

struct Node
{int _val;std::weak_ptr _next;std::weak_ptr _prev;~Node(){cout<<"~Node"<//不支持隐式类型转换,所以我们需要使用()来进行赋值std::shared_ptr n1(new Node);std::shared_ptr n2(new Node);n1->_next=n2;n2->_prev=n1;
}
int main()
{test_shared_ptr2();return 0;
}

这里当_next和_prev是weak_ptr的时候,它不参与资源的释放管理,但是可以访问和修改资源,不增加计数,不存在循环引用的问题。

可以使用use_count来看一下这里使用weak_ptr前后的计数是多少

void test_shared_ptr2()
{//不支持隐式类型转换,所以我们需要使用()来进行赋值std::shared_ptr n1(new Node);std::shared_ptr n2(new Node);cout<_next=n2;n2->_prev=n1;cout<

在这里插入图片描述

内存泄漏的危害

内存泄漏,在进程结束的时候,操作系统都是会回收的,我们为什么要解决内存泄漏?
因为我们很多服务器中的进程都是不会关掉的!除非停服维修!并且有一些进程不能够正常释放

相关内容

热门资讯

猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...