【C++】异常 智能指针

C++异常 & 智能指针

  • 1.C++异常
    • 1.1.异常的抛出与捕获
    • 1.2.异常体系
    • 1.3.异常安全与规范
    • 1.4.异常优缺点
  • 2.智能指针
    • 2.1.RAII
    • 2.2.智能指针的使用及原理
      • 2.2.1.auto_ptr
      • 2.2.2.unique_ptr
      • 2.2.3.shared_ptr
      • 2.2.4.shared_ptr的循环引用问题 & weak_ptr
    • 2.3.定制删除器

1.C++异常

C++异常是一种处理错误的方式。当一个函数遇到自己无法处理的错误时就可以抛出异常,让该函数的直接或间接调用者通过捕获这个异常来处理错误。
一些异常关键字的介绍:

  • throw:程序是通过throw关键字来抛出异常的
  • catch:异常要通过catch关键字来进行捕获
  • trytry{}中的程序可能会抛出异常
// 异常捕获的基本语法如下
try
{// ...
}
catch(Exception_1 e)
{// ...
}
catch(Exception_2 e)
{// ...
}
...

1.1.异常的抛出与捕获

异常是以对象形式抛出的,当throwtry{}里时,异常抛出,会首先查找匹配的catch。而且该对象的类型决定了由哪个catch进行捕获处理。而被选中的catch处理程序是与对象类型匹配且离异常抛出位置最近的一个。
如果没有匹配的catch则会跳出当前函数栈,在调用该函数的栈中查找匹配的catch。如果直到main函数栈帧都没有匹配的catch,程序则会终止。
异常对象可能是一个临时对象,在被抛出时出作用域会被销毁,所以异常对象的抛出会生成一个拷贝被catch捕获。
catch(...)语句可以捕获任意类型的对象,但是无法确定捕获的错误异常类型是什么。实际中通常会最后再加上一个catch(...),防止程序直接终止。匹配合适的catch处理后,会继续沿着catch语句后面的程序执行。
抛异常时可以拋任意类型的对象,捕获时要求类型匹配。但实际中,抛出的异常对象类型并不一定要求和catch类型完全匹配。因为可以抛出派生类对象,使用基类进行捕获。

异常有时候也会被重新抛出,当前catch不能完全处理这样一个异常,在完成一些校正处理后,通过throw重新抛出给更上层的catch处理。

class Exception
{
public:Exception(const string& errmsg, int errid): _errmsg(errmsg), _errid(errid){}virtual string what() const{return _errmsg;}virtual int get() const{return _errid;}
protected:string _errmsg;int _errid;
};class HttpServerException : public Exception
{
public:HttpServerException(const string& errmsg, int id, const string& type): Exception(errmsg, id), _type(type){}virtual string what() const{return "HttpServerException:" + _type + ":" + _errmsg;}
protected:string _type;
};void SendMsg(const string& str)
{srand((unsigned int)time(0));if (rand() % 3 == 0){throw HttpServerException("网络错误", 333, "get");}else if (rand() % 4 == 0){throw HttpServerException("权限不足", 444, "post");}cout << str << "发送成功" << endl;
}void Server()
{string str = "hello world";// 出现网络错误,重试3次int count = 3;while (count--){try{SendMsg(str);// 程序走到此处,说明没有发生异常break;}catch (const Exception& e){if (e.get() == 333 && count > 0){continue;}else{throw e; // 异常重新抛出}}}
}void Test1()
{while (true){try{Server();}catch (const Exception& e){// 多态cout << e.what() << endl;}catch (...){cout << "Unkown Exception" << endl;}Sleep(1000);}
}

1.2.异常体系

C++本身是提供了一系列标准的异常的,这些异常是以父子类层次结构体系组织起来的,我们可以在程序中直接使用这些标准的异常,抛各种子类对象的异常,用一个父类对象进行接收。
在这里插入图片描述
我们也可以去继承execption类来实现自己的异常类。但实际中很多公司会定义一套自己的异常继承体系,一方面是为了规范的异常管理,另一方面C++标准异常还是不够好用。

1.3.异常安全与规范

可以在一个函数的后面接 throw(type...),列出这个函数可能抛出的异常类型有哪些。
如果函数后面接的是throw(),表示函数不抛异常。C++11新增了noexcept,用于表示不会抛异常。

构造函数中最好不要抛异常,否则可能导致对象构造不完整。
析构函数中最好不要抛异常,否则可能导致资源泄露。

1.4.异常优缺点

C语言处理错误的方式有两种:

  • assert终止程序。
  • 返回错误码errno

实际中C语言基本都是使用错误码的方式处理错误,有时候会使用终止程序的方式来处理非常严重的错误。
C++异常的优势主要是相对C语言而言的。

  • 异常对象相比错误码的方式可以展示出错误的各种信息,来帮助更好的定位程序的bug。
  • 错误码的使用有一个很大的问题就是,在函数调用层次中,深层的函数返回错误得层层返回。而异常可以直接跳到catch的地方。

异常也有缺点。

  • 异常可能导致程序的执行流乱跳,使得跟踪调试以及分析程序时更麻烦。
  • 异常很容易导致内存泄漏、死锁等安全问题。但这个基本可以通过RAII处理。

但异常总体而言利大于弊,另外面向对象的语言基本都是使用异常处理错误的,这也是大势所趋。

2.智能指针

int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("Division by zero error");return a / b;
}void Func()
{// 1、如果p1处new 抛异常会如何?// 2、如果p2处new 抛异常会如何?// 3、如果div调用处又抛异常会如何?int* p1 = new int;int* p2 = new int;cout << div() << endl;delete p1;delete p2;
}void Test2()
{try{Func();}catch (exception& e){cout << e.what() << endl;}
}

通过这个程序我们可以看到,p1抛异常时没什么问题,p1p2都没创建出来;
p2抛异常时,p1创建出来了,但没有释放,导致内存泄露了;
div()抛异常时,p1p2都创建出来了,但都没有释放,导致内存泄露了。
这里简单介绍一下内存泄漏的知识。
内存泄漏是指因为人为的疏忽或错误,而造成的程序未能释放已经不再使用的内存的情况。
内存泄漏并不是物理上内存的消失,而是由于程序失去了对该段内存的控制管理,而造成的内存的浪费。
长期运行的程序如果存在内存泄漏,会导致服务响应越来越慢,直至最终卡死。
C/C++程序一般关注两种方式的内存泄漏:堆内存泄露 和 系统资源泄露。
而内存泄漏的解决方式也分为两种:

  • 事前预防型,如智能指针的使用。
  • 事后差错型,如使用内存泄漏工具进行检测。

2.1.RAII

RAII(Resource Acquisition Is Initialization) - 资源获得即初始化。
RAII是一种通过利用对象生命周期来控制程序资源的简单技术。
在对象构造时获取资源,使控制的资源在对象生命周期内始终保持有效,最后在对象析构时释放资源。
这样做就不需要显式地释放资源,而且对象所需的资源在其生命周期内始终保持有效。

template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr): _ptr(ptr){}~SmartPtr(){cout << "~SmartPtr()" << endl;delete _ptr;}
private:T* _ptr;
};int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("Division by zero error");return a / b;
}void Func()
{int* p1 = new int;SmartPtr<int> sp1(p1);int* p2 = new int;SmartPtr<int> sp2(p2);cout << div() << endl;
}void Test3()
{try{Func();}catch (exception& e){cout << e.what() << endl;}
}

在这里插入图片描述
这里有了RAII的设计,就不用担心p1p2的内存泄漏问题了。

2.2.智能指针的使用及原理

像上面的SmartPtr还不能称其为智能指针,因为它还不具备指针的行为。因此还需要重载*->,让其像指针一样去使用。

T& operator*()
{return *_ptr;
}T* operator->()
{return _ptr;
}

2.2.1.auto_ptr

C++98库中就提供了auto_ptr智能指针。其实现原理就是将资源管理权进行转移。

// auto_ptr 简单模拟实现
template<class T>
class auto_ptr
{
public:auto_ptr(T* ptr = nullptr): _ptr(ptr){}auto_ptr(auto_ptr<T>& ap): _ptr(ap._ptr){// 资源管理权转移,但同时被转移对象也被悬空ap._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap){if (this != &ap){// 被赋值对象需要先被清理delete _ptr;_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}~auto_ptr(){delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};

auto_ptr可以说是一个失败的设计, 所以能不用就不用。

2.2.2.unique_ptr

unique_ptr的实现就是简单粗暴的防拷贝。

// unique_ptr 简单模拟实现
template<class T>
class unique_ptr
{
public:unique_ptr(T* ptr = nullptr): _ptr(ptr){}// 防拷贝unique_ptr(const unique_ptr<T>& up) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;~unique_ptr(){delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};

2.2.3.shared_ptr

shared_ptr支持拷贝。
shared_ptr是通过引用计数的方式来支持拷贝,支持多个shared_ptr对象共享资源的。
shared_ptr中,为每份资源都维护着一份计数。当一个shared_ptr对象被析构时,引用计数就减一,直到引用计数减为0,才释放资源。

// shared_ptr 简单模拟实现
template<class T>
class shared_ptr
{
public:// 构造 引用计数初始化为1shared_ptr(T* ptr = nullptr): _ptr(ptr), _pCount(new int(1)){}// 拷贝 ++引用计数shared_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pCount(sp._pCount){++(*_pCount);}// 赋值 ++引用计数shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){// 需要先处理好当前资源的释放Release();_ptr = sp._ptr;_pCount = sp._pCount;++(*_pCount);}return *this;}void Release(){if (--(*_pCount) == 0){cout << "void Release()" << endl;delete _ptr;delete _pCount;}}// 直到引用计数减为0才彻底释放资源~shared_ptr(){Release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count(){return *_pCount;}T* get(){return _ptr;}
private:T* _ptr;int* _pCount; // 一个资源,配一个计数,多个智能指针对象共管
};

shared_ptr引用计数不能采用静态计数。静态成员属于整个类,属于类的所有对象。如果申请了两份相同类型的资源,无法做到区分。

2.2.4.shared_ptr的循环引用问题 & weak_ptr

class Node
{
public:int _val;std::shared_ptr<Node> _prev;std::shared_ptr<Node> _next;~Node(){cout << "~Node()" << endl;}
};void Test4()
{std::shared_ptr<Node> n1(new Node);std::shared_ptr<Node> n2(new Node);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;
}

在这里插入图片描述
上面的程序结束后资源有没有被释放呢?
结果告诉我们是没有的(如果释放了的话会调用析构打印~Node())。
那为什么没有释放呢?
如图,展示了程序中节点的指向情况:
在这里插入图片描述
在Test4()结束的时候,n2先析构,n1再析构,此时左边的节点和右边的节点,它们的引用计数都由2减为1;
此时,由左边节点的_next管着右边的节点,由右边节点的_prev管着左边的节点;
只要左边节点的_next被析构,右边的节点引用计数减为0,就可以被delete;
而左边节点的_next要析构,就必须让左边的节点被delete,左边节点的_next作为成员才会被析构;
而左边的节点要被delete,就必须析构右边节点的_prev,使左边的节点引用计数减为0;
而右边节点的_prev要析构,就必须让右边的节点被delete,右边节点的_prev作为成员才会被析构;
而右边的节点要被delete,就必须析构左边节点的_next,使右边的节点引用计数减为0。

由于左边节点的_next和右边节点的_prev相互牵制着,最后谁也释放不了。这就是循环引用问题。
weak_ptr可以用于解决循环引用问题,而且weak_ptr正是作为辅助型智能指针,为了配合解决shared_ptr的循环引用问题才被设计出来的。

// weak_ptr 的简单模拟实现
template<class T>
class weak_ptr
{
public:weak_ptr(): _ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp): _ptr(sp.get()){}weak_ptr(const weak_ptr<T>& wp): _ptr(wp._ptr){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}weak_ptr<T>& operator=(const weak_ptr<T>& wp){_ptr = wp._ptr;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};

weak_ptr不是常规的智能指针,它没有RAII,它可以访问和修改资源,但不参与资源释放管理。
weak_ptr主要是用shared_ptr构造,用来解决shared_ptr的循环引用问题。

class Node
{
public:int _val;std::weak_ptr<Node> _prev;std::weak_ptr<Node> _next;~Node(){cout << "~Node()" << endl;}
};

在这里插入图片描述
智能指针总结
智能指针的设计主要考虑三点:

  1. 利用RAII思想来管理释放资源
  2. 保持有指针一样的行为
  3. 解决拷贝问题

2.3.定制删除器

对于申请的资源需要释放,但对于不同的申请方式,就需要不同的释放方式。定制删除器正是为了使申请与释放的方式进行匹配。

void Test5()
{// shared_ptr 支持构造时传删除器 仿函数版std::shared_ptr<Node> n1(new Node[5], DeleteArray<Node>());std::shared_ptr<Node> n2(new Node, Delete<Node>());std::shared_ptr<int> n3(new int[5], DeleteArray<int>());std::shared_ptr<int> n4(new int, Delete<int>());std::shared_ptr<int> n5((int*)malloc(sizeof 12), Free<int>());// shared_ptr lambda版std::shared_ptr<Node> n6(new Node[5], [](Node* ptr) {cout << "delete[] ptr;" << endl; delete[] ptr; });std::shared_ptr<Node> n7(new Node, [](Node* ptr) {cout << "delete ptr;" << endl; delete ptr; });std::shared_ptr<int> n8(new int[5], [](int* ptr) {cout << "delete[] ptr;" << endl; delete[] ptr; });std::shared_ptr<int> n9(new int, [](int* ptr) {cout << "delete ptr;" << endl; delete ptr; });std::shared_ptr<int> n10((int*)malloc(sizeof 12), [](int* ptr) {cout << "free(ptr);" << endl; free(ptr); });std::shared_ptr<FILE> n11(fopen("test.cpp", "r"), [](FILE* ptr) {cout << "fclose(ptr);" << endl; fclose(ptr); });// unique_ptr 支持类模板传迭代器类型,不支持构造时传迭代器std::unique_ptr<Node, DeleteArray<Node>> n12(new Node[5]);
}

在这里插入图片描述

看到这里,你能回答下列问题了吗?

  1. 为什么需要智能指针?
  2. 什么是RAII
  3. auto_ptr/unique_ptr/shared_ptr/weak_ptr之间的使用区别?
  4. 什么是循环引用?如何解决循环引用?

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

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

相关文章

AWTK实现汽车仪表Cluster/DashBoard嵌入式GUI开发(六):FREERTOS移植

前言: 一般的GUI工程都需要一个操作系统,可能是linux,重量级的,也可能是FreeRTOS,轻量级的。 一句话理解那就是工程就是FreeRTOS task任务的集合。 一个main函数可以看到大框架: 很显然,除了第一个是硬件配置的初始化,中间最重要的部分就是要创建任务。而一个任务主…

Windows系统下使用docker部署redis

使用虚拟机部署redis&#xff0c;虚拟机很占用电脑资源&#xff0c;所以选择使用docker对redis进行部署。 一、安装docker 安装链接&#xff1a;https://docker.p2hp.com/ 二、配置redis.conf文件 下载配置文件&#xff1a;https://download.redis.io/redis-stable/redis.con…

TSINGSEE青犀视频平台EasyCVR自定义可视化页面一览

视频监控TSINGSEE青犀视频平台EasyCVR能在复杂的网络环境中&#xff0c;将分散的各类视频资源进行统一汇聚、整合、集中管理&#xff0c;在视频监控播放上&#xff0c;视频安防监控汇聚平台可支持1、4、9、16个画面窗口播放&#xff0c;可同时播放多路视频流&#xff0c;也能支…

EasyDarwin开源流媒体服务器

文章目录 前言一、EasyDarwin 简介二、EasyDarwin 主要功能特点三、安装部署四、推拉流测试1、进入控制页面2、推流测试3、拉流测试 前言 本文介绍一个十分实用的高性能开源 RTSP 流媒体服务器&#xff1a;EasyDarwin。 一、EasyDarwin 简介 EasyDarwin 是基于 go 语言研发&a…

React Hooks实战:Web开发与设计

目录 前言 一、React Hooks 简介 二、React Hooks 的基本用法 1. 使用 useState 创建状态 2. 使用 useEffect 添加副作用 3. 使用 useContext 获取上下文 三、React Hooks 的常见问题 1. 循环引用问题 2. 副作用问题 四、React Hooks 实战案例 1. 使用 useState钩子…

golang 2018,go 1.19安装Gin

GOPROXYhttps://mirrors.aliyun.com/goproxy/ 一致提示URL不能有点&#xff0c;给我整郁闷了&#xff0c;换了这个地址好了 但是一致提示zip的包问题&#xff0c;最后还是不行又换回七牛 NEWBEE&#xff01; [GIN-debug] Environment variable PORT is undefined. Using por…

【机器学习5】无监督学习聚类

相比于监督学习&#xff0c; 非监督学习的输入数据没有标签信息&#xff0c; 需要通过算法模型来挖掘数据内在的结构和模式。 非监督学习主要包含两大类学习方法&#xff1a; 数据聚类和特征变量关联。 1 K均值聚类及优化及改进模型 1.1 K-means 聚类是在事先并不知道任何样…

数据结构—队列的实现

前言&#xff1a;上次我们已经学习了数据结构中一个重要的线性表—栈&#xff0c;那么我们这一次就来学习另外一个重要的线性表—队列。 目录&#xff1a; 一、 队列的概念 二、 队列的实现&#xff1a; 1.队列的创建 三、 队列的操作 1.初始化队列 2.队尾入队列 3.队头出队列…

【Vue】过滤器Filters

hello&#xff0c;我是小索奇&#xff0c;精心制作的Vue系列持续发放&#xff0c;涵盖大量的经验和示例&#xff0c;如对您有用&#xff0c;可以点赞收藏哈 过滤器 filters过滤器已从Vue 3.0中删除&#xff0c;不再支持了&#xff0c;这里可以作为了解进行学习 vue3要精简代码&…

【unity】网格描边方法

【unity】网格描边方法 介绍对模型四边网格的三种描边方法&#xff1a;包括纯Shader方法、创建网格方法和后处理方法。于增强场景中3D模型的轮廓&#xff0c;使其在视觉上更加突出和清晰。这种效果可以用于增强三维场景中的物体、角色或环境&#xff0c;使其在视觉上更加吸引人…

适用于WPF的设计模式

适用于WPF的设计模式 讨论“XAML能不能写逻辑代码”这个问题。我发现这是个有歧义的问题。这个问题可以有两种意思&#xff1a; XAML能不能用来写逻辑代码&#xff1f; XAML文件里能不能包含逻辑代码&#xff1f; 对于第一种意思——XAML是一种声明性语言&#xff0c;就是用来…

保姆级vue-pdf的使用过程

第一步 引入vue-pdf npm install --save vue-pdf 第二步 按照需求我们慢慢进行 01.给你一个pdf文件的url&#xff0c;需要在页面渲染 代码 <template><div><pdfref"pdf":src"url"></pdf></div> </template> <…