【C++航海王:追寻罗杰的编程之路】异常——错误处理方式之一

简介: 【C++航海王:追寻罗杰的编程之路】异常——错误处理方式之一

引言

在C++编程中,异常处理是一种重要的技术,用于处理程序在运行时可能出现的错误或意外情况。异常是指在程序执行过程中发生的某种不正常的情况,例如除以零、内存访问错误或无效的输入等。传统的错误处理方式通常涉及使用错误代码或返回特殊值来指示问题,但这种方式可能会导致代码混乱、繁琐,并且容易被忽略或处理不当。


异常处理提供了一种更为结构化和灵活的方法来处理异常情况。当异常发生时,程序可以抛出(throw)一个异常对象,然后在适当的位置捕获(catch)并处理该异常。这种机制使得程序可以将错误处理逻辑与正常逻辑分离开来,提高了代码的可读性和可维护性。

1 -> C语言传统的处理错误的方式

传统的错误处理机制:

  1. 终止程序,如assert,缺陷:用户难以接受。如发生内存错误,除以零错误时就会终止程序。
  2. 返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到error中,表示错误。

实际中C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。

2 -> C++异常概念

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

  • throw:当问题出现时,程序会抛出一个异常。这是通过使用throw关键字来完成的。
  • catch:在想要处理问题的地方,通过异常处理程序捕获异常。catch关键字用于捕获异常,可以有多个catch进行捕获。
  • try:try块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个catch块。

如果有一个块抛出一个异常,捕获异常的方法会使用trycatch关键字。try块中放置可能抛出异常的代码,try块中的代码被称为保护代码。使用try/catch语句的语法如下:

try
{
  // 保护的标识代码
}
catch (ExceptionName e1)
{
  // catch 块
}
catch (ExceptionName e2)
{
  // catch 块
}
catch (ExceptionName e3)
{
  // catch 块
}

3 -> 异常的使用

3.1 -> 异常的抛出和捕获

异常的抛出和匹配原则

  1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
  2. 选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
  3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(类似函数的传值返回)
  4. catch()可以捕获任意类型的异常,问题是不知道异常错误是什么。
  5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获。

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

  1. 首先检查throw本身是否在try块内部,如果是在查找匹配的catch语句。如果有匹配,则调到catch的地方进行处理。
  2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
  3. 如果达到main函数的栈,依旧没有匹配的,则终止程序。这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们都要在最后加一个catch()捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
  4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
#define  _CRT_SECURE_NO_WARNINGS 1
 
#include <iostream>
using namespace std;
 
double Division(int a, int b)
{
  // 当b == 0时抛出异常
  if (b == 0)
    throw "Division by zero condition!";
  else
    return ((double)a / (double)b);
}
void Func()
{
  int len, time;
 
  cin >> len >> time;
  cout << Division(len, time) << endl;
}
int main()
{
  try 
  {
    Func();
  }
 
  catch (const char* errmsg)
  {
    cout << errmsg << endl;
  }
 
  catch (...) 
  {
    cout << "unkown exception" << endl;
  }
 
  return 0;
}

3.2 -> 异常的重新抛出

有可能单个的catch不能完全处理一个异常,在进行一些矫正处理后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。

double Division(int a, int b)
{
  // 当b == 0时抛出异常
  if (b == 0)
  {
    throw "Division by zero condition!";
  }
 
  return (double)a / (double)b;
}
 
void Func()
{
  // 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
  // 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
  // 重新抛出去。
  int* array = new int[10];
  try 
  {
    int len, time;
 
    cin >> len >> time;
    cout << Division(len, time) << endl;
  }
 
  catch (...)
  {
    cout << "delete []" << array << endl;
 
    delete[] array;
    throw;
  }
  // ...
  cout << "delete []" << array << endl;
 
  delete[] array;
}
 
int main()
{
  try
  {
    Func();
  }
 
  catch (const char* errmsg)
  {
    cout << errmsg << endl;
  }
 
  return 0;
}

3.3 -> 异常规范

  1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。可以在函数的后面接throw(类型),列出这个函数可能抛出的所有异常类型。
  2. 函数的后面接throw(),表示函数不抛异常。
  3. 若无异常接口声明,则此函数可以抛出任何类型的异常。

4 -> 自定义异常体系

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

// 服务器开发中通常使用的异常继承体系
class Exception
{
public:
  Exception(const string& errmsg, int id)
    :_errmsg(errmsg)
    , _id(id)
  {}
 
  virtual string what() const
  {
    return _errmsg;
  }
 
protected:
  string _errmsg;
  int _id;
};
 
class SqlException : public Exception
{
public:
  SqlException(const string& errmsg, int id, const string& sql)
    :Exception(errmsg, id)
    , _sql(sql)
  {}
 
  virtual string what() const
  {
    string str = "SqlException:";
 
    str += _errmsg;
    str += "->";
    str += _sql;
 
    return str;
  }
 
private:
  const string _sql;
};
 
class CacheException : public Exception
{
public:
  CacheException(const string& errmsg, int id)
    :Exception(errmsg, id)
  {}
 
  virtual string what() const
  {
    string str = "CacheException:";
 
    str += _errmsg;
 
    return str;
  }
};
 
class HttpServerException : public Exception
{
public:
  HttpServerException(const string& errmsg, int id, const string& type)
    :Exception(errmsg, id)
    , _type(type)
  {}
 
  virtual string what() const
  {
    string str = "HttpServerException:";
 
    str += _type;
    str += ":";
    str += _errmsg;
 
    return str;
  }
 
private:
  const string _type;
};
 
void SQLMgr()
{
  srand(time(0));
  if (rand() % 7 == 0)
  {
    throw SqlException("权限不足", 100, "select * from name = '张三'");
  }
  //throw "xxxxxx";
}
 
void CacheMgr()
{
  srand(time(0));
  if (rand() % 5 == 0)
  {
    throw CacheException("权限不足", 100);
  }
  else if (rand() % 6 == 0)
  {
    throw CacheException("数据不存在", 101);
  }
 
  SQLMgr();
}
 
void HttpServer()
{
  // ...
  srand(time(0));
  if (rand() % 3 == 0)
  {
    throw HttpServerException("请求资源不存在", 100, "get");
  }
  else if (rand() % 4 == 0)
  {
    throw HttpServerException("权限不足", 101, "post");
  }
 
  CacheMgr();
}
 
int main()
{
  while (1)
  {
    this_thread::sleep_for(chrono::seconds(1));
    try 
    {
      HttpServer();
    }
    catch (const Exception& e) // 这里捕获父类对象就可以
    {
      // 多态
      cout << e.what() << endl;
    }
    catch (...)
    {
      cout << "Unkown Exception" << endl;
    }
  }
 
  return 0;
}

5 -> C++标准库的异常体系

C++提供了一系列标准的异常,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组。

异常 描述
std::exception 该异常是所有标准C++异常的父类
std::bad_alloc 该异常可以通过new抛出
std::bad_cast 该异常可以通过dynamic_cast抛出
std::bad_exception 处理C++程序中无法预测的异常时非常有用
std::bad_typeid 该异常可以通过typeid抛出
std::logic_error 理论上可以通过读取代码来检测到的异常
std::domain_error 当使用了一个无效的数字域时,会抛出该异常
std::invalid_argument 当使用了无效参数时,会抛出该异常
std::length_error 当创建了太长的std::string时,会抛出该异常
std::out_of_range 该异常可以通过方法抛出,例如std::vector和std::bitset<>::operator[]()
std::runtime_error 理论上不可以通过读取代码来检测到的异常
std::overflow_error 当发生数学上溢时,会抛出该异常
std::range_error 当尝试存储超出范围的值时,会抛出该异常
std::underflow_error 当发生数学下溢时,会抛出该异常

说明:实际中我们可以去继承exception类实现自己的异常类。但是实际中很多公司像上面一样自己定义一套异常继承体系。因为C++标准库设计的不够好用。

int main()
{
  try
  {
    vector<int> v(10, 5);
    // 这里如果系统内存不够也会抛异常
    v.reserve(1000000000);
    // 这里越界会抛异常
    v.at(10) = 100;
  }
 
  catch (const exception& e) // 这里捕获父类对象就可以
  {
    cout << e.what() << endl;
  }
 
  catch (...)
  {
    cout << "Unkown Exception" << endl;
  }
 
  return 0;
}

6 -> 异常的优缺点

C++异常的优点:

  1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug
  2. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们要层层返回错误,最外层才能拿到错误。
  3. 很多的第三方库都包含异常,比如boost、gtest、gmock等常用的库,那么我们使用它们也需要使用异常。
  4. 部分函数使用异常更好处理,比如构造函数没用返回值,不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。

C++异常的缺点:

  1. 异常会导致程序的执行流乱跳,并且非常混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
  2. 异常会有一些性能的开销。
  3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
  4. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常混乱。
  1. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用func() throw();的方式规范化。

感谢各位大佬支持!!!

互三啦!!!

目录
打赏
0
0
0
0
20
分享
相关文章
|
6月前
|
C++
C++ 语言异常处理实战:在编程潮流中坚守稳定,开启代码可靠之旅
【8月更文挑战第22天】C++的异常处理机制是确保程序稳定的关键特性。它允许程序在遇到错误时优雅地响应而非直接崩溃。通过`throw`抛出异常,并用`catch`捕获处理,可使程序控制流跳转至错误处理代码。例如,在进行除法运算或文件读取时,若发生除数为零或文件无法打开等错误,则可通过抛出异常并在调用处捕获来妥善处理这些情况。恰当使用异常处理能显著提升程序的健壮性和维护性。
104 2
【C++】异常
C++异常处理机制允许在程序运行时出现错误时,通过`try`、`catch`和`throw`关键字将错误信息传递回调用栈,进行异常处理。它支持异常的重新抛出、自定义异常体系以及标准库提供的异常类层次结构,如`std::exception`及其派生类。异常处理提高了代码的健壮性和可维护性,但也带来了性能开销和代码复杂性等问题。合理使用异常机制,可以有效提升程序的稳定性和安全性。
74 3
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展
本文介绍了如何通过四步实现C++插件化编程,实现功能定制与扩展。主要内容包括引言、概述、需求分析、设计方案、详细设计、验证和总结。通过动态加载功能模块,实现软件的高度灵活性和可扩展性,支持快速定制和市场变化响应。具体步骤涉及配置文件构建、模块编译、动态库入口实现和主程序加载。验证部分展示了模块加载成功的日志和配置信息。总结中强调了插件化编程的优势及其在多个方面的应用。
650 71
C++语言学习指南:从新手到高手,一文带你领略系统编程的巅峰技艺!
【8月更文挑战第22天】C++由Bjarne Stroustrup于1985年创立,凭借卓越性能与灵活性,在系统编程、游戏开发等领域占据重要地位。它继承了C语言的高效性,并引入面向对象编程,使代码更模块化易管理。C++支持基本语法如变量声明与控制结构;通过`iostream`库实现输入输出;利用类与对象实现面向对象编程;提供模板增强代码复用性;具备异常处理机制确保程序健壮性;C++11引入现代化特性简化编程;标准模板库(STL)支持高效编程;多线程支持利用多核优势。虽然学习曲线陡峭,但掌握后可开启高性能编程大门。随着新标准如C++20的发展,C++持续演进,提供更多开发可能性。
109 0
【实战经验】17个C++编程常见错误及其解决方案
想必不少程序员都有类似的经历:辛苦敲完项目代码,内心满是对作品品质的自信,然而当静态扫描工具登场时,却揭示出诸多隐藏的警告问题。为了让自己的编程之路更加顺畅,也为了持续精进技艺,我想借此机会汇总分享那些常被我们无意间忽视却又导致警告的编程小细节,以此作为对未来的自我警示和提升。
645 13
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
103 2
C++提高篇:泛型编程和STL技术详解,探讨C++更深层的使用
文章详细探讨了C++中的泛型编程与STL技术,重点讲解了如何使用模板来创建通用的函数和类,以及模板在提高代码复用性和灵活性方面的作用。
94 2
C++提高篇:泛型编程和STL技术详解,探讨C++更深层的使用
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
112 11