• 异常与智能指针


    目录

    异常的概念

    异常的使用

    异常体系

    异常的优缺点

    智能指针


    异常的概念

    传统的错误处理机制(C语言)

    终止程序,如assert,如发生内存错误、除0错误就会终止程序,用户难以接收

    返回错误码,程序员无法直到它具体是什么错误,需要自己去查找对应的错误

    异常概念

    异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误

    三个关键字:throw、catch、try

    throw:用来抛出一个异常

    catch:用于捕获异常

    try:try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码

    异常的使用

    异常的抛出和匹配原则

    正常运行

    抛异常 

    异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码,如下图,这两者匹配,就会执行此catch中的代码

    被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个,比如上图中,如果Division抛出了一个异常,如果func和main函数中都有一个catch与其匹配,那么就会执行func中的catch

    抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁,类似于函数的传值返回

    catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么,如下图,没有这个程序就会中断

    有的话,程序还是会正常运行

    实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用

    在函数调用链中异常栈展开匹配原则

    首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句

    没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch

    如果到达main函数的栈,依旧没有匹配的,则终止程序,沿着调用链查找匹配的catch子句的
    过程称为栈展开

    找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行

    异常的重写抛出

    如上图,当要抛异常时,array的资源未清理,就会发生内存泄露

    解决方法一,则是在func中截留一下,后面的代码就会继续执行,但不能在最外层同一处理

    注意:拦截异常,不是要处理异常,而是要释放资源

    解决方法二

    捕获什么对象,就重新抛出什么对象

    异常安全

    最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化

    最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)

    C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题

    异常规范

    表示此函数会抛出A、B、C、D中的某种类型的异常

     表示此函数只会抛出bad_alloc的异常

    表示这个函数不会抛出异常(C++98)

    表示这个函数不会抛出异常(C++11)

    异常体系

    自定义异常体系

    很多公司都会自定义自己的异常体系进行规范的异常管理,如果大家随意抛异常,那么外层的调用者基本就没办法玩了,实际中都会定义一套继承的规范体系。这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了

    服务器开发中通常使用的异常继承体系

    大概有这样几层:网络服务层、业务逻辑层、缓存层、数据库持久化层

    基类,一般至少有两个成员,错误信息和错误编号

    网络服务层,相比基类多了一个请求类型的成员

    缓存层

    数据库持久化层,相比基类多了一个sql成员

     

    如上图,调用函数来测试 

    运行结果如上图,无法分辨出是哪个抛出的异常,所以需要用到多态,那就得先重写what()

    C++标准库异常体系

    异常的优缺点

    优点

    异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,可以帮助更好的定位程序的bug

    返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误,比如main函数调用func1,而func1调用func2,func2中出错了,就需要先将错误码返回给func1,再从func1返回给main,很困难,而异常则不需要这样做,会直接跳到main函数中catch捕获的地方,main函数直接处理错误

    很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那使用这些库也需要使用异常

    部分函数使用异常更好处理,比如T&operator[](size_t pos)这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误,只能这样:

    int at(size_t pos,T& x);

    缺点

    异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳,和goto相似,会导致我们跟踪调试时以及分析程序时,比较困难

    C++没有垃圾回收机制,资源需要自己管理,有了异常非常容易导致内存泄漏、死锁等异常安全问题,需要使用RAII来处理资源的管理问题。RAll:智能指针、lock_guard

     C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱

    异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。异常规范有两点:一个是抛派生类对象,另一个则是对接口函数声明抛异常规范

    总结:异常总体而言是利大于弊的

    智能指针

    为什么要使用智能指针

    如下图的代码及两个示例结果,可见,正常运行时,整个程序没有任何问题,但是当分母为0,即b==0时,就会抛出一个异常,然后直接跳到主函数去捕获异常,从而导致p指向的资源没有被清理,造成内存泄露

     

    内存泄露

    概念:

    因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不 是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而 造成了内存的浪费
    危害:
    长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会 导致响应越来越慢,最终卡死

    解决方法

    其中一种方法是在func中捕获异常,再重新抛出异常,但这种方法很挫,实用性不强

    另一种方法则是使用智能指针

    智能指针的使用及原理

    RAII

    RAII是一种利用对象生命周期来控制程序资源(如内存、文件句 柄、网络连接、互斥量等等)的简单技术,在对象构造时获取资源,在对象析构时释放资源
    好处:
    不需要显式地释放资源
    采用这种方式,对象所需的资源在其生命期内始终保持有效

     

    如上图代码及运行结果,内存泄露的问题得到了解决

    上图中的SmartPtr还不能被称为智能指针,因为其还不具备指针的作用,所以还需重载*,->运算符

    像指针一样使用

    智能指针的拷贝问题

    注:因为无法确定开辟的资源是malloc出来的还是new出来的,所以无法进行深拷贝,只能浅拷贝

    auto_ptr

     

    如上图,auto_ptr的原理是管理权转移,即将sp1指向的资源转给sp2,再将sp1置空,这样就不会出现多次析构的问题了,但sp1就悬空了,如果再对sp1解引用就会报错,所以这种设计不合理,是一个失败的设计 

    unique_ptr

    原理:直接防拷贝,简单粗暴

    同理,赋值运算符也需防止

    unique_ptr在需要拷贝构造时,就不适用了,就需要用到shared_ptr

    shared_ptr

    原理:通过引用计数的方式来实现多个shared_ptr对象之间共享资源,即对于开辟的空间,有几个指针指向它,就计数几次,只有引用计数为0时,才会去释放资源

    注意:这里的计数变量不能使用静态变量,因为如果开辟的资源不只有一块,那就无法发挥出此处引用计数的作用,所以对于引用计数变量也是new出来的

    构造时,将其引用计数变量初始化为1

     拷贝构造时,就对引用计数变量+1

    析构时,先判断引用计数变量是否为0,以及_ptr是否为nullptr 

    赋值运算符重载

    通过判断两个对象的管理资源的指针,来判断是否需要赋值,比如sp1,sp2指向同一块资源,如果sp1 = sp2,那就无需进行赋值

    例如上图,sp1 = sp3,先判断sp1指向的资源是否还有指针指向它,没有则释放这块资源,以及它的引用计数

     

    然后将sp3指向资源的指针和引用计数赋值给sp1,再将sp3指向资源的引用计数+1

     

    注意:上述中的shared_ptr以及其它智能指针都在memory头文件中

    shared_ptr的线程安全问题

    指向资源不是线程安全的

    指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了

    引用计数的++和--也不是线程安全的,需要智能指针去处理,采用加锁的方式

    以日期类为例,首先定义一个日期类

    然后在shared_ptr中多添加一个锁的成员,并且在构造函数中是new出来的,所以需要释放

    因为释放资源用的比较多,所以定义一个释放资源的函数,其它函数需要用时复用即可

    因为这里有--,所以需要加锁,而立一个flag则是为了方便释放锁占用的资源,因为如果在if里面释放了的话,而在外面解锁就会报错

    同理,这里写一个引用计数++的加锁函数

     

    所有前面的赋值运算符重载,析构,拷贝构造则可以直接调用上述函数,减少代码量

    如下图,对年月日的加加,这里加一对花括号是为了只对年月日进行加锁,因为局部变量出了作用域就销毁了

    shared_ptr的循环引用问题

    如下图,程序出问题了,没有调用ListNode的析构函数

    如下图,当需要析构时,n1指向的资源中的_next指向了n2指向的资源,而n2指向的资源中的_prev指向了n1指向的资源,形成了一个死结,无法解开,也就没办法析构

    解决方法

    weak_ptr

    weak_ptr的next和prev对象,可以访问指向节点资源,但是不参与节点资源释放管理,即不增加引用计数

    定制删除器

    定制删除器,实则就是一个可调用对象

    开辟资源与释放资源要匹配,比如new与delete,malloc与free,new[]与delete[]等等

    注意:默认情况,智能指针底层都是delete资源

    默认释放资源

    delete[]

    关闭文件

     

    测试结果如下

     

  • 相关阅读:
    Web安全——Web安全漏洞与利用上篇(仅供学习)
    nginx负载均衡和反向代理配置实例说明(内容来自网上,学习笔记,仅供交流学习使用无商业目的,如有侵权,通知我立马删除)
    基于SpringBoot的植物健康系统
    MATLB|改进的前推回代法求解低压配电网潮流
    Transformer和attention资料
    idea中 maven 本地仓库有jar包,但还是找不到,解决打包失败和无法引用的问题
    《QEMU/KVM源码分析与应用》读书笔记2 —— 第一章 QEMU与KVM概述
    采写编杂志采写编杂志社采写编编辑部2022年第10期目录
    Java 超新星开源项目 Solon v1.10.10 发布
    手撕二叉树
  • 原文地址:https://blog.csdn.net/weixin_58867976/article/details/125667208