【C++】C++异常

【C++】C++异常



🎬 个人主页MSTcheng · ZEEKLOG
🌱 代码仓库MSTcheng · Gitee
🔥 精选专栏: 《C语言
数据结构
《算法学习》
C++由浅入深

💬座右铭:路虽远行则将至,事虽难做则必成!


在前面的文章中,我们已经介绍了C++11的一些新特性。本文将和下一篇一起为大家讲解C++的最后两个重要主题:异常处理和智能指针。

文章目录

一、异常的概念及使用

1.1异常的概念

异常(Exception)是指在程序执行过程中发生的意外或错误情况,这些情况可能导致程序无法继续正常执行。异常处理是编程中用于管理这些意外情况的机制,旨在提高程序的健壮性和用户体验。
相比于C语言,C语言主要通过错误码的方式处理错误,而错误码的本质就是对错误的信息进行分类编写。拿到错误码以后还要去查询错误信息,是比较麻烦的。而异常时直接抛出一个对象,这个对象可以涵盖更全面的错误信息。

1.2异常的分类

1、编译时异常Checked Exception

  • 这类异常在编译阶段就会被检查,必须显式处理(捕获或声明抛出)。
    常见于外部资源操作,如文件不存在(FileNotFoundException)、数据库连接 失败等。

2、运行时异常Runtime Exception

  • 编译时不会被强制检查,通常由逻辑错误引发,如空指针访问NullPointerException)、数组越界(ArrayIndexOutOfBoundsException)等。

3、错误Error

  • 指严重问题(如内存耗尽OutOfMemoryError),通常无法通过程序处理,需从系统层面解决。

1.3异常的抛出与捕获

异常的抛出:

  • 当程序出现问题的时候,首先通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前调用链来决定匹配哪个catch,然后再被这个catch接收并处理异常。

异常的捕获:

  • 异常的捕获首先通过一个try/catch语句来捕获,并且该catch要与throw对象的类型匹配且为距离抛出异常位置最近的那一个catch,然后根据抛出的对象的类型和内容告知异常部分到底发生了什么错误。

下面我们就来看看C++的异常抛出与捕获机制:

#include<iostream>#include<string>usingnamespace std;//=================================//Divide这个函数是用来计算两个数相除的函数//对于一个除数来说被除数是不能为0的 也就是分子可以为0 但分母不能//所以我们针对被除数是否为0设计了一个异常机制//假设a为除数b为被除数 如果检测到b为0那么就抛出异常//=================================doubleDivide(int a,int b){try{// 当b == 0时抛出异常 引发除零错误if(b ==0){//===============================//抛出异常对象后,会⽣成⼀个异常对象的拷⻉,//因为抛出的异常对象可能是⼀个局部对象,//所以会⽣成⼀个拷⻉对象,//这个拷⻉的对象会在catch⼦句后销毁。//(这⾥的处理类似于函数的传值返回)//=============================== string s("Divide by zero condition!");throw s;//抛出的是一个string对象 catch的时候要用string类型接收}else{return((double)a /(double)b);}//... fxx()}catch(constint& s){ cout << s << endl;}//第一个catch 这个catch的类型与抛出异常的string对象类型匹配 且离抛出位置最近/*catch (const string& errmsg) { cout << errmsg << endl; }*/ cout << __FUNCTION__ <<":"<<__LINE__<<"行执行"<< endl;return0;}voidFunc(){int len, time; cin >> len >> time; cout <<Divide(len, time)<< endl; cout << __FUNCTION__ <<":"<<__LINE__<<"行执行"<< endl;}intmain(){try{Func();}//第二个catch 虽然也是string类型 与抛出异常的string对象类型配但是离抛出位置较远//所以第一个catch存在的情况下程序优先会跳到第一个catch中catch(const string& errmsg){ cout << errmsg << endl;}catch(int errid){ cout << errid << endl;}}
1、当被第一个catch捕获时

2、注释掉第一个catch,当被第二个catch捕获时

注意事项:

  1. throw执行时,throw后面的语句将不再执行,直接跳转到对应的catch中去执行catch可能是同一个函数中的局部catch,也可能是调用链中另一个函数中的catch,制空权从throw位置转移到了catch位置之后还有两个重要含义:
    1、沿着调用链的函数可能提早退出
    2、一旦程序开始执行异常处理程序,沿着调用链创建的对象都将销毁。
对于第一点含义我们对比两次catch执行的结果就会发现,当第一个catch捕获时,由于第一个catch与抛出异常的对象位于同一个域,所以当异常对象s被第一个catch捕获时并没有跳过func函数所以程序运行时会执行func函数中的内容而第二次catch就不同了,第二个catchmain函数中,throw之后直接跳过func函数,到main函数中执行catch之后的内容,跳过了func函数所以func函数中的内容不被执行。

对于第二点,我们就要来看看栈的展开了

1.4栈展开

如果一直到main函数都没有找到了与之类型匹配的catch子句,则程序会调用标准库的terminate函数终止程序,如下图:

在这里插入图片描述

当抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句首先检查throw本⾝是否在try块内部,如果在则查找匹配的catch语句:如果有匹配的,则跳到catch的地方进行处理;如果当前函数中没有try/catch子句,或者try/catch子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch过程被称为栈展开。 下面来看看栈展开图

在这里插入图片描述

1.5 查找匹配的处理代码

⼀般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个。
但是也有⼀些例外,允许从非常量向常量的类型转换,也就是权限缩小允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针允许从派⽣类向基类类型的转换,这个点非常实⽤,实际中继承体系基本都是用这个方式式设计的。下面来看一些例子:
//=============第一种==============//精确匹配try{throw42;// 抛出int类型}catch(int e){// 精确匹配int std::cout <<"Caught int: "<< e;}//=============第二种===============//权限缩小转换(非常量->常量)try{char* ptr =newchar[10];throw ptr;// 抛出char*}catch(constchar* e){// 允许非常量转常量 std::cout <<"Caught const pointer";delete[] e;}//=============第三种==============//派生类->基类转换classBase{virtualvoidfoo(){}};classDerived:publicBase{};try{throwDerived();// 抛出派生类对象}catch(Base& e){// 捕获基类引用(多态处理) std::cout <<"Caught Base reference";}//==============第四种=============//数组->指针转换try{int arr[5]{1,2,3};throw arr;// 抛出int[5]}catch(int* e){// 自动转为指针 std::cout <<"Caught pointer to first element: "<< e[0];}//==============第五种=============//就近原则匹配catchtry{throw std::string("error");}catch(const std::string& e){// 优先匹配更近的 std::cout <<"Caught by string ref";}catch(const std::exception& e){ std::cout <<"Caught by exception ref";}

另外:如果到main函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以⼀般main函数中最后都会使****⽤catch(...),它可以捕获任意类型的异常,但是是不知道异常错误是什么。

#include<iostream>#include<stdexcept>intmain(){try{// 可能抛出异常的代码throw std::runtime_error("An error occurred");}catch(const std::exception& e){ std::cerr <<"Caught exception: "<< e.what()<<std::endl;}catch(...)//使用三个点来接收异常{ std::cerr <<"Caught an unknown exception"<< std::endl;}return0;}

1.6异常重新抛出

有时catch到⼀个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理。捕获异常后需要重新抛出,直接 throw; 就可以把捕获的对象直接抛出
try{// 可能抛出异常的代码someRiskyOperation();}catch(const std::exception& e){if(isSpecialError(e)){// 特殊错误处理handleSpecialCase();}else{// 其他错误重新抛出throw;// 注意没有参数}}

注意throwthrow e的区别:

  1. throw重新抛出当前异常对象,不进行拷贝构造。
  2. throw e会通过拷贝构造函数创建一个新的异常对象。

1.7异常的安全问题

由于异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。中间我们需要捕获异常,释放资源后⾯再重新抛出。当然后面智能指针章节讲的RAII方式解决这种问题是更好的。
doubleDivide(int a,int b){// 当b == 0时抛出异常if(b ==0){throw"Division by zero condition!";}return(double)a /(double)b;}voidFunc(){// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。// 所以这里捕获异常后并不处理异常,异常还是交给外层处理,这里捕获了再// 重新抛出去。int* array =newint[10];try{int len, time; cin >> len >> time; cout <<Divide(len, time)<< endl;}catch(...){// 捕获异常释放内存 cout <<"delete []"<< array << endl;delete[] array;throw;// 异常重新抛出,捕获到什么抛出什么} cout <<"delete []"<< array << endl;delete[] array;}intmain(){try{Func();}catch(constchar* errmsg){ cout << errmsg << endl;}catch(const exception & e){ cout << e.what()<< endl;}catch(...){ cout <<"Unkown Exception"<< endl;}return0;}

1.8异常规范

相比于传统C++98时的异常规范C++11中进行了简化:函数参数列表后面加
noexcept表示不会抛出异常,什么都不加表示可能会抛出异常。
  • 编译器并不会在编译时检查noexcept,也就是说如果⼀个函数用noexcept修饰了,但是同时又包含了throw语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是⼀个声明了noexcept的函数抛出了异常,程序会调用terminate函数来终止程序。
  • 另外noexcept还可以作为一个运算符去检测一个表达式是否会抛出异常,如果会抛出异常就返回false,如果不会就返回true
doubleDivide(int a,int b)noexcept{// 当b == 0时抛出异常if(b ==0){throw"Division by zero condition!";}return(double)a /(double)b;}intmain(){try{int len, time; cin >> len >> time; cout <<Divide(len, time)<< endl;}catch(constchar* errmsg){ cout << errmsg << endl;}catch(...){ cout <<"Unkown Exception"<< endl;}int i =0; cout <<noexcept(Divide(1,2))<< endl; cout <<noexcept(Divide(1,0))<< endl; cout <<noexcept(++i)<< endl;return0;}
在这里插入图片描述

二、总结

C++的异常处理机制是管理运行时错误的重要工具,它通过try、catchthrow三个关键字的配合使用,能够有效应对程序执行中的意外情况。这套机制不仅确保了程序的健壮性,还能实现错误的优雅处理。因此,在代码编写时应当重视异常处理的应用。

MSTcheng 始终坚持用直观图解 + 实战代码,把复杂技术拆解得明明白白! 👁️ 【关注】 看普通程序员如何用实用派思路搞定复杂需求 👍 【点赞】 给 “不搞虚的” 技术分享多份认可 🔖 【收藏】 把这些 “好用又好懂” 的干货技巧存进你的知识库 💬 【评论】 来唠唠 —— 你踩过最 “离谱” 的技术坑是啥? 🔄 【转发】把实用技术干货分享给身边有需要的程序员伙伴 技术从无唯一解,让我们一起用最接地气的方式,写出最扎实的代码! 🚀💻 
感谢能够看到这里的小伙伴,如果这篇文章有帮到您,还请给个三连!你们的持续支持是我更新最大的动力!谢谢!

Read more

开源AI编程工具选型对比:opencode、GitHub Copilot谁更优?

开源AI编程工具选型对比:OpenCode、GitHub Copilot谁更优? 1. 引言 随着大模型技术的成熟,AI 编程助手已成为开发者日常开发中不可或缺的工具。从代码补全到项目规划,AI 正在重塑软件开发的工作流。在众多解决方案中,GitHub Copilot 作为最早进入市场的商业产品之一,凭借其与 VS Code 的深度集成广受欢迎;而 OpenCode 作为一个2024年开源的终端优先 AI 编程框架,迅速吸引了关注,尤其在隐私安全和本地化部署方面表现突出。 本文将围绕这两个代表性工具展开全面对比,重点分析它们的技术架构、功能特性、模型支持、隐私策略及适用场景,并结合实际使用体验,帮助开发者在不同需求下做出合理选型决策。特别地,我们还将探讨如何通过 vLLM + OpenCode 构建高性能的本地 AI Coding 应用,内置 Qwen3-4B-Instruct-2507 模型,实现高效、低延迟的代码生成能力。 2. OpenCode 核心特性解析

By Ne0inhk

2025最新Git LFS安装教程:Linux/macOS/Windows全平台覆盖

2025最新Git LFS安装教程:Linux/macOS/Windows全平台覆盖 【免费下载链接】git-lfsGit extension for versioning large files 项目地址: https://gitcode.com/gh_mirrors/gi/git-lfs 前言:为什么需要Git LFS? 你是否在使用Git管理大型文件时遇到过以下问题?仓库体积膨胀到GB级别、克隆项目耗时超过30分钟、CI/CD流程频繁失败?Git LFS(Git Large File Storage,Git大文件存储)通过将大型文件(如设计稿、数据集、二进制资产)存储在Git仓库之外,仅在代码库中保留轻量级指针文件,完美解决了这些痛点。本文将提供2025年最新的Git LFS全平台安装指南,涵盖Linux、macOS和Windows系统,让你5分钟内完成配置,告别大文件管理难题。 读完本文你将学到: * 3大操作系统的Git

By Ne0inhk
LLM - Claude-Mem 让 Claude Code 拥有长期记忆

LLM - Claude-Mem 让 Claude Code 拥有长期记忆

文章目录 * 一、概述 * 二、痛点:为什么 AI 编程助手必须要“长期记忆”? * 2.1 日常真实场景有多难受? * 2.2 问题本质:每次会话都是一张白纸 * 2.3 开发者已经开始“自救” * 三、Claude-Mem 是什么? * 四、整体架构:它怎么把“碎片操作”变成“长期记忆”? * 4.1 事件驱动:在不打扰你的情况下,捕获所有关键动作 * 4.2 本地混合存储:结构化 + 全文 + 语义 * 4.3 记忆压缩:别把“原始流水账”端给模型看 * 五、三层渐进式披露:Token 成本怎么砍到

By Ne0inhk
React Native鸿蒙跨平台实战:从项目初始化到开源交付完整指南

React Native鸿蒙跨平台实战:从项目初始化到开源交付完整指南

React Native鸿蒙跨平台实战:从项目初始化到开源交付完整指南 前言:本文聚焦React Native for OpenHarmony项目的完整落地流程,涵盖从零开始搭建工程、多设备适配验证、到开源仓库标准化交付的全过程。每个环节都附带实际踩坑经验与解决方案,帮助开发者快速掌握鸿蒙跨平台开发实战技能。 一、项目初始化:工程结构规划与基础配置 1.1 工程目录设计 在开始编码前,合理的目录结构能大幅提升后续维护效率。以下是推荐的工程结构: rnoh-multidevice-demo/ ├── rn/ # React Native工程目录 │ ├── src/ # 源码目录 │ ├── package.json # RN依赖配置 │ └── metro.config.js # Metro打包配置 ├── harmony/ # 鸿蒙工程目录 │ ├── entry/ │ │ ├── src/main/ │ │ │ ├── cpp/ # C++原生代码 │ │ │ ├── ets/ # ArkTS代码 │ │ │ └── resources/ # 资源文件 │ │ └──

By Ne0inhk