异常处理
异常处理
异常处理的前情提要:很多人不喜欢使用异常处理,认为它麻烦,应对可能出现的错误要写那么多代码,会非常麻烦。 但实际上不是这样的,我们只需要在一些开发人员难以控制,比较容易出错的地方对异常进行处理就可以了,需要进行异常处理的地方并不多。 举几个例子。
(1)接收传递过来的被除数,我们难以判断被除数是否为0,此时异常处理就很有意义了。
int divide(int divisior, int dividend) {
if(!dividend) {
throw std::string("dividend is zero");
}
return divisior / dividend;
}
void clientInputNum(const std::string& str, int& num) {
std::cout << str << std::endl;
while(std::cin >num, !std::cin.eof()) {
if(std::cin.bad()) {
throw std::runtime_error("cin is corrupted");
}
if(std::cin.fail()) {
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout << "number format error, please input again" << std::endl;
continue;
}
break;
}
}
int main() {
int divisior = 0;
int dividend = 0;
clientInputNum("please input divisior", divisior);
clientInputNum("please input dividend", dividend);
try {
std::cout << divide(divisior, dividend) << std::endl;
}catch(const std::string& str) {
std::cout << "exception occers, exception is : " << str << std::endl;
}catch(...) {
std::cout << "exception occers, exception is : unknown exception" << std::endl;
}
return 0;
}(2)接收文件名,如果文件不存在,我们可以按照之前的写法要求重发一遍,也可以直接报异常,异常就是文件不存在。
void outputFileContent(const std::string& fileName) {
std::ifstream ifs(fileName);
std::string fileLineContent;
if(ifs.open()) {
while (ifs >> fileLineContent) {
std::cout << fileLineContent << std::endl;
}
if(ifs.bad()) {
throw std::runtime_error("ifs is corrupted");
}
ifs.close();
}else {
if(!ifs.bad()) {
throw std::runtime_error("ifs is corrupted");
}
if(ifs.fail()) {
throw std::string("file not exist");
}
}
}
int main() {
std::string str;
std::cin >> string;
try {
outPutFileContent(str);
}catch(const std::string& str) {
std::cout << "exception occers, exception is : " << str << std::endl;
}catch(...) {
std::cout << "exception occers, exception is : unknown exception" << std::endl;
}
}(3)我们在动态分配内存时,经常出现内存不足的情况(在大型程序中,这是非常常见)。比如我们需要动态分配一个未知大小的数组,数组大小等待传入。使用new操作符会直接抛出bad_alloc的异常。 对new的处理非常重要,大家如果做专业的C++开发,会经常用到。 此外使用智能指针时如果内存分配不够也会抛出bad_alloc的异常
int main() {
try
{
while(1) {
int* pi = new int[100000]();
}
}
catch(const std::bad_alloc& except) {
std::cout << except.what() << std::endl;
}
catch(...) {
//std::cout << "unknown exception" << std::endl;
throw;
}
// 不过通常建议不要捕获自己的代码所无法处理的异常
// 否则只是悄悄藏起了这个异常 可能会导致更严重的后果
}#include <memory>
#include <vector>
int main() {
std::vector<std::shared_ptr<int>> shared_int_vec;
try{
while(1) {
std::unique_ptr<int> uniqueI(new int[1000000]());
shared_int_vec.push_back(std::move(uniqueI));
}
}catch(const std::bad_alloc& except) {
std::cout << except.what() << std::endl;
}catch(...) {
std::cout << "unknown exception" << std::endl;
}
}(4)有个vector,我们需要接受一个参数,然后取出参数对应的数组元素。此时就经常出现数组的越界问题。
int main() {
std::vector<int> ivec{1,2,3,4};
try{
ivec.at(10);
}catch(const std::out_of_range& except) {
std::cout << except.what() << std::endl;
}catch(...) {
std::cout << "unknown exception" << std::endl;
}
}#include <vector>
template<typaname T>
class Test{
public:
T& operator[](unsigned count)const {
if(count >= data.size()) {
throw std::out_of_range("Test data is out of range");
}
}
private:
std::vector<T> data;
}最常用的基本就这些例子了,剩下的也都和这些类似。 异常处理这一章东西不多,一会儿把这些例子演示一下就可以了。
异常处理的介绍:
异常是程序在执行期间产生的问题(编译期出现的错误在写代码时开发环境就有提示)。C++的异常是指程序运行时发生的特殊情况。
异常提供了一种转移程序控制权的方式。C++的异常处理涉及到三个关键字:try,catch,throw。 (1) throw:当问题出现时,程序会抛出一个异常。这是通过throw关键字来完成的。 (2) catch:在你想要处理问题的地方,通过异常处理程序捕获异常。catch关键字用于捕获异常。 (3) try:try块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个catch块。
如果有一个块抛出一个异常,捕获异常的方法会使用try和catch关键字。try块中放可能抛出异常的代码,try块中的代码被称为保护代码。常见的异常处理格式如下所示。
try {
//保护代码
}catch( ExceptionName e1) {
//catch块
}catch( ExceptionName e2) {
//catch块
}catch( ExceptionName eN) {
//catch块
}抛出异常
throw语句可以在代码块的任何地方抛出异常,throw抛出的表达式的结果决定了抛出的异常的类型。
标准异常
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 | 当发生数学下溢时,会抛出该异常。 |
常用异常
别看图很复杂,异常种类有很多,但经常使用的其实就几个。 (1) bad_alloc错误,使用new分配内存失败就会抛出bad_alloc错误。 (2) out_of_range错误,在使用at时,容器越界就会抛出这个错误,这也是at比[]更加优秀的原因。
(3) runtime_error错误,运行时错误,只有在程序运行时才能检测到的错误。这是一个相对的概念,和logic_error形成对比。logic_error可以读代码读出来,runtime_error就不行。 我们也经常将一些读代码无法判断的异常标识为runtime_error。
(4) ... 错误,可以接受任何错误,我们一般都会在catch最后加上...,这样就可以接受所有类型的异常了。
自定义异常类型
其实需要自定义异常类型的情况真的非常少
class my_exception : public exception {
private:
unsigned int error_code;
std::string reason;
public:
my_exception(const string& message, const int& code):
reason(message),error_code(code) {
return reason.c_str();
};
virtual const char* what() const noexcept override {
return reason.c_str();
};
}
int main() {
try {
throw my_exception("custom exception", 1000);
}
catch (const my_exception& e) {
std::cout<<e.what()<<std::endl;
}
return 0;
}资源释放问题 RAII
在异常处理过程中的资源释放问题.
对于自动创建的对象 例如我们在函数中定义的类对象 在程序抛出异常后 他们都能被自动释放 例如这段代码中创建的MyResource对象
class MyResource {
public:
MyResource(const char* name) : mName(name) {
std::cout<<"MyResource Construct"<<mName<<std::endl;
}
virtual ~MyResource() {
std::cout<<"MyResource Destruct"<<mName<<std::endl;
}
};
void doSomething() {
MyResource res("[doSomething]");
std::cout<<"doSomething()"<<std::endl;
throw runtime_error("do something error");
}
int main() {
try {
MyResource res("[main]");
doSomething();
}
catch(const runtime_error& e) {
std::cout<<e.what()<<std::endl;
}
}但是如果是动态分配的对象 则需要采取措施。实际情况比较复杂。
例如下面这段代码里doSomething()方法里调用了几个其他的方法。这几个方法都有可能抛出异常。 当他们任何一个方法抛出异常后, delete语句就无法执行了, 就会造成内存泄漏。
因此我们经常使用类对这些资源进行封装。当自动创建的类对象被自动释放时会调用类的析构函数, 在析构函数中可以释放动态调用的内存或关闭打开的文件
float doSomething() {
MyResource* pRes = new MyResource("[doSomething]");
functionA(pRes);
functionB(pRes);
float result = functionC(pRes);
delete pRes;
return result;
}对于动态分配的内存 我们可以使用智能指针进行封装。例如像这样
float doSomething() {
std::shared_ptr<MyResource> pRes(new MyResource("[doSomething]"));
functionA(pRes);
functionB(pRes);
float result = functionC(pRes);
delete pRes;
return result;
}在任何一个地方抛出异常后, pRes会被自动释放, 从而释放他所封装的指针。
像上述这种管理资源的方式叫做
