当前位置: 首页 > news >正文

C++从入门到起飞之——智能指针!

1. 智能指针的使用场景

下面程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后面的delete没有得到执行,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本身也可能抛异常,连续的两个new和下面的Divide都可能会抛异常,让我们处理起来麻烦。智能指针放到这样的场景里面就让问题简单多了。

#include <iostream>using namespace std;double Divide(int a, int b)
{// 当b == 0时抛出异常 if (b == 0){throw "Divide by zero condition!";}else{return (double)a / (double)b;}
}
void Func()
{// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。 // 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。 // 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案 // 是智能指针,否则代码太戳了 int* array1 = new int[10];int* array2 = new int[10]; // 抛异常呢 try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;}catch (...){cout << "delete []" << array1 << endl;cout << "delete []" << array2 << endl;delete[] array1;delete[] array2;throw; // 异常重新抛出,捕获到什么抛出什么 }// ...cout << "delete []" << array1 << endl;delete[] array1;cout << "delete []" << array2 << endl;delete[] array2;
}
int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

2. RAII和智能指针的设计思路

RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问, 资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。

智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类⼀
样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。

template<class T>
class smart_ptr
{
public:smart_ptr(T* ptr) :_ptr(ptr){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}~smart_ptr(){delete[] _ptr;}
private:T* _ptr;
};void Func()
{这⾥使⽤RAII的智能指针类管理new出来的数组以后,程序简单多了 smart_ptr<int> p1 = new int[10];smart_ptr<pair<string,int>> p2 = new pair<string, int>[10];int len, time;cin >> len >> time;cout << Divide(len, time) << endl;
}

3. C++标准库智能指针的使用

C++标准库中的智能指针都在<memory>这个头文件下面,我们包含<memory>就可以是使用了,
智能指针有好几种,除了weak_ptr他们都符合RAII和像指针⼀样访问的行为,原理上而言主要是解
决智能指针拷贝时的思路不同。

> auto_ptr

auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给
拷贝对象
,这是⼀个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计
出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用
这个智能指针的。

#include <iostream>
#include <memory>using namespace std;struct Date
{int _year;int _month;int _day;Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}~Date(){cout << "~Date()" << endl;}
};int main()
{auto_ptr<Date> p1(new Date);auto_ptr<Date> p2 = p1;return 0;
}

被拷贝对象p1直接就为空了,p1是一个左值,如果再访问程序就会报错 !!

> unique_ptr

unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他

	unique_ptr<Date> p1(new Date);//无法拷贝//unique_ptr<Date> p2 = p1;//可以move移动构造转移资源unique_ptr<Date> p2(move(p1));

虽然move移动转移资源后,p1是空的,但是p1本就是左值,使用者显然是知道move可能会带来的后果的。所以,move左值的时候一定要谨慎。

下面简单封装一下unique_ptr:

#include <iostream>
#include <memory>using namespace std;namespace my_unique_ptr
{template<class T>class unique_ptr{public://删除拷贝构造和拷贝赋值unique_ptr(const unique_ptr<T>& uptr) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& uptr) = delete;//构造函数explicit unique_ptr(T* ptr) :_ptr(ptr){}//移动构造unique_ptr(unique_ptr<T>&& uptr) :_ptr(uptr._ptr){uptr._ptr = nullptr;}//移动赋值unique_ptr<T>& operator=(const unique_ptr<T>&& uptr){//释放当前资源if (_ptr) delete _ptr;_ptr = uptr._ptr;uptr._ptr = nullptr;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}const T& operator[](size_t i) const{return _ptr[i];}~unique_ptr(){if(_ptr){delete[] _ptr;cout << "~unique_ptr()" << endl;}}private:T* _ptr;};
}int main()
{my_unique_ptr::unique_ptr<int> p1(new int[10]);my_unique_ptr::unique_ptr<int> p2(new int[10]);for (int i = 0; i < 10; i++){p1[i] = i;p2[i] = i;}my_unique_ptr::unique_ptr<int> p3(move(p1));my_unique_ptr::unique_ptr<int> p4 = move(p2);for (int i = 0; i < 10; i++){cout << p3[i] << " ";}cout << endl;return 0;
}

> shared_ptr

shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝, 也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的

我们先来简单使用一下:

int main()
{shared_ptr<int> sp1(new int(10));shared_ptr<int> sp2(new int[10]);//支持拷贝shared_ptr<int> sp3 = sp1;return 0;
}

我们知道智能指针如果支持拷贝,那一定是浅拷贝。因为智能指针的本质是方便我们访问和管理我们new的资源。如果是深拷贝,那就违背智能指针的初衷了!!不过,浅拷贝又会引发两个问题:多个指针管理同一片资源,资源的释放时机,资源的释放次数所以,我们要确保这份资源只析构一次,并且只有在没有指针管理这份资源的时候才释放!!

因此,在share_ptr的底层中使用了引用计数的方式解决这些问题!!原理就是,用一个计数器管理一篇资源,计数器的数目代表有多少个指针管理这片资源。那我们该怎么实现呢??在share_ptr中,我们new一个count,用这个count来维护。这里不能使用static的原因就是静态变量是属于整个类实例化出来的对象的,因此只有一个计数器是无法管理多份资源的!!

下面简单封装了一下share_ptr:

template<class T>
class share_ptr
{
public://构造share_ptr(T* ptr):_ptr(ptr),_pcnt(new int(1)){}//析构~share_ptr(){if (--(*_pcnt) == 0){delete _ptr;delete _pcnt;}}//拷贝构造share_ptr(const share_ptr<T>& sptr):_ptr(sptr._ptr), _pcnt(sptr._pcnt){++(*_pcnt);}//拷贝赋值share_ptr<T>& operator=(const share_ptr<T>& sptr){// 如果指向同一份资源就不要赋值了if (_ptr != sptr._ptr){//当前引用计数--if (--(*_pcnt) == 0){delete _ptr;delete _pcnt;}//更改资源_ptr = sptr._ptr;_pcnt = sptr._pcnt;(*_pcnt)++;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}private:T* _ptr;int* _pcnt;
};

> 删除器

无论是unique_ptr还是share_ptr在默认析构时都是使用delete释放资源的,所以如果智能指针管理的资源不是new出来的,程序在释放资源时就会崩溃。

//这样写程序就会崩溃
shared_ptr<Date> sp1(new Date[10]);
unique_ptr<Date> sp2(new Date[10]);

为了解决这个问题,shared_ptr支持在构造的时候给定一个删除器,这个删除器本质就是一个可调用对象,比如:仿函数,函数指针,lambda表达式……而我们的智能指针在大多数情况下用的都是new所以就特化了一个delete[]的版本,我们只需要如下传递模版参数即可,很方便

shared_ptr<Date[]> sp1(new Date[10]);
unique_ptr<Date[]> sp2(new Date[10]);

而传删除器是一种通用的方法【这里直接传lambda表达式是真的香】。

	shared_ptr<Date[]> sp1(new Date[10], [](Date* ptr) {delete[] ptr;});shared_ptr<FILE> sp3(fopen("code1.cpp", "r"),[](FILE* ptr) {fclose(ptr);});

对于特化的new来说,unique_ptr和shared_ptr使用删除器都一样,但是如果是其他情况就不一样了。因为unique_ptr和shared_ptr对删除器的设计有所不同,shared_ptr是在类内部定义的构造函数模版,我们直接传对象编译器就会自动推导类型。但是,unique_ptr却是在类模版多定义了一个模版参数来支持删除器。所以,在使用unique_ptr传删除器的时候,使用仿函数类型会比较方便,因为仿函数类型可以直接定义对象。但是lambda表达式和函数指针是不能直接定义对象的,所以还是要在后面传递一个实例化的对象

//仿函数
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
//函数指针
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
//lambda
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);

下面我们就自己实现一下shared_ptr的删除器:

template<class T>class shared_ptr{public://构造(没有传删除器就走这个构造)shared_ptr(T* ptr):_ptr(ptr),_pcnt(new int(1)){}template<class D>shared_ptr(T* ptr, D del): _ptr(ptr),_pcnt(new int(1)),_del(del){}//析构~shared_ptr(){if (--(*_pcnt) == 0){_del(_ptr);delete _pcnt;}}//拷贝构造shared_ptr(const shared_ptr<T>& sptr):_ptr(sptr._ptr), _pcnt(sptr._pcnt){++(*_pcnt);}//拷贝赋值shared_ptr<T>& operator=(const shared_ptr<T>& sptr){// 如果指向同一份资源就不要赋值了if (_ptr != sptr._ptr){//当前引用计数--if (--(*_pcnt) == 0){delete _ptr;delete _pcnt;}//更改资源_ptr = sptr._ptr;_pcnt = sptr._pcnt;(*_pcnt)++;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}private:T* _ptr;int* _pcnt;function<void(T*)> _del = [](T* ptr) {delete ptr; };};
}

> 使用的小细节

• shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造。

• shared_ptr 和 unique_ptr 都支持了operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否空。

• shared_ptr 和 unique_ptr 都得构造函数都使用explicit【在编程语言中,“explicit”作为关键字,表示需要显式转换的数据类型,需手动调用转换函数,而非自动转换修饰,防止普通指针隐式类型转换成智能指针对象

4. shared_ptr和weak_ptr

4.1 shared_ptr循环引用问题

shared_ptr在大多数资源管理的场景下都可以很好的解决,但是在循环引用的场景下却会发生资源无法释放,内存泄漏的问题!

在如下场景就会造成循环引用,导致内存泄漏!!

struct ListNode
{int _data;std::shared_ptr<ListNode> _next;std::shared_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{// 循环引⽤ -- 内存泄露 std::shared_ptr<ListNode> n1(new ListNode);std::shared_ptr<ListNode> n2(new ListNode);n1->_next = n2;n2->_prev = n1;return 0;
}

在上图的分析中,next管理着prev指向的资源,而prev也管理着next指向的资源。但是,它们互相指向,也就是说,它们之中一定要有一个释放的话,其条件都是对方要先释放。可是,它们之间相互依赖,彼此制衡,形成一个环状的资源释放链,最终导致它们之间谁都无法释放!!

4.2 weak_ptr

为了解决上述问题,C++11中就引入了weak_ptr。weak_ptr既不支持RAII【资源请求立即初始化】,也不支持访问资源。所以,我们看文档时发现weak_ptr不支持构造时绑定资源,只支持绑定到share_ptr,但是绑定到share_ptr时并不增加计数器的数目。因此,weak_ptr可以解决上述问题

struct ListNode
{int _data;//std::shared_ptr<listnode> _next;//std::shared_ptr<listnode> _prev;std::weak_ptr<ListNode> _next;std::weak_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};

weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的
shared_ptr已经释放了资源
,那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的
资源是否过期
,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用
lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如
果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

int main()
{std::shared_ptr<string> sp1(new string("111111"));std::shared_ptr<string> sp2(sp1);std::weak_ptr<string> wp = sp1;cout << wp.expired() << endl;cout << wp.use_count() << endl;// sp1和sp2都指向了其他资源,则weak_ptr就过期了 sp1 = make_shared<string>("222222");cout << wp.expired() << endl;cout << wp.use_count() << endl;sp2 = make_shared<string>("333333");cout << wp.expired() << endl;cout << wp.use_count() << endl;wp = sp1;auto sp3 = wp.lock();cout << wp.expired() << endl;cout << wp.use_count() << endl;*sp3 += "###";cout << *sp1 << endl;return 0;
}
http://www.lryc.cn/news/605341.html

相关文章:

  • Unity UI的未来之路:从UGUI到UI Toolkit的架构演进与特性剖析(5)
  • Tableau 2019可视化数据分析软件安装包下载安装教程
  • 微软:科技领域的创新巨头
  • 华为昇腾NPU卡 文生视频[T2V]大模型WAN2.1模型推理使用
  • 【Qt】QTime::toString(“hh:mm:ss.zzz“) 显示乱码的原因与解决方案
  • OpenWrt Network configuration
  • SpringBoot 2.7.18 升级 3.4.6
  • LLMs之Agent:GLM-4.5的简介、安装和使用方法、案例应用之详细攻略
  • Python基础--Day04--流程控制语句
  • html的onBlur
  • 洛谷刷题7.30
  • 外键列索引优化:加速JOIN查询的关键
  • 【Arch-Linux,hyprland】常用配置-已实验成功指令大全(自用)(持续更新)
  • IBM Watsonx BI:AI赋能的下一代商业智能平台
  • 2.3.1-2.3.5获取资源-建设团队- 管理团队-实施采购-指导
  • Effective C++ 条款11:在operator=中处理“自我赋值”
  • ros2 launch文件编写详解
  • Python 程序设计讲义(46):组合数据类型——集合类型:集合间运算
  • 【百卷编程】Go语言大厂高级面试题集
  • 如何修改VM虚拟机中的ip
  • 2024 年 NOI 最后一题题解
  • 《汇编语言:基于X86处理器》第10章 复习题和练习
  • 歌尔微报考港交所上市:业绩稳增显韧性,创新引领生态发展
  • S3、SFTP、FTP、FTPS 协议的概念、对比与应用场景
  • openwrt中br-lan,eth0,eth0.1,eth0.2
  • 第2章 cmd命令基础:常用基础命令(3)
  • cmake_parse_arguments()构建清晰灵活的 CMake 函数接口
  • G9打卡——ACGAN
  • 获取TensorRT引擎文件(.engine)版本号的几种方法
  • 2022 年 NOI 最后一题题解