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

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。

相关文章:

  • 『Apisix进阶篇』动态负载均衡:APISIX的实战演练与策略应用
  • MySQL面试题--MySQL内部技术架构
  • SVM回归预测
  • AWS EC2设置root登录
  • centos 安装wget
  • npm常用命令详解
  • GEE:将分类特征和标签提取到样本点,并以(csv/shp格式)下载到本地
  • Rancher(v2.6.3)——Rancher部署Mysql(单机版)
  • Java解决幸运数字
  • 专项测试之「 性能测试」总结
  • 连组里服务器(自用)
  • 【Java程序设计】【C00344】基于Springboot的船舶维保管理系统(有论文)
  • DOcker in Docker 原理与实战代码详解
  • 【目标检测】YOLOv4 网络结构
  • 自定义拦截器处理作用在Controller方法上的自定义注解
  • [微信小程序] 使用ES6特性Class后出现编译异常
  • Java Agent 学习笔记
  • JS 面试题总结
  • Laravel 实践之路: 数据库迁移与数据填充
  • pdf文件如何在线转换为jpg图片
  • Python3爬取英雄联盟英雄皮肤大图
  • Spring思维导图,让Spring不再难懂(mvc篇)
  • 测试如何在敏捷团队中工作?
  • 动态魔术使用DBMS_SQL
  • 服务器从安装到部署全过程(二)
  • 排序(1):冒泡排序
  • 前端性能优化--懒加载和预加载
  • 十年未变!安全,谁之责?(下)
  • 实战:基于Spring Boot快速开发RESTful风格API接口
  • 我从编程教室毕业
  • 再谈express与koa的对比
  • ​【已解决】npm install​卡主不动的情况
  • # Swust 12th acm 邀请赛# [ A ] A+B problem [题解]
  • # Swust 12th acm 邀请赛# [ E ] 01 String [题解]
  • #我与Java虚拟机的故事#连载02:“小蓝”陪伴的日日夜夜
  • (2)Java 简介
  • (delphi11最新学习资料) Object Pascal 学习笔记---第8章第2节(共同的基类)
  • (二)pulsar安装在独立的docker中,python测试
  • (附程序)AD采集中的10种经典软件滤波程序优缺点分析
  • (附源码)计算机毕业设计SSM疫情社区管理系统
  • (七)MySQL是如何将LRU链表的使用性能优化到极致的?
  • (十七)Flask之大型项目目录结构示例【二扣蓝图】
  • (原創) 如何優化ThinkPad X61開機速度? (NB) (ThinkPad) (X61) (OS) (Windows)
  • (转)PlayerPrefs在Windows下存到哪里去了?
  • .bat批处理(二):%0 %1——给批处理脚本传递参数
  • .net core控制台应用程序初识
  • .net6使用Sejil可视化日志
  • .net通用权限框架B/S (三)--MODEL层(2)
  • .NET性能优化(文摘)
  • [ C++ ] STL_list 使用及其模拟实现
  • [AutoSAR 存储] 汽车智能座舱的存储需求
  • [Avalon] Avalon中的Conditional Formatting.
  • [C#]科学计数法(scientific notation)显示为正常数字
  • [C++随笔录] 红黑树
  • [CodeForces-759D]Bacterial Melee