C++ 异常处理机制详解
C 语言与 C++ 错误处理方式的对比及应用
在编程中,错误处理是不可避免的。传统的 C 语言和现代的 C++ 在处理错误上有着明显的区别,前者依赖返回错误码的方式,而后者则引入了更为灵活的异常机制。
C++ 异常处理机制通过 throw 和 catch 实现错误传递,相比 C 语言返回错误码更灵活。文章对比了两种语言的错误处理方式,介绍了异常的抛出、捕获及重新抛出机制。重点讲解了异常安全性,包括构造函数与析构函数的异常风险,以及利用 RAII 技术管理资源。此外,还展示了如何构建自定义异常继承体系,结合多态实现统一的错误处理,并分析了异常机制的优缺点及适用场景。

在编程中,错误处理是不可避免的。传统的 C 语言和现代的 C++ 在处理错误上有着明显的区别,前者依赖返回错误码的方式,而后者则引入了更为灵活的异常机制。
assertC 语言中最简单的错误处理方式之一是直接终止程序,例如使用 assert。当程序在运行时遇到不可恢复的错误(如除零、内存访问越界等),程序会被强制终止。这种方式虽然简单,但有明显的缺陷:
另一种常用的处理方式是返回错误码。很多 C 语言的库都会通过返回一个整数值来表示函数的执行结果。具体的错误信息通常会被存储在全局变量 errno 中,程序员可以通过查阅 errno 的值来判断出错的具体原因。
示例代码:
int func() {
if (/* 错误发生 */) {
errno = EINVAL; // 设置错误码
return -1; // 返回错误
}
return 0; // 返回成功
}
errno 做出相应的处理。这增加了代码的复杂性和出错的可能性。例如:
int ConnectSql() {
// 用户名密码错误
if (...) return 1;
// 权限不足
if (...) return 2;
}
int ServerStart() {
if (int ret = ConnectSql() < 0) return ret;
int fd = socket();
if (fd < 0) return errno;
}
int main() {
if (ServerStart() < 0) ...
return 0;
}
C++ 引入了异常处理机制,使得程序在遇到错误时不需要直接终止或通过返回值处理,可以通过抛出异常的方式将问题传递到合适的地方进行处理。
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;
}

C++ 的异常处理机制基于类型匹配。在抛出异常时,程序会根据异常的类型查找最接近的 catch 块。如果没有匹配的 catch,程序将继续沿调用链向外查找,最终未被捕获的异常会导致程序终止。
catch 块的类型匹配,否则将无法捕获。catch (...) {
// 捕获所有类型的异常,防止程序崩溃
cout << "Unknown exception occurred!" << endl;
}

有时,捕获到异常后,当前函数无法处理该异常,而需要将其传递给更高层的函数来处理。这时,可以通过 throw 关键字重新抛出异常。
catch (...) {
// 做一些处理,例如释放资源
throw; // 重新抛出异常
}
异常处理虽然强大,但在 C++ 中引入了新的风险,特别是资源泄漏问题。异常抛出后,如果没有正确释放资源(如内存、文件句柄等),可能导致程序资源泄漏。
C++ 使用 RAII 技术来确保资源在异常发生时也能被正确释放。通过智能指针等工具,程序员可以确保资源在超出作用域时自动被释放,避免了内存泄漏问题。
std::unique_ptr<int[]> array(new int[10]);
实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异常,那么外层的调用者基本就没办法玩了。所以实际中都会定义一套继承的规范体系。这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了。

下面代码展示了一个服务器开发中常用的异常继承体系,并模拟了数据库、缓存、HTTP 服务器中的错误处理方式。通过继承 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() 函数是一个虚函数,用于返回错误信息。子类可以重写这个方法,提供更多的上下文信息。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 查询语句,用于调试和定位问题。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: 和相应的错误信息。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 相关错误信息。void SQLMgr() {
srand(time(0));
if (rand() % 7 == 0) {
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
}
SQLMgr 模拟了数据库操作,在某些情况下会抛出 SqlException,表示数据库权限不足的异常,并附带了 SQL 查询语句。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。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,表示请求资源不存在或权限不足。CacheMgr,因此可能抛出缓存或数据库相关的异常。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(...) 捕获所有没有明确类型的异常,确保即使抛出了未知类型的异常,程序也不会崩溃。在每次循环中,程序随机抛出不同的异常,如:
SqlException)CacheException)HttpServerException)这些异常会被捕获,并根据异常类型输出相应的错误信息。程序不会因为异常而崩溃,因为有全面的异常捕获机制。

会随机捕获异常~

SqlException、CacheException、HttpServerException 通过继承 Exception 基类实现了多态。即在捕获基类异常时,能够正确识别并调用派生类的 what() 方法。catch(...) 捕获未识别的异常,确保程序不会因未捕获的异常导致崩溃,从而提高了程序的稳定性。这是一个常见的异常处理体系,在服务器开发和大型系统中尤为重要。
C++ 提供了一系列标准的异常,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的。


说明:
无论是 C 语言的返回错误码还是 C++ 的异常机制,错误处理都是程序开发中的重要组成部分。C 语言适合处理简单错误,而 C++ 的异常处理则为复杂项目提供了更多的灵活性。合理使用这两种语言的错误处理方式,可以提高程序的健壮性和可维护性。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online