目录
C语言与C++错误处理方式的对比及应用
在编程中,错误处理是不可避免的。传统的C语言和现代的C++在处理错误上有着明显的区别,前者依赖返回错误码的方式,而后者则引入了更为灵活的异常机制。这篇文章将探讨这两种语言在错误处理方面的不同,并介绍如何在实际工程中合理使用它们。
一、C语言传统的错误处理方式
1. 终止程序:assert
C语言中最简单的错误处理方式之一是直接终止程序,例如使用 assert
。当程序在运行时遇到不可恢复的错误(如除零、内存访问越界等),程序会被强制终止。这种方式虽然简单,但有明显的缺陷:
- 用户体验不佳:一旦程序遇到问题就立即崩溃,用户无法继续使用程序,特别是在遇到小问题时。
- 缺乏灵活性:程序员无法提供其他的错误处理路径,例如记录日志、尝试恢复等。
2. 返回错误码
另一种常用的处理方式是返回错误码。很多C语言的库都会通过返回一个整数值来表示函数的执行结果。具体的错误信息通常会被存储在全局变量 errno
中,程序员可以通过查阅 errno
的值来判断出错的具体原因。
示例代码:
int func() {
if (/* 错误发生 */) {
errno = EINVAL; // 设置错误码
return -1; // 返回错误
}
return 0; // 返回成功
}
缺点:
- 额外的错误处理负担:程序员需要手动检查每个函数调用的返回值,并根据
errno
做出相应的处理。这增加了代码的复杂性和出错的可能性。 - 函数调用链复杂:如果错误在深层函数中发生,那么需要逐层返回错误,直到外层函数捕捉并处理,这导致代码难以维护。
例如:
- 下面这段伪代码我们可以看到ConnnectSql中出错了,先返回给ServerStart,ServerStart再返回给main函数,main函数再针对问题处理具体的错误。
- 如果是异常体系,不管是ConnnectSql还是ServerStart及调用函数出错,都不用检查,因为抛出的异常异常会直接跳到main函数中catch捕获的地方,main函数直接处理错误。
int ConnnectSql()
{
// 用户名密码错误
if (...)
return 1;
// 权限不足
if (...)
return 2;
}
int ServerStart() {
if (int ret = ConnnectSql() < 0)
return ret;
int fd = socket()
if(fd < 0)
return errno;
}
int main()
{
if (ServerStart() < 0)
...
return 0;
}
二、C++中的异常处理机制
C++ 引入了异常处理机制,使得程序在遇到错误时不需要直接终止或通过返回值处理,可以通过抛出异常的方式将问题传递到合适的地方进行处理。
1. 基本概念
throw
:用于在错误发生时抛出异常。异常可以是任何类型的对象,程序员可以抛出字符串、整型或自定义对象来传递错误信息。try
:包含可能抛出异常的代码块。如果try
块中的代码抛出了异常,程序会跳转到相应的catch
块处理异常。catch
:用于捕获异常。catch
块可以捕获特定类型的异常,并根据异常类型进行处理。
示例代码:
double Division(int a, int b) {
if (b == 0)
throw "Division by zero condition!"; // 抛出异常
return (double)a / (double)b;
}
int main() {
try {
cout << Division(10, 0) << endl; // 可能抛出异常
} catch (const char* errmsg) {
cout << errmsg << endl; // 捕获并处理异常
}
return 0;
}
2. 异常的抛出与捕获
C++的异常处理机制基于类型匹配。在抛出异常时,程序会根据异常的类型查找最接近的 catch
块。如果没有匹配的 catch
,程序将继续沿调用链向外查找,最终未被捕获的异常会导致程序终止。
- 类型匹配:抛出的异常必须与
catch
块的类型匹配,否则将无法捕获。 - 继承与多态:可以抛出派生类对象,并使用基类捕获。这在复杂项目中非常实用,因为可以通过捕获基类来处理一类相关的错误。
catch (...) {
// 捕获所有类型的异常,防止程序崩溃
cout << "Unknown exception occurred!" << endl;
}
- 抛异常可以抛任意类型对象
- 捕获时,要求类型匹配
3. 异常的重新抛出
有时,捕获到异常后,当前函数无法处理该异常,而需要将其传递给更高层的函数来处理。这时,可以通过 throw
关键字重新抛出异常。
catch (...) {
// 做一些处理,例如释放资源
throw; // 重新抛出异常
}
三、C++中的异常安全
异常处理虽然强大,但在C++中引入了新的风险,特别是资源泄漏问题。异常抛出后,如果没有正确释放资源(如内存、文件句柄等),可能导致程序资源泄漏。
1. 构造函数与析构函数的异常
- 构造函数:如果构造函数抛出异常,可能导致对象没有被完全构造,进而出现内存泄漏等问题。
- 析构函数:最好不要在析构函数中抛出异常,析构函数的主要职责是清理资源,如果抛出异常,可能导致资源无法被正确释放。
2. RAII(资源获取即初始化)
C++使用RAII技术来确保资源在异常发生时也能被正确释放。通过智能指针等工具,程序员可以确保资源在超出作用域时自动被释放,避免了内存泄漏问题。
std::unique_ptr<int[]> array(new int[10]);
3.使用
3.异常的使用
我们先看看异常怎么用的,再说其他细节!
下面这段代码出现除0错误程序就会终止。但我不想让程序终止,因此当被除数为0就抛一个异常,很简单就是用throw后面加一个对象,可以是任意类型对象如int、string等等。后面catch对异常进行捕获。
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;
}
return 0;
}
测试:
逐语句调试,感受一下捕获的过程:
四、C++异常体系的优缺点
优点:
- 清晰的错误信息:通过异常对象,程序员可以详细描述错误信息,便于调试。
- 简化代码逻辑:相比返回错误码的方式,异常处理可以自动沿调用链传播错误,减少显式的错误处理逻辑。
- 支持多态:通过抛出派生类异常,捕获基类异常,实现统一的异常处理。
缺点:
- 程序执行流混乱:异常使得程序的执行流不再线性,导致调试困难。
- 性能开销:尽管现代硬件的处理速度很快,但异常的捕获与栈展开仍有一定的性能影响。
- 容易导致资源泄漏:没有正确处理异常时,资源(如内存、文件句柄)可能无法被释放,导致泄漏。
五、实际应用
1.自定义异常体系
- 实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异常,那么外层的调用者基本就没办法玩了
- 所以实际中都会定义一套继承的规范体系。 这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了
下面代码展示了一个服务器开发中常用的异常继承体系,并模拟了数据库、缓存、HTTP服务器中的错误处理方式。通过继承 Exception
类,定义了不同类型的异常类,模拟了抛出和捕获异常的过程。让我们逐步解释代码中的各个部分。
⭕1. Exception
基类
class Exception
{
public:
Exception(const string &errmsg, int id)
: _errmsg(errmsg), _id(id)
{}
virtual string what() const//捕获后的处理
{
return _errmsg;
}
protected:
string _errmsg; // 错误信息
int _id; // 错误编号
};
Exception
是一个基类,代表通用的异常。它有两个成员变量:
-
_errmsg
:表示错误信息。_id
:表示错误的编号(可以用作错误分类或错误码)。
what()
函数是一个虚函数,用于返回错误信息。子类可以重写这个方法,提供更多的上下文信息。
2. SqlException
子类
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; // SQL查询语句
};
SqlException
是Exception
的派生类,专门用于处理数据库相关的异常。- 它有一个额外的成员变量
_sql
,用于存储触发异常的 SQL 查询语句。 - 它重写了
what()
方法,提供了更详细的错误信息,包含了 SQL 查询语句,用于调试和定位问题。
3. CacheException
子类
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;
}
};
CacheException
也是从Exception
继承而来,表示缓存相关的异常。- 它没有额外的成员变量,只重写了
what()
方法,返回了缓存错误的标识CacheException:
和相应的错误信息。
4. HttpServerException
子类
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; // HTTP请求类型 (如 "GET", "POST")
};
HttpServerException
是另一个从Exception
派生的类,处理 HTTP 服务器相关的异常。- 它多了一个
_type
成员变量,表示 HTTP 请求的类型(如GET
或POST
)。 - 它同样重写了
what()
方法,返回更具体的 HTTP 相关错误信息。
5. 模拟抛出异常的函数
SQLMgr()
void SQLMgr()
{
srand(time(0));
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
}
SQLMgr
模拟了数据库操作,在某些情况下会抛出SqlException
,表示数据库权限不足的异常,并附带了 SQL 查询语句。
CacheMgr()
void CacheMgr()
{
srand(time(0));
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
throw CacheException("数据不存在", 101);
}
SQLMgr();
}
CacheMgr
模拟了缓存管理操作,它可能会抛出两种不同的CacheException
:权限不足或数据不存在。- 如果缓存没有抛出异常,它还会调用
SQLMgr
,这可能会进一步抛出SqlException
。
HttpServer()
void HttpServer()
{
srand(time(0));
if (rand() % 3 == 0)
{
throw HttpServerException("请求资源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
throw HttpServerException("权限不足", 101, "post");
}
CacheMgr();
}
HttpServer
模拟了 HTTP 请求处理,可能抛出HttpServerException
,表示请求资源不存在或权限不足。- 如果没有发生 HTTP 异常,它会调用
CacheMgr
,因此可能抛出缓存或数据库相关的异常。
6. 异常处理的主函数 main()
int main()
{
while (1)
{
try
{
HttpServer(); // 可能抛出多种异常
}
catch (const Exception& e) // 捕获基类的异常
{
cout << e.what() << endl; // 多态调用,输出具体的异常信息
}
catch (...)
{
cout << "Unkown Exception" << endl; // 捕获所有未明确处理的异常
}
}
return 0;
}
- 在
main()
函数中,程序通过try-catch
块捕获所有从HttpServer()
抛出的异常。 - 使用基类
Exception
的引用捕获所有派生类异常,并通过多态机制调用派生类的what()
方法输出具体的错误信息。 catch(...)
捕获所有没有明确类型的异常,确保即使抛出了未知类型的异常,程序也不会崩溃。
7. 程序行为
在每次循环中,程序随机抛出不同的异常,如:
- SQL权限不足 (
SqlException
) - 缓存数据不存在 (
CacheException
) - HTTP请求资源不存在 (
HttpServerException
)
这些异常会被捕获,并根据异常类型输出相应的错误信息。程序不会因为异常而崩溃,因为有全面的异常捕获机制。
运行:
会随机捕获异常~
⭕ 总结
- 继承与多态:派生类如
SqlException
、CacheException
、HttpServerException
通过继承Exception
基类实现了多态。即在捕获基类异常时,能够正确识别并调用派生类的what()
方法。 - 异常处理结构化:通过不同的异常类型,开发者可以根据问题的类别和严重程度灵活处理不同模块的错误,比如数据库、缓存和HTTP服务器。
- 防御性编程:通过
catch(...)
捕获未识别的异常,确保程序不会因未捕获的异常导致崩溃,从而提高了程序的稳定性。
这是一个常见的异常处理体系,在服务器开发和大型系统中尤为重要。
2.C++标准库的异常体系
C++提供了一系列标准的异常,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的
说明:
- 实际中我们可以去继承exception类实现自己的异常类
- 但是实际中很多公司像上面一 样自己定义一套异常继承体系。因为C++标准库设计的不够好用
六. 总结
无论是C语言的返回错误码还是C++的异常机制,错误处理都是程序开发中的重要组成部分。C语言适合处理简单错误,而C++的异常处理则为复杂项目提供了更多的灵活性。合理使用这两种语言的错误处理方式,可以提高程序的健壮性和可维护性。
- C语言中,优先使用返回错误码:对于简单的错误,C语言通过返回值来处理是合理的。需要注意检查每个函数调用的返回值,确保及时处理错误。
- C++中,建议使用异常处理:对于复杂的应用,C++的异常机制更为合适,尤其是在处理构造函数等无法返回错误码的场景下。
- 遵循异常安全原则:在异常可能引发的资源管理问题上,使用RAII(如智能指针)来确保资源的正确释放。
- 自定义异常体系:在大型项目中,建议自定义异常类,并通过继承实现不同模块的异常管理。捕获基类异常可以简化代码,并提高异常处理的灵活性。