目录
1.引言
2.异常处理(Exception Handling)
3.错误码(Error Codes)
4.断言(Assertions)
5.日志记录
6.条件检查和防御性编程
7.实际运用
8.总结
1.引言
不管是谁,都不能保证自己写的程序没有bug。那么永远都抛不开的一个话题就是,离不开错误处理,尤其是生产型大型软件系统。应用软件系统运行属于循环处理事务,出错后需要保证不能让软件程序直接退出。这就需要使用一定的程序容错处理来应对。一般情况下,大型软件开发中的软件系统容错处理会结合异常处理、错误代码定义的使用与相应的出错处理日志记录,包括一定的参与大型生产系统的监控系统等配合保障系统的稳定性。C++也提供了多种错误处理机制,这些机制可以帮助开发者检测和响应程序运行中的各种错误情况。下面本章将会就C++软件系统中提供的异常处理作详细的讲述,包括基本概念以及应用操作情况。
2.异常处理(Exception Handling)
软件应用程序中,异常处理机制是一种比较有效的处理系统运行时错误的方法。C++针对异常处理提供了一种标准的方法,用于处理软件程序运行时的错误,并用于处理软件系统中可预知或不可预知的问题。这样就可以保证软件系统运行的稳定性与健壮性。
C++中异常的处理主要用于针对程序在运行时刻出现错误而提供的语言层面的保护机制。它允许开发者最大限度地发挥,针对异常处理进行相应的修改调整。
C++应用程序中在考虑异常设计时,并不是所有的程序模块处理都需要附加上异常情况的处理。相对来说,异常处理没有普通方法函数调用速度快。过度的错误处理会影响应用程序运行的效率。通常在C++开发的软件系统中,应用程序都由对应的库、组件以及运行的具体不同模块组成。在设计时,异常的处理应充分考虑到独立程序库以及组件之间的情况。便于使用者在程序出现异常情况下,使用库或者组件的开发者能够快速定位出库、组件还是应用程序的错误。
C++ 支持通过异常处理机制来捕获和处理错误。主要使用的关键字包括 try
、throw
和 catch
。
- try 块:用
try
关键字标识一个可能抛出异常的代码块。 - throw 语句:用于抛出一个异常。可以抛出任何类型的对象,但通常是派生自
std::exception
的对象。 - catch 块:用于捕获并处理异常。
catch
块后面跟随的是异常类型和变量名,该变量存储了被捕获的异常对象。
#include <iostream>
#include <stdexcept>void func() {throw std::runtime_error("Something go wrong!");
}int main() {try {func();} catch (const std::runtime_error& e) {std::cerr << "Caught a runtime_error: " << e.what() << std::endl;} catch (...) {std::cerr << "Caught an unknown exception" << std::endl;}return 0;
}
自定义异常类
#pragma once
#include <stdexcept>class CJFixFormatException : public std::exception
{
public:using _Mybase = std::exception;explicit CJFixFormatException(const std::string& _Message) : _Mybase(_Message.c_str()) {}explicit CJFixFormatException(const char* _Message) : _Mybase(_Message) {}
};void func1() {throw CJFixFormatException("recv data less than J format template size");
}int main() {try {func1();} catch (const CJFixFormatException& e) {std::cerr << "Caught custom exception: " << e.what() << std::endl;}return 0;
}
异常的抛出和匹配原则
1.异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
2.被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
3.抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象, 所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
4.catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。
5.实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象, 使用基类捕获。
在函数调用链中异常栈展开匹配原则
1.首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。
2.没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
3.如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的 catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异 常,否则当有异常没捕获,程序就会直接终止。
4.找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
3.错误码(Error Codes)
使用返回码是另一种常见的错误处理方法,特别是在异常开销不合适的情况下。通过函数返回值指示错误,并在调用处检查这些返回值。错误码通常通过返回整数或枚举类型来表示不同的错误情况。
示例如下:
#include <iostream>enum ErrorCode {Success = 0,FileNotFound,PermissionDenied,UnknownError
};ErrorCode openFile(const char* filename) {// Simulated file opening logicif (filename == nullptr) return FileNotFound;// More conditions could be checked herereturn Success;
}int main() {ErrorCode code = openFile(nullptr);if (code != Success) {std::cerr << "Error: " << code << std::endl;} else {std::cout << "File opened successfully" << std::endl;}return 0;
}
注意:错误码有一个问题,它只能定义可知的或者说可预知的。而对一些动态的、随机产生的异常或错误,它是无法正确返回的。或者说,当有一种错误码有N种情形可以产生时,就不好确定是哪种错误了。错误码一般不会有性能上的考虑,毕竟只是返回值的处理。使用错误码一定要有一个良好的习惯,对所有的错误码有一个非常好的注释或者有专门的说明文档,并且需要定时维护和更新。只有这样错误码才会起到应有的作用。
4.断言(Assertions)
C++之assert惯用法_c++ assert-CSDN博客
断言用于在调试阶段捕捉不可恢复的错误。它在条件为假时终止程序执行并打印错误信息。适用于开发阶段的调试。
示例如下:
#include <cassert>int divide(int a, int b) {assert(b != 0 && "Division by zero!");return a / b;
}int main() {int result = divide(10, 2);std::cout << "Result: " << result << std::endl;result = divide(10, 0); // This will trigger the assertion and terminate the programstd::cout << "Result: " << result << std::endl; // This line will not be executedreturn 0;
}
5.日志记录
spdlog一个非常好用的C++日志库(一): 简介与使用-CSDN博客
日志记录是一种监控和记录错误发生情况的方法,常用于生产环境中进行故障排查。通过日志记录库(如 sdplog、log4cpp
、Boost.Log
或标准库中的 <fstream>
)记录程序的运行状态和错误信息,有助于调试和追踪问题。
示例如下:
#include <iostream>
#include <fstream>
#include <stdexcept>void logError(const std::string& message) {std::ofstream logFile("error.log", std::ios_base::app);logFile << message << std::endl;
}void functionThatMightFail() {throw std::runtime_error("An error occurred");
}int main() {try {functionThatMightFail();} catch (const std::exception& e) {logError(e.what());std::cerr << "Caught exception: " << e.what() << std::endl;}return 0;
}
6.条件检查和防御性编程
浅谈C++中的防御性编程_c++防御性编程-CSDN博客
通过条件检查在运行时防止错误发生,例如参数验证。
示例如下:
#include <iostream>void processInput(int input) {if (input < 0) {std::cerr << "Error: input must be non-negative" << std::endl;return;}// 处理输入
}int main() {processInput(-1);processInput(10);return 0;
}
7.实际运用
知晓了错误码与异常的控制后,如何进行选择呢?其实非常容易。那就是根据实际情况,能直接控制的,尽量选择错误码(包括在C++中使用枚举)。而对于一些影响到程序运行的稳定性和安全性的能随时可能产生的一般建议选择使用异常控制。
在实际的开发过程中,为什么经常禁止使用异常。这里面有一个重要的原因,除了性能的问题外,一般来说,在C/C++中的严重的异常问题(包括动态生产的等),比如内存问题、指针问题等往往程序就直接Crash了。这时,再好的异常抛出来也没有什么用了。
所以,异常的处理往往就这样尴尬了。但这并不代表异常没有用武之地,在一些明确有异常抛出的相关库的API中异常还是很有用处的。或者开发者自己封装相关的错误当做异常抛出,在某些情况下也会起到很好的控制作用。
从目前实际看到的开源代码来看,异常更倾向在业务逻辑的上层应用,中下层一般以返回错误码居多。所以这也可以是开发者如何使用二者的一个大方向上的推荐,在底层,特别是接近于硬件的底层,建议使用错误码;在上层业务或UI上可以考虑使用异常,但个人建议要严格限制异常的使用。
掌握这些大原则后,在实际应用中再根据实际情况,适当的进行个别的处理即可。还是那句话,合适的就是最好的。
8.总结
1.异常处理:用于捕获和处理运行时错误,适合处理不可预见的异常情况,而且性能开销较大。
2.错误返回码:适用于函数调用链中明确的错误检查,常用于嵌入式系统和性能敏感的代码,性能开销小。
3.断言:用于开发阶段捕捉编程错误,发布版本通常禁用。
4.日志记录:用于记录和追踪错误,帮助调试和维护。
5.条件检查和防御性编程:通过提前检查条件防止错误发生。
可以根据具体情况选择最合适的方法来确保程序的健壮性和可靠性。异常处理机制是现代 C++ 推荐的错误处理方式,而断言和错误码在特定情况下也非常有用。