异常
约 3095 字大约 10 分钟
2025-04-06
错误处理在所有编程语言中,都是一件棘手的问题,也是一个备受争议的话题。这个问题在 C++
这里尤其严重:因为历史的原因,C++
并没有统一的错误处理方式。目前,关于错误处理的方式,**C++**
** 社区基本分裂为异常和非异常(返回值)两个阵营**。
异常
异常是程序在执行期间产生的问题。C++
异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。异常处理提供了一种可以使程序从执行的某点将控制流和信息转移到与执行先前经过的某点相关联的处理代码的方法(换言之,异常处理将控制权沿调用栈向上转移)。
C++
异常处理涉及到三个关键字:try、catch、throw、noexcept
。
throw
: 当问题出现时,程序会抛出一个异常。这是通过使用throw
关键字来完成的。catch
: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch
关键字用于捕获异常。try
:try
块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个catch
块。noexcept
**:**用于描述函数不会抛出异常,一旦有异常抛出,会立刻终止程序,它可以阻止异常的传播与扩散。noexcept
可以带一个“常量表达式作为参数,常量表达式为true
,表示不会抛出异常,否则代表可以抛出异常
如果有一个块抛出一个异常,捕获异常的方法会使用 try
和 catch
关键字。try
块中放置可能抛出异常的代码,try
块中的代码被称为保护代码。使用 try/catch
语句的语法如下所示:
try
{
// 保护代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}
如果 try
块在不同的情境下会抛出不同的异常,这个时候可以尝试罗列多个 catch
语句,用于捕获不同类型的异常。
抛出异常
可以使用 throw
语句在代码块中的任何地方抛出异常。throw
语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。以下是尝试除以零时抛出异常的实例:
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 异常的代码
}
上面的代码会捕获一个类型为 ExceptionName
的异常。如果您想让 catch
块能够处理 try
块抛出的任何类型的异常,则必须在异常声明的括号内使用省略号 ...
,如下所示:
try
{
// 保护代码
}catch(...)
{
// 能处理任何异常的代码
}
下面是一个实例,抛出一个除以零的异常,并在 catch
块中捕获该异常。
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!
C++ 标准的异常
C++
提供了一系列标准的异常,定义在<exception>
中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:

下表是对上面层次结构中出现的每个异常的说明:
异常 | 描述 |
---|---|
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**
类来定义新的异常。下面的实例演示了如何使用 std::exception
类来实现自己的异常:
#include <iostream>
#include <exception>
using namespace std;
struct MyException : public exception
{
const char * what () const
{
return "C++ Exception";
}
};
int main()
{
try
{
throw MyException();
}
catch(MyException& e)
{
std::cout << "MyException caught" << std::endl;
std::cout << e.what() << std::endl;
}
catch(std::exception& e)
{
//其他的错误
}
}
这将产生以下结果:
MyException caught
C++ Exception
抑制new抛异常
当使用new
申请内存时,如果内存申请失败,会抛出std::bad_alloc
异常,需要如下处理:
try
{
while (true)
{
new char[1024];
}
}
catch (const std::bad_alloc& e)
{
cout << "has exception "<<e.what() << endl;
}
如果想根据返回的指针来判断,就需要抑制new
抛出异常。
double* p = nullptr;
do
{
p = new(std::nothrow) double[1024];
} while (p);
非异常
为了保证与C
语言的兼容性,C++
从C
语言那里继承了各种基于错误返回码的机制,常见的有两种:
- 返回各种特殊值用于表示各种不同的错误原因(通常返回 0 表示成功)。
- 返回 0 值表示成功;返回非 0 表示错误(大部分情况返回 -1),并通过设置全局状态(
errno
)来表示具体错误原因。
int getchar(); // 遇到文件结尾返回 -1
char* malloc(int); // 如果分配出错,返回 0
基于错误返回码的错误处理机制,存在一些天然的缺陷:
- 繁琐且重复的错误检查使代码变得混乱。
- 构造函数没有返回值,错误返回码无法处理构造函数出错的情形。类似的,重载的运算符执行出错也没法返回错误码。
- 还有,最令人头疼的是,开发者可能会忘记检查错误或者没有正确处理返回码。
关于基于返回值的错误处理方式,C++ 也进行了一些增强:
std::error_code
C++11 引入了 std::error_code
增强了错误码的概念。
class maye_category : public std::error_category
{
const char* error_msg[100] = {"one","two","three","four"};
const char* mapStr(int errval) const
{
if (errval == 0)
{
return "No Error";
}
if (errval < 255 || errval > 255 + 100)
{
return "unknown Error";
}
return error_msg[errval - 255];
}
public:
virtual const char* name() const noexcept override
{
return "maye_category";
}
virtual std::string message(int _Errval) const override
{
return std::string(mapStr(_Errval));
}
};
void sendMsg(const std::string& msg, std::error_code& code)
{
//code = std::make_error_code(std::errc::invalid_argument);
static maye_category cate;
code.assign(257, cate);
//code.assign(255,)
}
int main()
{
std::error_code error;
sendMsg("hello", error);
if (error)
{
std::cout << "has erro " << error.value() << " " << error.message() << " " << error.category().name() << std::endl;
}
std::cout << sqrt(-1) << std::endl;
struct ss
{
ss(int a, int b) {}
int a;
int b;
};
std::optional<ss> v({ 2,3 });
//std::cout << v.has_value() <<" "<<v.value() << std::endl;
return 0;
}
std::optional
有时我们会用一个值来表示一种“没有什么意义”的状态,这就是C++17
的std::optional
的用处,允许函数返回“空值(nothing
)“,增强了函数接口的表达能力。
在编写程序时,我们常常遇到一种情况,那就是我们不总是有一个固定值来表示一个事物。例如,找出文本中的第一个偶数(如果存在的话)。在以前的代码中,这些情况一般使用魔术值(magic value
)或者空指针(null pointers
)来表示。一个魔术值可以是一个空的字符串、0、-1或者一个最大的非负值(例如std::string::npos
)。
这两个方法都有他们的缺点。魔术值人为地限制了可获得的值得范围,它也仅仅按照惯例与那些合法、正常的值分开来。对于一些类型,没有明显的魔术值,或者无法用常规手段创建魔术值。用空指针表示没有意义的值意味着其他合法的值必须被分配一个地址空间,这是一个代价高昂的操作并且难以实现。
另一种方法是提供两次查询:首先询问是否有一个有意义的值,如果答案是真的,就查找这个值。实现这个会导致查找代码的不必要的重复,并且他的使用也不够安全。如果要查找的值不存在,第二次查询的实现就必须要做点什么,例如返回一个容易被误解的值,这个值会引起未定义的行为,或者直接抛出一个异常,后者通常是唯一明智的行为。
C++17
引入了std::optional
,类似于std::variant
,std:optional
是一个和类型(译者注:和类型即sum type
,如果你熟悉C++
中的union
,那么就不难理解这里的sum
。如果一个union
包含两个类型,一个bool
类型和一个uint8_t
类型,那么这个union
一共会有2+28 = 258种值,所以我们称之为和类型,因为它们的类型数量是用各个类型的类型数量累加求得的。如果换成struct
,那么这里的类型数量就是2*28=512种),它是类型T
所有值和一个单独的“什么都没有”的状态的和。
后者有专门的名字:它的类型是std::nullopt_t
,并且它有一个值std::nullopt
。那听上去很熟悉,它和nullptr
的概念相同,不同的是后者是C++内置的关键词。
std::optional
具有我们所期望的所有特性:我们可以用任何可以被转化为T
的类型来构造和赋值,我们也可用std::nullopt
和默认构造函数来构造和赋值。我们还能从其他类型的std::optional
初始化一个另外类型的std::optional
,只要这两个类型可以相互转化。结果会包含被转换的值或者会为空,跟我们的预期相符。
我们可以像上面描述的那样查询std::optional
,has_value()
告诉我们是否有一个值,value()
则返回这个值。如果没有值并且我们还调用了value()
,会抛出一个类型为std::bad_optional_access
的异常。或者我们可以使用value_or(U&& default)
来得到值,如果std::optional
为空,则得到default
。
#include<iostream>
#include<optional>
//从字符串中找到第一个能被n整除的数
std::optional<int> firstNumberDivisible(const std::string& str, int n)
{
//0不能做除数
if (n == 0)
{
return std::optional<int>();
}
for (size_t i = 0; i < str.size(); i++)
{
if (std::isdigit(str[i]))
{
if ((str[i] - '0') % n == 0)
{
return std::make_optional<int>(str[i] - '0');
}
//std::cout << str[i] - '0' << " ";
}
}
return std::optional<int>();
}
int main()
{
std::string text = "876543210";
std::optional<int> opt = firstNumberDivisible(text, 10);
//如果找到返回找到的值,没有找到返回999
int v = opt.value_or(999);
std::cout << "first number is " << v << std::endl;
if (opt.has_value())
{
std::cout << "first number is " << opt.value() << std::endl;
}
else
{
std::cout << "not found" << std::endl;
}
return 0;
}
除了这些显式的方法,std::optional
还重载了bool
类型转换,它可以显式转化为bool
来表示std::optional
是否有一个值。指针的解引用操作符*
和->
都实现了,但是没有std::bad_optional_access
异常,用这种方式访问一个空的std::optional
是一个未定义的行为。最后,reset()
清除std::optional
包含的对象,让它为空。
上面的代码因此可以写成这样:
if (opt)
{
std::cout << "first number is " << *opt << std::endl;
}
为了方便构造std::optional
,提供了std::make_optional
函数模板;emplace(Args..)
可以对std::optional
对象重新构造值。
auto optNums = std::make_optional<std::vector<int>>({ 1,3,5,7,9,3,4,56 });