C/C++ ② —— C++11智能指针

1. 为什么要使用智能指针?

  • 智能指针可以解决忘记释放内存导致内存泄漏的问题;
  • 智能指针可以解决异常安全问题。

2. 智能指针的原理

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

例子1:不使用智能指针

#include <iostream>
using namespace std;int div(){int a, b;cin >> a >> b;if(b == 0){throw invalid_argument("除0错误");}return a / b;
}void Func(){int *p1 = new int;cout << div() << endl;delete p1;
}int main(){try{Func();}catch(exception& e){cout << e.what() << endl;}return 0;
}
  • new空间也有可能会抛出异常,对于 p1 如果抛出异常:没有问题,可以不管,直接到最外面去了。
  • 如果用户输入的除数为0,那么div函数就会抛出异常,跳到主函数的catch块中执行,此时Func()中的申请的内存资源还没有释放,就会发生内存泄漏。

例子2:在例子1基础上对new进行异常捕获

void Func(){int* p1 = new int;try{cout << div() << endl;}catch (...){delete p1;throw;}delete p1;
}
  • 如果还要申请的p2,p3…这时候就需要套很多。因此要根本解决这个问题,可以使用智能指针。

例子3:使用智能指针

#include <iostream>
using namespace std;template<class T>
class smart_ptr{
public:smart_ptr(T *ptr = nullptr):_ptr(ptr){ }~smart_ptr(){if(_ptr){cout << "delete: " << _ptr << endl;delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t pos){return _ptr[pos];}
private:T *_ptr;
};int div(){int a, b;cin >> a >> b;if(b == 0){throw invalid_argument("除0错误");}return a / b;
}void Func(){smart_ptr<int> sp1(new int);int *p2 = new int;smart_ptr<int> sp2(p2);cout << div() << endl;
}int main(){try{Func();}catch(exception& e){cout << e.what() << endl;}return 0;
}
  • 在构造smart_ptr对象时,自动调用构造函数,将传入的需要管理的内存保存起来;
  • 在析构smart_ptr对象时,自动调用析构函数,将管理的内存空间进行释放
  • smart_ptr还可以与普通指针一样使用,需对*和->以及[]进行运算符重载
  • 问题
    • 如果用一个smart_ptr对象来拷贝构造另一个smart_ptr对象,或者一个smart_ptr对象赋值给另一个smart_ptr对象,最终结果会导致程序崩溃
    • 原因:编译器默认生成的拷贝构造函数对内置类型完成浅拷贝(值拷贝),单纯的浅拷贝会导致空间多次释放。

3. C++11中的几种智能指针

3.1 shared_ptr

  • 通过引用计数的方式解决智能指针的拷贝问题
  • shared_ptr 内部包含两个指针,一个指向对象,另一个指向控制块(control block),控制块中包含一个引用计数(reference count), 一个弱计数(weak count)和其它一些数据
  • shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候,内存才会被释放。
  • shared_ptr共享被管理对象,同一时刻可以有多个shared_ptr拥有对象的所有权,当最后一个shared_ptr对象销毁时,被管理对象自动销毁。
int main(){shared_ptr<int> sp1(new int(1));shared_ptr<int> sp2(sp1);*sp1 = 10;*sp2 = 20;cout << sp1.use_count() << endl;  // 2shared_ptr<int> sp3(new int(1));shared_ptr<int> sp4(new int(2));sp3 = sp4;cout << sp3.use_count() << endl;  // 2return 0;
}

shared_ptr常用函数和基本方法

  • 初始化和reset
    • sp.reset():重置shared_ptr,reset()不带参数时,若智能指针sp是唯一指向该对象的指针,则释放,并置空。若智能指针sp不是唯一指向该对象的指针,则引用计数减少1,同时将sp置空
    • sp.reset(new int(200)):reset()带参数时,若智能指针sp是唯一指向对象的指针,则释放并指向新的对象。若sp不是唯一的指针,则只减少引用计数,并指向新的对象
shared_ptr<int> p1(new int(1));
shared_ptr<int> p2 = p1;
shared_ptr<int> p3;
p3.reset(new int(1));
  • 使用make_shared来构造智能指针更高效
auto sp1 = make_shared<int>(100);
或
shared_ptr<int> sp1 = make_shared<int>(100);// shared_ptr<int> p = new int(1);  // 这是错误的,不能通过直接用原始赋值来初始化
  • 获取原始指针
shared_ptr<int> ptr(new int(1));
int *p = ptr.get();  // 返回shared_ptr中保存的裸指针
  • 指定删除器
    • 如果用shared_ptr管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器。
    • 当p的引用计数为0时,自动调用删除器DeleteIntPtr来释放对象的内存。
#include <iostream>
#include <memory>
using namespace std;void DeleteIntPtr(int *p){cout << "call DeleteIntPtr" << endl;delete p;
}int main(){shared_ptr<int> p(new int(1), DeleteIntPtr);return 0;
}

或者使用lambda表达式

shared_ptr<int> p(new int(1), [](int*p)){cout << "call DeleteIntPtr" << endl;delete p; });
  • 当我们用shared_ptr管理动态数组时,需要指定删除器,因为shared_ptr的默认删除器不支持数组对象
shared_ptr<int> p3(new int[10], [](int *p) { delete [] p; });

shared_ptr线程安全问题

  • 管理同一个资源的多个对象共享引用计数,多个线程可能会同时对同一个个引用计数进行加或减,而自增或自减都不是原子操作,所以需要通过加锁对引用计数进行保护。
  • 通过加锁让引用计数的++、-- 操作变成原子操作,对引用计数的操作进行加锁保护,也可以用原子类atomic对引用计数封装。
template<class T>
class shared_ptr{
public:shared_ptr(T* ptr):_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex){}~shared_ptr(){Release();}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount),_pmtx(sp._pmtx){_pmtx->lock();++(*_pcount);_pmtx->unlock();}// flag作用:当引用计数减到0时需要释放互斥锁,但是不能在临界区直接进行释放,因为后面还要解锁// 所以可以通过flag去标记,判断解锁后是否释放互斥锁资源void Release(){bool flag = false;_pmtx->lock();if (--(*_pcount) == 0){delete _pcount;delete _ptr;flag = true;}_pmtx->unlock();if (flag == true) delete _pmtx;}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){Release();_pcount = sp._pcount;_ptr = sp._ptr;_pmtx = sp._pmtx;_pmtx->lock();++(*_pcount);_pmtx->unlock();}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t pos){return _ptr[pos];}int use_count(){return *_pcount;}
private:T* _ptr;int* _pcount;mutex* _pmtx;
};
  • shared_ptr本身是线程安全的(拷贝和析构时,引用计数++、-- 都是线程安全的),不需要保证管理的资源的线程安全问题;而shared_ptr管理资源的访问不是线程安全的,需要用的地方自行保护。
struct Date{int _year = 0;int _month = 0;int _day = 0;
};
void test_shared_ptr(){int n = 100000;mutex mtx;shared_ptr<Date> sp1(new Date);thread t1([&](){for (int i = 0; i < n; i++){shared_ptr<Date> sp2(sp1);mtx.lock();sp2->_year++;sp2->_month++;sp2->_day++;mtx.unlock();}});thread t2([&](){for (int i = 0; i < n; i++){shared_ptr<Date> sp3(sp1);mtx.lock();sp3->_year++;sp3->_month++;sp3->_day++;mtx.unlock();}});t1.join();t2.join();cout << sp1.use_count() << endl;cout << sp1->_year << endl;cout << sp1->_month << endl;cout << sp1->_day << endl;
}

注意事项

    1. 不要用一个原始指针初始化多个shared_ptr
    1. 不要在函数实参中创建shared_ptr
function(shared_ptr<int>(new int), g());  //有缺陷// 正确做法应该是先创建智能指针
shared_ptr<int> p(new int);
function(p, g());
    1. 不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针,这样可能会导致重复析构
class A{
public:shared_ptr<A> GetSelf(){ return shared_ptr<A>(this); }~A(){ cout << "Destructor A" << endl; }
};
int main(){shared_ptr<A> sp1(new A);shared_ptr<A> sp2 = sp1->GetSelf();return 0;
}
  • 由于用同一个指针(this)构造了两个智能指针sp1和sp2,而他们之间是没有任何关系的,在离开作用域之后this将会被构造的两个智能指针各自析构,导致重复析构的错误。
  • 正确返回this的shared_ptr的做法是:让目标类通过std::enable_shared_from_this类,然后使用基类的成员函数shared_from_this()来返回this的shared_ptr
class A: public enable_shared_from_this<A>{
public:shared_ptr<A> GetSelf(){ return shared_from_this(); }~A(){ cout << "Destructor A" << endl; }
};
int main(){shared_ptr<A> sp1(new A);shared_ptr<A> sp2 = sp1->GetSelf();return 0;
}
    1. 避免循环引用,循环引用会导致内存泄漏
class A;
class B;class A{
public:shared_ptr<B> bptr;~A(){ cout << "A is deleted" << endl; }
};class B{
public:shared_ptr<A> aptr;~B(){ cout << "B is deleted" << endl; }
};int main(){{shared_ptr<A> ap(new A);shared_ptr<B> bp(new B);ap->bptr = bp;bp->aptr = ap;}cout<< "main leave" << endl;  // 循环引用导致ap bp退出了作用域都没有析构return 0;
}
  • 循环引用导致ap和bp的引用计数为2,在离开作用域之后,ap和bp的引用计数减为1,并不回减为0,导致两个指针都不会被析构,产生内存泄漏。

3.2 weak_ptr

  • share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
  • weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的shared_ptr,weak_ptr只是提供了对管理对象的一个访问手段。
  • weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。

weak_ptr 使用方法

shared_ptr<int> p1(new int(10));
weak_ptr<int> p2(p1);// 通过use_count()方法获取当前观察资源的引用计数
cout << p2.use_count() << endl;   // count = 1// 通过expired()方法判断所观察资源是否已经释放
if(p2.exxpired()) cout << "weak_ptr无效,资源已释放" << endl;
else cout << "weak_ptr有效" << endl;// 通过lock方法获取监视的shared_ptr
weak_ptr<int> wp;
void func(){auto spt = wp.lock();if(wp.expired()) cout "weak_ptr无效,资源已释放" << endl;else cout << "weak_ptr有效,*spt=" << *spt << endl;
}
int main(){{auto sp = make_shared<int>(42);wp = sp;func();}func();return 0;
}

weak_ptr返回this指针

  • shared_ptr中提到不能直接将this指针返回shared_ptr,需要通过派生enable_shared_from_this类,并通过其方法shared_from_this来返回指针
    • 原因是enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观察this智能指针
    • 调用shared_from_this()方法是,会调用内部这个weak_ptr的lock()方法,将所观察的shared_ptr返回
#include <iostream>
#include <memory>
using namespace std;class A: public enable_shared_from_this<A>{
public:share_ptr<A>GetSelf(){ return shared_from_this(); }~A(){ cout << "Destructor A" << endl; }
};
int main(){shared_ptr<A> sp1(new A);shared_ptr<A> sp2 = sp1->GetSelf();return 0;
}
  • 在外面创建A对象的智能指针和通过对象返回this的智能指针都是安全的,因为shared_from_this()是内部的weak_ptr调用lock()方法之后返回的智能指针,在离开作用域之后,spy的引用计数减为0,A对象会被析构,不会出现A对象被析构两次的问题。
  • 获取自身智能指针的函数尽在shared_ptr的构造函数被调用之后才能使用,因为enable_shared_from_this内部的weak_ptr只有通过shared_ptr才能构造。

weak_ptr 解决循环引用问题

  • 只要将A或B的任意一个成员变量改为weak_ptr即可解决智能指针的循环引用导致内存泄漏的问题
class A;
class B;class A {
public:weak_ptr<B> bptr;~A(){ cout << "A is deleted" << endl; }
};class B{
public:shared_ptr<A> aptr;~B(){ cout << "B is deleted" << endl; }
};int main(){{shared_ptr<A> ap(new A);shared_ptr<B> bp(new B);ap->bptr = bp;bp->aptr = ap;}return 0;
}

3.3 unique_ptr

  • unique_ptr独占对象的所有权,没有引用计数。同⼀时刻只能有⼀个unique_ptr指向给定对象,离开作⽤域时,若其指向对象,则将其所指对象销毁(默认delete)
  • 它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr
  • 定义unique_ptr时,需要将其绑定到⼀个new返回的指针上
  • unique_ptr不⽀持普通的拷⻉和赋值(因为拥有指向的对象),但是可以拷⻉和赋值⼀个将要被销毁的unique_ptr;可以通过release或者reset将指针所有权从⼀个(⾮const) unique_ptr转移到另⼀个unique
unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = move(my_ptr);  // 正确
unique_ptr<T> my_other_ptr = my_ptr;  // 报错,不能复制
  • unique_ptr可以指向一个数组
unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
shared_ptr<int []> ptr2(new int[10]);  // 这个是不合法的
  • unique_ptr指定删除器和shared_ptr有区别
shared_ptr<int> ptr1(new int(1), [](int *p){delete p;});  // 正确
unique_ptr<int> ptr2(new int(1), [](int *p){delete p;});  // 错误
unique_ptr<int, void(*)(int*)> ptr3(new int(1), [](int *p){delete p;});  // 正确
  • 如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/773970.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

刷题之贪心3

前言 大家好&#xff0c;我是jiantaoyab&#xff0c;这篇文章将给大家介绍贪心算法和贪心算法题目的练习和解析&#xff0c;贪心算法的本质就是每一个阶段都是局部最优&#xff0c;从而实现全局最优。加上这篇文章一共有30道贪心题目了&#xff0c;加油! 坏了的计算器 题目分析…

【学习】软件测试行业有哪些从业方向

从事任何一个行业&#xff0c;不论想入行的新人还是已经在职的从业人员&#xff0c;一定要系统化的掌握自身的学习路线和发展方向&#xff0c;随时对自身的优劣点掌握清楚。尤其是对于软件测试这个岗位。测试职业所涉及的技能范围比较广&#xff0c;测试流程、测试计划、缺陷管…

考研数学|《1800》《1000》《880》《660》最佳搭配使用方法

直接说结论&#xff1a;基础不好先做1800、强化之前660&#xff0c;强化可选880/1000题。 首先&#xff0c;传统习题册存在的一个问题是题量较大&#xff0c;但难度波动较大。《汤家凤1800》和《张宇1000》题量庞大&#xff0c;但有些题目难度不够平衡&#xff0c;有些过于简单…

前端 使用递归函数优化循环请求过程

目录 背景&#xff1a; 简介&#xff1a; 举个栗子&#x1f330;&#xff1a; 这是我们的原始代码&#xff1a; 这是改造后的代码&#xff1a; 总结一下&#xff1a; 背景&#xff1a; 在软件开发中&#xff0c;经常会遇到需要进行多次循环请求的情况。然而&#xff0c;…

财务收支系统怎么做,财务收支记账软件管理系统教程

财务收支系统怎么做&#xff0c;财务收支记账软件管理系统教程 一、前言 以下软件操作教程以 佳易王财务收支记账软件V17.0为例说明 件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 财务收支记账管理系统软件可按需定制 1、权限设置&#xff1a;管理员账…

【Java程序设计】【C00345】基于Springboot的船舶监造管理系统(有论文)

基于Springboot的船舶监造管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 项目获取 &#x1f345;文末点击卡片获取源码&#x1f345; 开发环境 运行环境&#xff1a;推荐jdk1.8&#xff1b; 开发工具&#xff1a;eclipse以及i…

第四届数字信号与计算机通信国际学术会议(DSCC 2024)

#高录用&#xff0c;稳检索# #高校背书&#xff0c;更可靠# DSCC 2024已通过SPIE出版社审核&#xff0c;ISSN号已确定&#xff1a;ISSN: 0277-786X&#xff0c;往届均已见刊EI检索&#xff01; 第四届数字信号与计算机通信国际学术会议&#xff08;DSCC 2024&#xff09; 2024 …

Linux服务器mamba环境配置

# windows目前装不了 目录 安装步骤&#xff1a; 可能出现的问题&#xff1a; 安装步骤&#xff1a; 需要的配置&#xff1a; CUDA(11.6) pytorch (1.12) 先装好和CUDA版本对应的torch&#xff01;&#xff01;不然后面问题一堆 packaging triton 官方readme.md文件要…

智慧校园数据可视化有什么好处?怎么推进数字化校园方案?

在当今数字化时代&#xff0c;越来越多学校开始实施智慧校园计划&#xff0c;旨在为学生和教师提供更高效、便捷的学习和教学环境。智慧校园运用互联网、大数据、人工智能等技术&#xff0c;对校园内各信息进行收集、整合、分析和应用&#xff0c;实现教学、管理、服务等多方面…

华为openGauss数据库命令大全:一站式掌握核心运维操作

引言&#xff1a; 华为openGauss作为一款高性能、安全可控的企业级数据库产品&#xff0c;其强大功能离不开一套完善且高效的命令行工具。本文将为您详细介绍openGauss数据库的常用命令&#xff0c;帮助您快速掌握数据库的日常运维操作。 一、服务管理命令 启动数据库服务 gs…

链表题牛客

链表反转 ListNode* ReverseList(ListNode* head){ListNode* a NULL;ListNode* b head;while(b!NULL){ListNode* cb->next;b->nexta;ab;bc;}return a; }链表内指定区间反转 ListNode* reverseBetween(ListNode* head, int m, int n) {ListNode* res new ListNode(-1…

python和Vue开发的RBAC用户角色权限管理系统

后端框架&#xff1a;python的FastAPI作为后端服务和python-jose作为JWT认证 前端框架&#xff1a;Vue3构建页面和Vue Router作为路由管理&#xff0c;Pinia作为数据存储&#xff0c;Vite作为打包工具 可以实现菜单控制和路由控制&#xff0c;页面里面有按钮权限控制&#xf…

【R包开发:包的组件】 第4章 包的元数据

DESCRIPTION(描述文件) 的作用是存储包中重要的元数据。当第一次开发包时, 你会 使用这个文件记录包运行时所需要的包。然而,随着时间的流逝,当开始与他人分享包 时,元数据文件变得越来越重要,因为它指定了谁可以使用它(许可证),以及如果包有 什么问题时需要和谁(你!)联系…

客户端测试 可测性改进-学习记录

客户端测试面临的挑战 难点&#xff1a; 业务复杂&#xff0c;产品多&#xff0c;技术栈多样 测试过程的痛点&#xff1a; 配置-》执行-〉检查-》结果 手工测试前置配置操作极其繁琐&#xff1a;安装测试包-〉进入debug页面-》设置h-〉设置AB test-》锁定rn包-〉进入业务页…

java解决跨域问题

浏览器js在访问服务器中的资源时&#xff0c;会出现同一页面或者不同域名(协议&#xff0c;IP&#xff0c;端口)不可访问 例如:file://d://test.html页面(file协议)中通过ajax访问服务器api.test.com的接口(http协议)&#xff0c;由于协议不同&#xff0c;此时会出现浏览器访问…

CVE-2020-15778 OpenSSH命令注入漏洞

预备知识 漏洞描述 OpenSSH8.3p1及之前版本中scp的scp.c文件存在操作系统命令注入漏洞。该漏洞即使在禁用ssh登录的情况下,但是允许使用scp传文件,而且远程服务器允许使用反引号(`)。攻击者可利用scp复制文件到远程服务器时,执行带有payload的scp命令,从而在后续利用中ge…

相对全面的四足机器人驱动规划MATLAB和Simulink实现方式(足端摆线规划,Hopf-CPG,Kimura-CPG)

许久没更新四足机器人相关的博客文章&#xff0c;由于去年一整年都在干各种各样的~活&#xff0c;终于把硕士毕业论文给写好&#xff0c;才有点时间更新自己的所学和感悟。步态规划和足端规划只是为了在运动学层面获取四足机器人各关节的期望角位移和速度信号&#xff0c;再由底…

Glide报错:FileNotFoundException: XX open failed: EACCES (Permission denied)

问题描述&#xff1a; targetVersion:33&#xff0c;Manifest已经申请了READ_MEDIA_IMAGES&#xff0c;WRITE_EXTERNA权限&#xff0c;代码里也动态申请了权限。但是通过系统相册选择图片后&#xff0c;将uri转为path之后&#xff0c;用glide加载path一直报这个错误。如果用gl…

JavaScript语法和数据类型

基础 JavaScript 借鉴了 Java 的大部分语法&#xff0c;但同时也受到 Awk、Perl 和 Python 的影响。 JavaScript 是区分大小写的&#xff0c;并使用 Unicode 字符集。举个例子&#xff0c;可以将单词 Frh&#xff08;在德语中意思是“早”&#xff09;用作变量名。 var Frh …

Nacos部署(二)Linux部署Nacos2.3.x集群环境

&#x1f60a; 作者&#xff1a; 一恍过去 &#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390 &#x1f38a; 社区&#xff1a; Java技术栈交流 &#x1f389; 主题&#xff1a; Nacos部署&#xff08;二&#xff09;Linux部署Nacos2.3.x集群环境 ⏱️…