第23章 异常处理 异常处理的基本概念 异常是程序执行过程中发生的错误或特殊情况,例如除以零、数组越界、内存分配失败等。C++的异常处理机制允许我们捕获和处理这些异常,使程序能够更加健壮和可靠。
异常处理的三个关键字 try :定义可能抛出异常的代码块catch :捕获并处理异常throw :抛出异常异常处理的基本语法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> #include <string> int divide (int numerator, int denominator) { if (denominator == 0 ) { throw std::string ("Division by zero" ); } return numerator / denominator; } int main () { try { int result = divide (10 , 0 ); std::cout << "Result: " << result << std::endl; } catch (const std::string& error) { std::cout << "Error: " << error << std::endl; } std::cout << "Program continues..." << std::endl; return 0 ; }
异常的抛出和捕获 抛出异常 异常可以是任何类型的对象,包括内置类型和自定义类型。通常,我们使用标准库提供的异常类或自定义异常类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <iostream> #include <stdexcept> void checkAge (int age) { if (age < 0 ) { throw std::invalid_argument ("Age cannot be negative" ); } if (age > 150 ) { throw std::out_of_range ("Age is too large" ); } std::cout << "Valid age: " << age << std::endl; } int main () { try { checkAge (-5 ); } catch (const std::invalid_argument& e) { std::cout << "Invalid argument: " << e.what () << std::endl; } catch (const std::out_of_range& e) { std::cout << "Out of range: " << e.what () << std::endl; } try { checkAge (200 ); } catch (const std::exception& e) { std::cout << "Error: " << e.what () << std::endl; } return 0 ; }
捕获异常 异常捕获的顺序很重要,应该先捕获具体的异常,再捕获通用的异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> #include <stdexcept> void process (int value) { if (value == 0 ) { throw std::runtime_error ("Value cannot be zero" ); } if (value < 0 ) { throw std::logic_error ("Value cannot be negative" ); } std::cout << "Processing value: " << value << std::endl; } int main () { try { process (-5 ); } catch (const std::logic_error& e) { std::cout << "Logic error: " << e.what () << std::endl; } catch (const std::runtime_error& e) { std::cout << "Runtime error: " << e.what () << std::endl; } catch (const std::exception& e) { std::cout << "General exception: " << e.what () << std::endl; } catch (...) { std::cout << "Unknown exception" << std::endl; } return 0 ; }
标准异常类 C++标准库提供了一系列异常类,它们都派生自std::exception类。
标准异常类层次结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 std::exception ├── std::bad_alloc ├── std::bad_cast ├── std::bad_exception ├── std::bad_function_call ├── std::bad_typeid ├── std::bad_weak_ptr ├── std::logic_error │ ├── std::domain_error │ ├── std::invalid_argument │ ├── std::length_error │ ├── std::out_of_range ├── std::runtime_error │ ├── std::range_error │ ├── std::overflow_error │ ├── std::underflow_error │ ├── std::regex_error ├── std::future_error ├── std::ios_base::failure ├── std::system_error
常用标准异常类 std::bad_alloc :内存分配失败时抛出std::bad_cast :类型转换失败时抛出std::invalid_argument :无效参数时抛出std::out_of_range :超出范围时抛出std::length_error :长度错误时抛出std::logic_error :逻辑错误时抛出std::runtime_error :运行时错误时抛出std::overflow_error :溢出错误时抛出std::underflow_error :下溢错误时抛出1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <iostream> #include <stdexcept> #include <vector> int main () { try { while (true ) { new int [1000000000 ]; } } catch (const std::bad_alloc& e) { std::cout << "bad_alloc: " << e.what () << std::endl; } try { std::vector<int > v (5 ) ; std::cout << v.at (10 ) << std::endl; } catch (const std::out_of_range& e) { std::cout << "out_of_range: " << e.what () << std::endl; } try { std::string s = "abc" ; std::stoi (s); } catch (const std::invalid_argument& e) { std::cout << "invalid_argument: " << e.what () << std::endl; } return 0 ; }
自定义异常类 我们可以通过继承std::exception类或其派生类来创建自定义异常类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <iostream> #include <stdexcept> #include <string> class NetworkException : public std::runtime_error {private : int errorCode; public : NetworkException (const std::string& message, int code) : std::runtime_error (message), errorCode (code) {} int getErrorCode () const { return errorCode; } }; class DatabaseException : public std::runtime_error {private : std::string query; public : DatabaseException (const std::string& message, const std::string& q) : std::runtime_error (message), query (q) {} const std::string& getQuery () const { return query; } }; void connectToServer (const std::string& server) { if (server.empty ()) { throw NetworkException ("Server name cannot be empty" , 404 ); } std::cout << "Connected to " << server << std::endl; } void executeQuery (const std::string& query) { if (query.empty ()) { throw DatabaseException ("Query cannot be empty" , query); } std::cout << "Executing query: " << query << std::endl; } int main () { try { connectToServer ("" ); } catch (const NetworkException& e) { std::cout << "Network error: " << e.what () << " (Code: " << e.getErrorCode () << ")" << std::endl; } try { executeQuery ("" ); } catch (const DatabaseException& e) { std::cout << "Database error: " << e.what () << " (Query: '" << e.getQuery () << "')" << std::endl; } return 0 ; }
异常处理的最佳实践 1. 只在特殊情况下使用异常 异常应该用于处理特殊情况,而不是常规的控制流。例如,网络连接失败、文件不存在等是特殊情况,应该使用异常处理;而用户输入验证等常规操作,应该使用返回值或其他方式处理。
2. 抛出有意义的异常 异常应该包含足够的信息,以便于调试和处理。使用标准异常类或自定义异常类,提供清晰的错误消息。
3. 捕获具体的异常 应该先捕获具体的异常,再捕获通用的异常。这样可以根据不同的异常类型采取不同的处理措施。
4. 不要捕获所有异常 避免使用catch (...)捕获所有异常,除非你确实需要处理所有可能的异常情况。捕获所有异常可能会掩盖真正的问题,使调试变得困难。
5. 释放资源 在异常处理中,确保释放所有已分配的资源。可以使用RAII(资源获取即初始化)技术,通过对象的构造和析构来管理资源。
6. 异常规格说明(已废弃) C++11之前,我们可以使用异常规格说明来指定函数可能抛出的异常类型。但在C++11中,异常规格说明已被废弃,推荐使用noexcept说明符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <iostream> #include <stdexcept> void func () throw (std::runtime_error) { throw std::runtime_error ("Error" ); } void func2 () noexcept { } void func3 () noexcept (false ) { } int main () { try { func (); } catch (const std::runtime_error& e) { std::cout << "Error: " << e.what () << std::endl; } return 0 ; }
7. 使用RAII管理资源 RAII(Resource Acquisition Is Initialization)是一种资源管理技术,通过对象的构造和析构来管理资源。当对象创建时获取资源,当对象销毁时释放资源,无论是否发生异常,都能保证资源被正确释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <iostream> #include <stdexcept> #include <fstream> class FileHandler {private : std::ifstream file; public : FileHandler (const std::string& filename) : file (filename) { if (!file) { throw std::runtime_error ("Failed to open file" ); } std::cout << "File opened successfully" << std::endl; } ~FileHandler () { if (file.is_open ()) { file.close (); std::cout << "File closed" << std::endl; } } std::string readLine () { std::string line; std::getline (file, line); return line; } bool eof () { return file.eof (); } }; void processFile (const std::string& filename) { FileHandler handler (filename) ; while (!handler.eof ()) { std::string line = handler.readLine (); std::cout << "Line: " << line << std::endl; if (line == "error" ) { throw std::runtime_error ("Error found in file" ); } } } int main () { try { processFile ("example.txt" ); } catch (const std::runtime_error& e) { std::cout << "Error: " << e.what () << std::endl; } std::cout << "Program continues..." << std::endl; return 0 ; }
异常处理与构造函数和析构函数 构造函数中的异常 如果构造函数抛出异常,对象的析构函数不会被调用,因为对象没有完全构造。因此,在构造函数中,我们需要确保已经分配的资源被正确释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <iostream> #include <stdexcept> class Resource {private : int * data; bool initialized; public : Resource (int size) : initialized (false ) { data = new int [size]; if (size > 1000 ) { delete [] data; throw std::runtime_error ("Size too large" ); } initialized = true ; std::cout << "Resource constructed" << std::endl; } ~Resource () { if (initialized) { delete [] data; std::cout << "Resource destructed" << std::endl; } } }; int main () { try { Resource r1 (100 ) ; Resource r2 (2000 ) ; } catch (const std::runtime_error& e) { std::cout << "Error: " << e.what () << std::endl; } return 0 ; }
析构函数中的异常 析构函数不应该抛出异常,因为如果析构函数在处理另一个异常时抛出异常,程序会终止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <iostream> class BadResource {private : int * data; public : BadResource () : data (new int [100 ]) { std::cout << "BadResource constructed" << std::endl; } ~BadResource () { std::cout << "BadResource destructor called" << std::endl; delete [] data; throw std::runtime_error ("Error in destructor" ); } }; int main () { try { BadResource r; throw std::runtime_error ("Error in main" ); } catch (const std::runtime_error& e) { std::cout << "Caught exception: " << e.what () << std::endl; } return 0 ; }
异常处理的性能考虑 异常处理会带来一定的性能开销,主要包括:
异常表 :编译器会为每个函数生成异常表,记录可能抛出异常的位置和对应的处理代码栈展开 :当异常抛出时,需要沿着调用栈向上查找匹配的catch块,这一过程称为栈展开异常对象的创建和销毁 :异常对象需要在堆上分配内存,使用完毕后需要销毁减少异常处理的性能开销 只在特殊情况下使用异常 :不要将异常用于常规的控制流使用 noexcept 说明符 :对于不会抛出异常的函数,使用 noexcept 说明符,这样编译器可以进行优化避免在异常处理中执行复杂操作 :异常处理代码应该简洁,只处理必要的逻辑使用移动语义 :对于自定义异常类,实现移动构造函数和移动赋值运算符,减少异常对象的复制开销异常处理的应用场景 1. 资源管理 使用异常处理来管理资源的分配和释放,确保资源在发生错误时能够被正确释放。
2. 错误恢复 在某些情况下,程序可以从异常中恢复,继续执行。例如,网络连接失败时,可以尝试重新连接。
3. 错误报告 异常可以携带详细的错误信息,帮助开发者和用户了解错误的原因。
4. 分层错误处理 在大型应用程序中,可以在不同的层次处理不同类型的异常,提高代码的可维护性。
总结 异常处理是C++中一种重要的错误处理机制,它允许我们:
分离错误处理代码 :将正常的业务逻辑与错误处理代码分离,使代码更加清晰提供详细的错误信息 :通过异常对象携带详细的错误信息,便于调试和处理确保资源释放 :结合RAII技术,确保资源在发生异常时能够被正确释放实现错误传播 :异常可以沿着调用栈向上传播,直到找到匹配的catch块提高代码的健壮性 :通过捕获和处理异常,使程序能够更加健壮地运行然而,异常处理也有一些缺点:
性能开销 :异常处理会带来一定的性能开销代码复杂性 :过度使用异常会增加代码的复杂性调试困难 :异常的传播路径可能比较复杂,增加调试的难度因此,我们应该合理使用异常处理,在需要的地方使用,避免滥用。通过遵循异常处理的最佳实践,我们可以充分发挥异常处理的优势,编写更加健壮和可靠的C++程序。
异常处理是C++的重要特性之一,也是成为优秀C++程序员的必备知识。通过不断学习和实践,你会逐渐掌握异常处理的使用技巧,并能够在实际项目中灵活应用。