1. c++异常概念
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。 C++ 异常处理涉及到三个关键字:try、catch 和 throw。
- throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
- catch: 在想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
- try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。
使用 try/catch 语句的语法如下所示:
try{
// 保护代码
}
catch (ExceptionName e1) {
// catch 块
}
catch (ExceptionName e2) {
// catch 块
}
catch (ExceptionName eN) {
// catch 块
}
如果 try 块在不同的情境下会抛出不同的异常,这个时候可以尝试罗列多个 catch 语句,用于捕获不同类型的异常。
抛出异常时,可以使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。
throw有时需要exception,它是C++标准库中的一个类,它是所有标准C++异常的基类exception类提供了一些公共方法,其中最重要的是what()方法,它返回一个字符串,该字符串描述了异常的原因。
exception
其中我们只需要知道他是一个类,里面有一个what函数,exception.what()抛出exception异常,如果想改变抛出的信息,我们可以用一个自定义类继承exception类,然后用虚函数的重写来实现即可。
具体例子如下:
// exception constructor
#include <iostream> // std::cout
#include <exception> // std::exception
struct ooops : std::exception {
const char* what() const noexcept {return "Ooops!\n";}
};
int main () {
ooops e;
std::exception* p = &e;
try {
throw e; // throwing copy-constructs: ooops(e)
} catch (std::exception& ex) {
std::cout << ex.what();
}
try {
throw *p; // throwing copy-constructs: std::exception(*p)
} catch (std::exception& ex) {
std::cout << ex.what();
}
return 0;
}
运行结果:
Ooops!
exception
2. 异常的简单使用
异常的抛出和匹配原则:
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
- 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
- catch(…)可以捕获任意类型的异常,问题是不知道异常错误是什么。
- 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用。
捕获异常的例子:
double division(int a, int b)
{
if (b == 0)
{
throw "Division by zero condition!";
}
return (a / b);
}
捕获异常时,catch 块跟在 try 块后面,用于捕获异常。你可以指定想要捕捉的异常类型,这是由 catch 关键字后的括号内的异常声明决定的。例如:
try
{
// 保护代码
}
catch (ExceptionName e)
{
// 处理 ExceptionName 异常的代码
}
下面是一个实例,抛出一个除以零的异常,并在 catch 块中捕获该异常:
#include <iostream>
using namespace std;
double division(int a, int b)
{
if (b == 0)
{
throw "Division by zero condition!";
}
return (a / b);
}
int main()
{
int x = 50;
int y = 0;
double z = 0;
try
{
z = division(x, y);
cout << z << endl;
}
catch (const char* msg)
{
cerr << msg << endl;
}
return 0;
}
由于我们抛出了一个类型为 const char* 的异常,因此,当捕获该异常时,我们必须在 catch 块中使用 const char*。当上面的代码被编译和执行时,它会产生下列结果:Division by zero condition!
在函数调用链中异常栈展开匹配原则:
- 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。
- 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
- 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(…)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
- 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
栈展开过程如下,首先检查throm本身 是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则处理。没有则退出 当前函数栈,继续在调用函数的栈中进行查找,不断;重复上述过程,若到达main函数的栈,依旧没有匹配的,则终止程序
#include <iostream>
using namespace std;
class Exception
{
public:
Exception(const int code, const string msg)
:_code(code)
,_msg(msg)
{}
int GetCode() const
{
return _code;
}
string Getmsg() const
{
return _msg;
}
~Exception()
{}
private:
int _code; // 错误码
string _msg; // 错误信息
};
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
Exception e(1, "Division by zero condition!");
throw(e);
}
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 Exception& e)
{
cout << "错误码:" << e.GetCode() << ",错误信息:" << e.Getmsg() << endl;
}
catch (...)
{
cout << "unkown exception" << endl;
}
return 0;
}
3. 自定义异常体系
实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异常,那么外层的调用者基本就用不了了,所以实际中都会定义一套继承的规范体系。这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了。
class MyException : public std::exception
{
public:
MyException(const std::string& msg)
: message(msg)
{}
const char* what() const noexcept override
{
return message.c_str();
}
private:
std::string message;
};
class FileReadException : public MyException
{
public:
FileReadException(const std::string& filename)
: MyException("Error reading file: " + filename)
{}
};
class NetworkException : public MyException
{
public:
NetworkException(const std::string& host)
: MyException("Network error with host: " + host)
{}
};
void readFromFile(const std::string& filename)
{
throw FileReadException(filename);
}
void connectToServer(const std::string& host)
{
throw NetworkException(host);
}
int main()
{
try
{
readFromFile("data.txt");
}
catch (const MyException& e)
{
std::cerr << "Exception caught: " << e.what() << std::endl;
}
return 0;
}
首先,我们可以创建一个自定义的基本异常类,例如 MyException,继承自标准库的 std::exception,在基本异常类的基础上,我们可以派生出其他业务类型的异常,例如 FileReadException 和 NetworkException。
4. 异常的规范和安全
4.1 异常规范
C++ 异常规范是一项 C++ 语言功能,用于指示程序员对函数可能抛出的异常类型的意图。它允许我们明确声明函数是否可以或不可以因异常而退出。编译器可以利用这些信息来优化对函数的调用,并在异常意外地离开函数时终止程序。
异常规范有两个关键字:
throw:当问题出现时,程序会抛出一个异常。我们使用 throw 关键字来实现这一点。
noexcept:在您想要处理问题的地方,通过异常处理程序捕获异常。noexcept 关键字用于指定函数不会抛出异常。
让我们来看一个简单的例子:
#include <iostream>
double divide(int a, int b) noexcept
{
if (b == 0)
{
throw "Division by zero condition!";
}
return static_cast<double>(a) / b;
}
int main()
{
int numerator = 50;
int denominator = 0;
double result = 0;
try
{
result = divide(numerator, denominator);
std::cout << "Result: " << result << std::endl;
}
catch (const char* msg)
{
std::cerr << "Exception caught: " << msg << std::endl;
}
return 0;
}
在上面的代码中,我们定义了一个 divide 函数,它计算两个整数的除法。如果除数为零,我们抛出一个异常。在 main 函数中,我们使用 try 块来调用 divide 函数,并在 catch 块中捕获异常。注意,我们在 divide 函数的声明中使用了 noexcept 关键字,表示该函数不会抛出异常。
C++ 异常规范是一个有关异常处理的重要主题。它的优点和缺点如下。
优点:
- 清晰准确的错误信息:异常对象的抛出相比于错误码的方式,可以清晰准确地展示出错的各种信息,甚至还可以包含堆栈的信息,有助于定位程序的 Bug.
- 简化代码:异常减少了对错误码的层层传递和检查,使得正常代码的结构更清晰。每个错误码的处理不再需要一堆 if-else 语句,提高了代码可读性.
- 方便的构造函数报错:异常让构造函数更容易报告错误,而不需要返回错误码.
缺点:
- 历史原因:某些项目禁用异常,主要是因为历史原因。例如,Google 曾禁用异常,但这是因为他们已有大量非异常安全的代码,而不是出于哲学或道德原因
- 性能问题:在某些项目中,异常被禁用,因为工具链不能保证抛出异常时的实时性能。这可能导致二进制文件大小增加,因为异常产生的位置决定了需要如何做栈展开,这些数据需要存储在表里.
- 二进制文件大小:使用异常会增加二进制文件大小,因为异常产生的位置需要存储在表里。典型情况下,使用异常和不使用异常比,二进制文件大小会有约百分之十到二十的上升.
4.2 异常安全
C++ 异常安全是一项关键的编程概念,旨在确保程序在发生异常时能够维持一致的状态,避免资源泄漏和数据结构破坏。
异常安全的概念:异常安全意味着当程序在异常发生时,它可以“回退得很干净”。具体而言,一个函数在发生异常时应该满足两个条件:1.不泄漏资源:已申请的资源必须被正确释放。2.不破坏数据结构:不会导致野指针等问题。
异常安全分为三个级别:
- 基本级别:可能发生异常,但在异常发生时代码保证做了必要的清理工作,使对象保持合法状态。
- 强烈级别:可能发生异常,但在异常发生时代码保证对数据的任何修改都可以回滚,即要么完全成功,要么保持调用之前的状态。
- 无异常:函数不会抛出异常(例如标准库的 swap 函数)。
反面例子:资源泄漏:例如,互斥锁的获取和释放。如果在获取锁后发生异常,释放锁的代码将不会执行,导致资源泄漏。
数据破坏:例如,自定义类的赋值操作符重载。如果在分配新资源时抛出异常,对象的数据可能会遭到破坏。
解决方案:
资源泄漏:使用对象来管理资源,例如使用 RAII 技术或智能指针。
数据破坏:使用“拷贝并交换”策略,先创建副本,然后在副本上进行修改,最后交换资源。
总结:编写异常安全的代码需要注意资源管理和数据修改的问题,以及使用 RAII 和拷贝并交换等技术。
最佳实践:
使用 RAII 管理资源,避免资源泄漏。
注意异常发生时的回滚机制。
减少全局变量的使用,保证局部变量的异常安全性。
不知道如何处理异常时,不要捕获异常,直接终止程序。
5. C++标准库的异常体系和异常的优缺点
C++标准库定义了一套异常类体系,其根部是名为 exception 的抽象基类。标准库抛出的异常都是 exception 的子类,称为标准异常(Standard Exception)。接下来深入了解一下这些异常类:
1.exception:
exception 是所有标准异常的基类。
它声明了一个 what() 虚函数,用于返回一个 const char*,表示被抛出异常的文字描述。
2.标准异常类:
这些异常类都继承自 exception,并提供了特定类型的错误信息。
一些常见的标准异常类包括:
logic_error 及其子类:表示逻辑错误,例如 std::invalid_argument、std::domain_error 等。
runtime_error 及其子类:表示运行时错误,例如 std::overflow_error、std::out_of_range 等。
3.使用标准异常:
在代码中,我们可以使用这些标准异常来处理特定的错误情况。
例如,如果发生了除以零的操作,可以抛出 std::runtime_error。
异常处理是软件开发中的重要主题,它有一些优点和缺点。
优点:
- 清晰准确的错误信息:异常对象的抛出相比于错误码的方式,可以清晰准确地展示出错的各种信息,甚至还可以包含堆栈的信息,有助于定位程序的 Bug。
- 简化代码:异常减少了对错误码的层层传递和检查,使得正常代码的结构更清晰。每个错误码的处理不再需要一堆 if-else 语句,提高了代码可读性。
- 方便的构造函数报错:异常让构造函数更容易报告错误,而不需要返回错误码。
缺点:
- 历史原因:某些项目禁用异常,主要是因为历史原因。例如,Google 曾禁用异常,但这是因为他们已有大量非异常安全的代码,而不是出于哲学或道德原因。
- 性能问题:
在某些项目中,异常被禁用,因为工具链不能保证抛出异常时的实时性能。这可能导致二进制文件大小增加,因为异常产生的位置决定了需要如何做栈展开,这些数据需要存储在表里。 - 二进制文件大小:使用异常会增加二进制文件大小,因为异常产生的位置需要存储在表里。
典型情况下,使用异常和不使用异常比,二进制文件大小会有约百分之十到二十的上升。
总之,异常处理在不同项目和场景中有不同的利弊。在编写代码时,需要权衡这些因素,根据项目需求和性能要求来选择是否使用异常。