一、C 语言传统的错误处理方式
在 C 语言中,处理错误主要有两种传统方式,每种方式都有其特点和局限性。
1. 终止程序
- 原理:使用类似
assert
这样的断言机制,当程序运行到某个条件不满足时,直接终止程序的执行。 - 示例代码
#include <assert.h> #include <stdio.h> void divide(int a, int b) { assert(b != 0); // 断言b不为0,如果b为0,程序会终止 printf("%d / %d = %d\n", a, b, a / b); } int main() { divide(10, 0); return 0; }
- 缺陷:这种方式对用户不太友好,因为一旦出现错误,程序会直接崩溃,用户无法进行一些必要的处理或恢复操作。例如在发生内存错误、除 0 错误等情况时,程序会突然终止,给用户带来不好的体验。
2. 返回错误码
- 原理:函数在执行过程中,如果遇到错误,会返回一个特定的错误码。程序员需要根据这个错误码去查找对应的错误信息。在系统的很多库的接口函数中,会把错误码放到
errno
变量中,表示发生的错误。 - 示例代码
#include <stdio.h> #include <errno.h> #include <string.h> int divide(int a, int b) { if (b == 0) { errno = EINVAL; // 设置错误码为无效参数 return -1; } return a / b; } int main() { int result = divide(10, 0); if (result == -1) { printf("Error: %s\n", strerror(errno)); // 根据错误码输出错误信息 } else { printf("Result: %d\n", result); } return 0; }
- 缺陷:需要程序员手动去查找和处理错误码对应的错误信息,增加了开发的复杂度。而且不同的库可能使用不同的错误码体系,容易造成混淆。
实际应用情况
在实际的 C 语言编程中,基本都是使用返回错误码的方式处理错误,只有在处理非常严重的错误时,才会使用终止程序的方式。
二、C++ 异常概念
C++ 引入了异常机制,为错误处理提供了一种更加灵活和强大的方式。当一个函数发现自己无法处理的错误时,可以抛出异常,让函数的直接或间接调用者来处理这个错误。
1. 异常处理的关键字
throw
:当问题出现时,程序会抛出一个异常。通过使用throw
关键字来完成,后面可以跟任意类型的数据,如整数、字符串、自定义对象等。catch
:用于捕获异常。可以有多个catch
块,每个catch
块可以捕获不同类型的异常。try
:try
块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个catch
块。try
块中的代码被称为保护代码。
2. try/catch
语句的语法
try
{
// 保护的标识代码
}
catch( ExceptionName e1 )
{
// catch 块,处理类型为ExceptionName的异常
}
catch( ExceptionName e2 )
{
// catch 块,处理类型为ExceptionName的异常
}
catch( ExceptionName eN )
{
// catch 块,处理类型为ExceptionName的异常
}
3. 示例代码
#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);
}
}
// 定义一个函数,调用Division函数并处理异常
void Func() {
try {
int len, time;
cout << "请输入两个整数(用空格分隔): ";
cin >> len >> time;
cout << "除法结果: " << Division(len, time) << endl;
}
catch (const char* errmsg) {
cout << "捕获到异常: " << errmsg << endl;
}
}
int main() {
try {
Func();
}
catch (const char* errmsg) {
cout << "捕获到字符串类型异常: " << errmsg << endl;
}
return 0;
}
三、异常处理基础
1. 异常抛出与捕获原则
抛出机制:通过抛出对象触发异常(可抛出任意类型对象)
匹配规则:
匹配类型相同且位置最近的catch块
#include <iostream> // 函数B抛出整数类型异常 void functionB() { throw 42; } // 函数A调用函数B,并尝试捕获异常 // functionB 抛出一个 int 类型异常。 void functionA() { try { functionB(); } catch (double) { std::cout << "在functionA中捕获到double类型异常" << std::endl; } catch (int num) { //与第二个 catch 块匹配,且它离异常抛出点 functionB 最近, //所以会执行该 catch 块,输出“在functionA中捕获到int类型异常, //值为: 42”。 std::cout << "在functionA中捕获到int类型异常,值为: " << num << std::endl; } } int main() { try { functionA(); } catch (...) { //如果functionA中没有匹配int类型的catch块,异常会传递到main函数, //main函数中的 catch(...) 会捕获所有类型异常。 std::cout << "在main中捕获到其他类型异常" << std::endl; } return 0; }
会生成异常对象的拷贝(保证异常对象有效性)
// 抛出字符串异常示例 double Division(int a, int b) { if (b == 0) { string s("Division by zero!"); throw s; // 抛出拷贝后的临时对象 } return static_cast<double>(a)/b; } void func() { int x, y; cin >> x >> y; cout << Division(x, y) << endl; } int main() { while (true) { try { func(); } catch (const string& err) { // 捕获引用避免拷贝 cout << "Error: " << err << endl; } } }
派生类异常可用基类捕获(实际开发常用方式)
2. 通用捕获与继承体系
// 通用捕获与继承示例
class BaseException {};
class MathException : public BaseException {};
int main() {
try {
throw MathException();
}
catch (const BaseException&) { // 基类捕获派生类异常
cout << "Base exception caught" << endl;
}
catch (...) { // 最后防线捕获所有异常
cout << "Unknown exception" << endl;
}
}
四、异常传播机制
1. 栈展开过程
检查throw所在try块
逐层回退调用栈查找匹配catch
到达main未匹配则程序终止
异常处理后继续执行catch块后续代码
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;
}
五、异常安全规范
1. 关键原则
构造函数:避免抛出异常(可能导致对象不完整)
析构函数:禁止抛出异常(防止资源泄漏)
RAII机制:通过智能指针等实现资源自动管理
2.异常规范
- 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的 后面接throw(类型),列出这个函数可能抛掷的所有异常类型。
- 函数的后面接throw(),表示函数不抛异常。
- 若无异常接口声明,则此函数可以抛掷任何类型的异常。
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;
六、自定义异常体系设计
1. 服务器开发典型继承体系
在服务器开发中,为了更好地管理和处理不同类型的异常,通常会设计一个异常继承体系。以下是一个示例代码:
// 异常基类(抽象错误类型)
// Exception 类:作为基类异常,包含异常信息 _errmsg 和异常编号 _id,
// 并定义了虚函数 what() 用于返回异常信息,支持多态。
class Exception {
public:
Exception(const string& errmsg, int id)
: _errmsg(errmsg), _id(id) {}
virtual string what() const {
return "[" + to_string(_id) + "] " + _errmsg;
}
virtual ~Exception() = default; // 虚析构保证正确释放
protected:
string _errmsg; // 错误描述
int _id; // 错误编号
};
// SQL操作异常(具体错误类型)
// SqlException 类:继承自 Exception 类,添加了 _sql 成员变量,重写了
// what() 函数,返回更详细的 SQL 异常信息。
class SqlException : public Exception {
public:
SqlException(const string& errmsg, int id, const string& sql)
: Exception(errmsg, id), _sql(sql) {}
virtual string what() const override {
return Exception::what()
+ "\n[SQL] " + _sql;
}
private:
string _sql; // 错误关联的SQL语句
};
// 缓存异常类,继承自 Exception
// CacheException 类:继承自 Exception 类,重写了 what() 函数,返回缓存异常信息
class CacheException : public Exception {
public:
CacheException(const std::string& errmsg, int id)
: Exception(errmsg, id)
{}
// 重写 what 函数,返回缓存异常信息
virtual std::string what() const {
std::string str = "CacheException:";
str += _errmsg;
return str;
}
};
// HTTP服务异常(具体错误类型)
// HttpServerException 类:继承自 Exception 类,添加了 _type 成员变量,
// 重写了 what() 函数,返回 HTTP 服务器异常信息。
class HttpServerException : public Exception {
public:
HttpServerException(const string& errmsg, int id, const string& type)
: Exception(errmsg, id), _type(type) {}
virtual string what() const override {
return "[HTTP " + _type + " Error] "
+ Exception::what();
}
private:
string _type; // 请求类型(GET/POST等)
};
what()
函数的作用:what()
函数是一个虚函数,在基类中定义,派生类可以重写该函数。通过基类指针或引用调用what()
函数时,会根据实际对象的类型调用相应派生类的what()
函数,实现多态。这样可以方便地根据不同的异常类型输出不同的异常信息。
2. 异常体系使用示例
// 模拟 SQL 管理函数,可能抛出 SqlException
void SQLMgr() {
srand(time(0));
if (rand() % 7 == 0) {
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
std::cout << "执行成功" << std::endl;
}
// 模拟缓存管理函数,可能抛出 CacheException 或调用 SQLMgr 抛出 SqlException
void CacheMgr() {
srand(time(0));
if (rand() % 5 == 0) {
throw CacheException("权限不足", 100);
} else if (rand() % 6 == 0) {
throw CacheException("数据不存在", 101);
}
SQLMgr();
}
// 模拟 HTTP 服务器函数,可能抛出 HttpServerException 或调用 CacheMgr 抛出其他异常
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) {
Sleep(500);
try {
HttpServer();
} catch (const Exception& e) { // 捕获基类异常对象,利用多态处理不同派生类异常
std::cout << e.what() << std::endl;
} catch (...) {
std::cout << "Unkown Exception" << std::endl;
}
}
return 0;
}
3. 常用异常编号
编号 | 异常描述 |
---|---|
1 | 没有权限 |
2 | 服务器挂了 |
3 | 网络错误 |
七、常用标准库异常
bad_alloc
:当使用new
运算符进行动态内存分配失败时,会抛出bad_alloc
异常。out_of_range
:当使用容器(如std::vector
、std::string
等)的成员函数访问越界元素时,会抛出out_of_range
异常。invalid_argument
:当函数接收到无效的参数时,会抛出invalid_argument
异常。
八、异常的优缺点
1. 优点
- 清晰准确的错误信息:异常对象可以定义丰富的信息,相比错误码方式,能更清晰准确地展示错误信息,甚至可以包含堆栈调用信息,有助于更好地定位程序的 bug。
- 简化错误处理流程:在函数调用链中,使用返回错误码的传统方式需要层层返回错误,最外层才能拿到错误信息并处理。而异常体系中,不管是深层函数还是中间层函数出错,抛出的异常会直接跳到合适的
catch
块中,由调用者直接处理错误。//示例代码(错误码方式) #include <iostream> #include <errno.h> int ConnectSql() { // 用户名密码错误 if (...) { return 1; } // 权限不足 if (...) { return 2; } return 0; } int ServerStart() { if (int ret = ConnectSql() < 0) { return ret; } int fd = socket(); if (fd < 0) { return errno; } return 0; } int main() { if (ServerStart() < 0) { // 处理错误 } return 0; }
//示例代码(异常方式) #include <iostream> #include <stdexcept> void ConnectSql() { // 用户名密码错误 if (...) { throw std::runtime_error("用户名密码错误"); } // 权限不足 if (...) { throw std::runtime_error("权限不足"); } } void ServerStart() { ConnectSql(); int fd = socket(); if (fd < 0) { throw std::system_error(errno, std::system_category(), "socket 错误"); } } int main() { try { ServerStart(); } catch (const std::exception& e) { std::cout << "捕获到异常: " << e.what() << std::endl; } return 0; }
- 与第三方库兼容:很多第三方库(如
boost
、gtest
、gmock
等)都使用了异常机制,使用这些库时,我们也需要使用异常来与之配合。 - 适合特定函数:对于一些没有返回值的函数(如构造函数),或者不方便使用返回值表示错误的函数(如
T& operator[]
),使用异常处理错误更加合适。
2. 缺点
- 执行流混乱:异常会导致程序的执行流乱跳,尤其是在运行时出错抛异常时,会使程序的控制流变得复杂,增加了跟踪调试和分析程序的难度。
- 性能开销:异常处理会有一定的性能开销,不过在现代硬件速度较快的情况下,这个影响通常可以忽略不计。
- 异常安全问题:C++ 没有垃圾回收机制,资源需要自己管理。使用异常容易导致内存泄漏、死锁等异常安全问题,需要使用 RAII(资源获取即初始化)技术来管理资源,增加了学习成本。
- 标准库异常体系混乱:C++ 标准库的异常体系定义不够完善,导致不同开发者各自定义自己的异常体系,使得代码的可维护性和兼容性受到影响。
- 异常规范问题:异常需要规范使用,否则会给外层捕获异常的用户带来困扰。异常规范主要包括两点:一是抛出的异常类型都继承自一个基类,二是函数是否抛异常、抛什么异常,都使用
func() throw();
的方式进行规范化。
总结
异常总体而言利大于弊,在工程中鼓励使用异常。而且面向对象的编程语言基本都采用异常处理错误,这也是软件开发的趋势。