第25章 异常处理基础 异常处理概述 异常处理是一种处理程序运行时错误的机制,它允许程序在遇到错误时,跳转到专门的错误处理代码,而不是直接崩溃。C++的异常处理机制提供了一种结构化的方式来处理错误,使得错误处理代码与正常业务逻辑分离,提高了代码的可读性和可维护性。
异常处理的基本概念 异常 :程序运行时发生的错误情况,如除零、数组越界、内存不足等抛出异常 :当程序遇到错误时,使用throw关键字抛出异常捕获异常 :使用try-catch块捕获并处理异常异常传播 :如果异常在当前函数中没有被捕获,它会沿着调用栈向上传播,直到被捕获或导致程序终止异常处理的优势 错误处理与业务逻辑分离 :异常处理代码集中在catch块中,使正常业务逻辑更加清晰分层错误处理 :不同层次的代码可以处理不同类型的异常资源自动释放 :结合RAII(资源获取即初始化),确保即使发生异常也能正确释放资源错误信息传递 :异常对象可以携带详细的错误信息强制错误处理 :某些异常必须被处理,否则会导致程序终止异常的基本语法 抛出异常 使用throw关键字抛出异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <iostream> #include <stdexcept> void divide (int a, int b) { if (b == 0 ) { throw std::runtime_error ("Division by zero" ); } std::cout << "Result: " << a / b << std::endl; } int main () { try { divide (10 , 0 ); } catch (const std::exception& e) { std::cout << "Error: " << e.what () << std::endl; } return 0 ; }
捕获异常 使用try-catch块捕获异常:
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 <string> void process (int value) { if (value < 0 ) { throw std::invalid_argument ("Value must be non-negative" ); } else if (value > 100 ) { throw std::out_of_range ("Value must be less than or equal to 100" ); } std::cout << "Processing value: " << value << std::endl; } int main () { try { process (-1 ); } 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; } catch (const std::exception& e) { std::cout << "General error: " << e.what () << std::endl; } try { process (200 ); } catch (const std::exception& e) { std::cout << "Error: " << e.what () << std::endl; } return 0 ; }
异常规格说明 C++11之前,使用异常规格说明(exception specification)来声明函数可能抛出的异常类型:
1 2 3 void func () throw (std::runtime_error, std::logic_error) ; void func () throw () ;
C++11引入了noexcept说明符,替代了异常规格说明:
1 2 3 4 5 6 7 void func () noexcept ; void func () noexcept (false ) ; template <typename T> void swap (T& a, T& b) noexcept (noexcept (std::swap(a, b))) ;
异常安全保证 异常安全是指当异常发生时,程序能够保持一致的状态,不会泄漏资源,也不会破坏数据结构。C++中,异常安全保证通常分为三个级别:
1. 基本异常安全(Basic Exception Safety) 保证 :当异常发生时,程序不会泄漏资源,数据结构保持有效状态,但可能不是原始状态示例 :使用智能指针管理内存,确保即使发生异常也能释放内存1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <memory> #include <vector> void processData (std::vector<int >& data) { std::vector<int > temp; for (int value : data) { temp.push_back (value * 2 ); } data.swap (temp); }
2. 强异常安全(Strong Exception Safety) 保证 :当异常发生时,程序状态完全回滚到操作前的状态,就像操作从未发生过一样示例 :使用复制-交换技术(Copy-and-Swap)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 <algorithm> class ResourceManager {private : int * data; size_t size; public : ResourceManager (size_t s) : size (s), data (new int [s]()) {} ResourceManager (const ResourceManager& other) : size (other.size), data (new int [other.size]) { std::copy (other.data, other.data + other.size, data); } ~ResourceManager () { delete [] data; } ResourceManager& operator =(ResourceManager other) { swap (*this , other); return *this ; } friend void swap (ResourceManager& a, ResourceManager& b) { std::swap (a.data, b.data); std::swap (a.size, b.size); } void resize (size_t newSize) { ResourceManager temp (newSize) ; std::copy (data, data + std::min (size, newSize), temp.data); swap (*this , temp); } };
3. 无异常保证(No-Throw Guarantee) 保证 :操作永远不会抛出异常示例 :使用noexcept说明符标记的函数1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <vector> void swapValues (int & a, int & b) noexcept { int temp = a; a = b; b = temp; } template <typename T>const T& getElement (const std::vector<T>& vec, size_t index) noexcept { return vec[index]; }
异常传播 异常传播机制 当异常在函数中抛出但未被捕获时,它会沿着调用栈向上传播,直到找到匹配的catch块。这个过程称为栈展开(Stack Unwinding)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> #include <stdexcept> void func3 () { throw std::runtime_error ("Error in func3" ); } void func2 () { func3 (); } void func1 () { func2 (); } int main () { try { func1 (); } catch (const std::exception& e) { std::cout << "Caught exception: " << e.what () << std::endl; } return 0 ; }
栈展开过程 当异常发生时,栈展开过程会:
销毁当前函数中已经构造的局部对象 返回到调用函数 重复上述过程,直到找到匹配的catch块 如果没有找到catch块,调用std::terminate()终止程序 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 #include <iostream> class MyClass {public : MyClass (int id) : id (id) { std::cout << "Constructor " << id << std::endl; } ~MyClass () { std::cout << "Destructor " << id << std::endl; } private : int id; }; void func () { MyClass c3 (3 ) ; throw 42 ; MyClass c4 (4 ) ; } void caller () { MyClass c2 (2 ) ; func (); MyClass c5 (5 ) ; } int main () { try { MyClass c1 (1 ) ; caller (); MyClass c6 (6 ) ; } catch (int e) { std::cout << "Caught: " << e << std::endl; } MyClass c7 (7 ) ; return 0 ; }
异常与RAII RAII简介 RAII(Resource Acquisition Is Initialization)是一种C++编程技术,它通过对象的构造和析构来管理资源,确保资源在使用完毕后被正确释放。
RAII与异常安全 RAII是实现异常安全的关键技术,它确保无论是否发生异常,资源都能被正确释放:
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 #include <fstream> #include <stdexcept> class FileGuard {private : std::ofstream& file; public : explicit FileGuard (std::ofstream& f) : file(f) { if (!file.is_open ()) { throw std::runtime_error ("Failed to open file" ); } } ~FileGuard () { file.close (); std::cout << "File closed" << std::endl; } FileGuard (const FileGuard&) = delete ; FileGuard& operator =(const FileGuard&) = delete ; }; void writeToFile (const std::string& filename, const std::string& content) { std::ofstream file (filename) ; FileGuard guard (file) ; throw std::runtime_error ("Error during write" ); file << content << std::endl; } int main () { try { writeToFile ("test.txt" , "Hello, RAII!" ); } catch (const std::exception& e) { std::cout << "Error: " << e.what () << std::endl; } return 0 ; }
智能指针与异常 智能指针是RAII的典型应用,它们确保即使发生异常,动态分配的内存也能被正确释放:
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 #include <memory> #include <stdexcept> void processData () { std::unique_ptr<int > data (new int [1000 ]) ; throw std::runtime_error ("Processing error" ); } void processDataModern () { auto data = std::make_unique <int []>(1000 ); throw std::runtime_error ("Processing error" ); } int main () { try { processData (); } catch (const std::exception& e) { std::cout << "Error: " << e.what () << std::endl; } try { processDataModern (); } catch (const std::exception& e) { std::cout << "Error: " << 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 #include <stdexcept> #include <string> class DatabaseError : public std::runtime_error {public : DatabaseError (const std::string& message, int errorCode) : std::runtime_error (message), errorCode (errorCode) {} int getErrorCode () const { return errorCode; } private : int errorCode; }; class NetworkError : public std::runtime_error {public : NetworkError (const std::string& message, int statusCode) : std::runtime_error (message), statusCode (statusCode) {} int getStatusCode () const { return statusCode; } private : int statusCode; }; void connectToDatabase () { throw DatabaseError ("Failed to connect to database" , 5001 ); } void connectToServer () { throw NetworkError ("Failed to connect to server" , 404 ); } int main () { try { connectToDatabase (); } catch (const DatabaseError& e) { std::cout << "Database error: " << e.what () << ", Code: " << e.getErrorCode () << std::endl; } catch (const std::exception& e) { std::cout << "General error: " << e.what () << std::endl; } try { connectToServer (); } catch (const NetworkError& e) { std::cout << "Network error: " << e.what () << ", Status: " << e.getStatusCode () << std::endl; } catch (const std::exception& e) { std::cout << "General error: " << e.what () << std::endl; } return 0 ; }
异常类的最佳实践 继承自标准异常类 :便于与标准异常处理机制集成提供有意义的错误信息 :在异常对象中存储详细的错误信息实现移动构造函数 :减少异常对象的复制开销保持异常类简单 :只包含必要的成员变量和方法使用异常层次结构 :创建有层次的异常类体系,便于分类处理异常处理的最佳实践 1. 只在特殊情况下使用异常 不要将异常用于常规的控制流,只在真正的异常情况下使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int findElement (const std::vector<int >& vec, int value) { for (size_t i = 0 ; i < vec.size (); i++) { if (vec[i] == value) { return i; } } throw std::runtime_error ("Element not found" ); } std::optional<size_t > findElement (const std::vector<int >& vec, int value) { for (size_t i = 0 ; i < vec.size (); i++) { if (vec[i] == value) { return i; } } return std::nullopt ; }
2. 捕获适当的异常类型 只捕获你能够处理的异常类型,避免捕获所有异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void process () { try { } catch (...) { std::cout << "Something went wrong" << std::endl; } } void process () { try { } catch (const std::invalid_argument& e) { std::cout << "Invalid argument: " << e.what () << std::endl; } catch (const std::runtime_error& e) { std::cout << "Runtime error: " << e.what () << std::endl; } }
3. 重新抛出异常时保持原始异常信息 当需要重新抛出异常时,使用空的throw语句,保持原始异常信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void processFile (const std::string& filename) { try { std::ifstream file (filename) ; if (!file) { throw std::runtime_error ("Failed to open file" ); } } catch (const std::exception& e) { std::cerr << "Error in processFile: " << e.what () << std::endl; throw ; } }
4. 使用异常层次结构 创建有层次的异常类体系,便于分类处理:
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 class AppException : public std::exception {public : explicit AppException (const std::string& message) : message(message) { } const char * what () const noexcept override { return message.c_str (); } private : std::string message; }; class BusinessException : public AppException {public : explicit BusinessException (const std::string& message) : AppException(message) { } }; class DataAccessException : public AppException {public : explicit DataAccessException (const std::string& message) : AppException(message) { } }; void processOrder () { try { throw BusinessException ("Order processing failed" ); } catch (const DataAccessException& e) { std::cout << "Data access error: " << e.what () << std::endl; } catch (const BusinessException& e) { std::cout << "Business error: " << e.what () << std::endl; } catch (const AppException& e) { std::cout << "Application error: " << e.what () << std::endl; } }
5. 异常处理与错误码的结合 在某些情况下,特别是与C API交互或需要向后兼容时,可能需要结合使用异常和错误码:
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 enum class ErrorCode { SUCCESS = 0 , INVALID_ARGUMENT = 1 , NETWORK_ERROR = 2 , DATABASE_ERROR = 3 }; class AppException : public std::runtime_error {public : AppException (ErrorCode code, const std::string& message) : std::runtime_error (message), code (code) {} ErrorCode getErrorCode () const { return code; } private : ErrorCode code; }; ErrorCode processWithErrorCodes (int value, std::string& errorMessage) { try { if (value < 0 ) { throw AppException (ErrorCode::INVALID_ARGUMENT, "Value must be non-negative" ); } return ErrorCode::SUCCESS; } catch (const AppException& e) { errorMessage = e.what (); return e.getErrorCode (); } catch (const std::exception& e) { errorMessage = e.what (); return ErrorCode::NETWORK_ERROR; } } void processWithExceptions (int value) { if (value < 0 ) { throw AppException (ErrorCode::INVALID_ARGUMENT, "Value must be non-negative" ); } }
异常处理的性能考虑 异常处理的性能开销 异常处理的性能开销主要来自:
异常表生成 :编译器为每个函数生成异常表,增加代码大小栈展开 :当异常抛出时,需要沿着调用栈向上查找catch块异常对象的创建和销毁 :异常对象通常在堆上分配性能优化策略 只在真正的异常情况下使用异常 :避免将异常用于常规控制流使用noexcept说明符 :对于不会抛出异常的函数,使用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 27 class MyException : public std::exception {private : std::string message; public : explicit MyException (std::string msg) : message(std::move(msg)) { } MyException (MyException&& other) noexcept : message (std::move (other.message)) {} MyException& operator =(MyException&& other) noexcept { if (this != &other) { message = std::move (other.message); } return *this ; } MyException (const MyException&) = delete ; MyException& operator =(const MyException&) = delete ; const char * what () const noexcept override { return message.c_str (); } };
异常处理的测试 测试异常抛出 使用测试框架测试函数是否在预期条件下抛出正确的异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdexcept> int divide (int a, int b) { if (b == 0 ) { throw std::invalid_argument ("Division by zero" ); } return a / b; } TEST (DivideTest, ThrowsOnZeroDivision) { EXPECT_THROW (divide (10 , 0 ), std::invalid_argument); } TEST (DivideTest, ReturnsCorrectResult) { EXPECT_EQ (divide (10 , 2 ), 5 ); EXPECT_EQ (divide (7 , 3 ), 2 ); }
测试异常安全 测试函数在抛出异常时是否保持异常安全:
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 #include <vector> void appendIfPositive (std::vector<int >& vec, int value) { if (value > 0 ) { vec.push_back (value); } } TEST (ExceptionSafetyTest, AppendIfPositiveIsStrongExceptionSafe) { std::vector<int > original = {1 , 2 , 3 }; std::vector<int > testVec = original; appendIfPositive (testVec, 4 ); EXPECT_EQ (testVec, std::vector <int >({1 , 2 , 3 , 4 })); testVec = original; appendIfPositive (testVec, 0 ); EXPECT_EQ (testVec, original); testVec = original; appendIfPositive (testVec, -1 ); EXPECT_EQ (testVec, original); }
模拟异常 在测试中模拟异常,验证系统的恢复能力:
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 63 64 65 66 67 68 #include <stdexcept> #include <memory> class Database {public : virtual ~Database () = default ; virtual void save (const std::string& data) = 0 ; }; class RealDatabase : public Database {public : void save (const std::string& data) override { } }; class MockDatabase : public Database {public : MockDatabase (bool throwOnSave) : throwOnSave (throwOnSave) {} void save (const std::string& data) override { if (throwOnSave) { throw std::runtime_error ("Simulated database error" ); } } private : bool throwOnSave; }; class UserService {private : std::unique_ptr<Database> db; public : UserService (std::unique_ptr<Database> db) : db (std::move (db)) {} bool createUser (const std::string& userData) { try { db->save (userData); return true ; } catch (const std::exception& e) { return false ; } } }; TEST (UserServiceTest, HandlesDatabaseError) { auto mockDb = std::make_unique <MockDatabase>(true ); UserService service (std::move(mockDb)) ; bool result = service.createUser ("{\"name\": \"John\"}" ); EXPECT_FALSE (result); } TEST (UserServiceTest, CreatesUserSuccessfully) { auto mockDb = std::make_unique <MockDatabase>(false ); UserService service (std::move(mockDb)) ; bool result = service.createUser ("{\"name\": \"John\"}" ); EXPECT_TRUE (result); }
C++标准库中的异常 标准异常层次结构 C++标准库提供了一套异常类层次结构,所有标准异常都继承自std::exception:
std::exception :所有标准异常的基类std::bad_alloc :内存分配失败std::bad_cast :类型转换失败std::bad_typeid :类型信息获取失败std::bad_exception :异常处理过程中的异常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::system_error :系统错误(C++11)标准异常的使用 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 #include <iostream> #include <stdexcept> #include <vector> void processVector (const std::vector<int >& vec, size_t index) { if (index >= vec.size ()) { throw std::out_of_range ("Index out of range" ); } std::cout << "Value at index " << index << ": " << vec[index] << std::endl; } void calculate (int a, int b) { if (b == 0 ) { throw std::invalid_argument ("Division by zero" ); } if (a > 1000000 || b > 1000000 ) { throw std::domain_error ("Arguments too large" ); } std::cout << "Result: " << a / b << std::endl; } int main () { std::vector<int > vec = {1 , 2 , 3 , 4 , 5 }; try { processVector (vec, 10 ); } catch (const std::out_of_range& e) { std::cout << "Out of range error: " << e.what () << std::endl; } try { calculate (10 , 0 ); } catch (const std::invalid_argument& e) { std::cout << "Invalid argument error: " << e.what () << std::endl; } try { calculate (2000000 , 1 ); } catch (const std::domain_error& e) { std::cout << "Domain 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 31 32 33 34 35 class DatabaseException : public std::runtime_error {public : DatabaseException (const std::string& sql, const std::string& error) : std::runtime_error ("Database error: " + error + " (SQL: " + sql + ")" ) {} }; class OrderProcessingException : public std::runtime_error {public : OrderProcessingException (const std::string& orderId, const std::exception& cause) : std::runtime_error ("Failed to process order " + orderId + ". Cause: " + cause.what ()) {} }; void processOrder (const std::string& orderId) { try { } catch (const OrderProcessingException& e) { logError (e.what ()); notifyUser ("Order processing failed: " + e.what ()); scheduleRetry (orderId); } catch (const std::exception& e) { logError ("Unexpected error: " + e.what ()); notifyAdmin ("Unexpected error: " + e.what ()); emergencyRecovery (); } }
异常日志 良好的异常日志对于调试和监控至关重要。异常日志应该包含:
异常类型和消息 异常发生的位置(文件、行号、函数名) 异常发生时的上下文信息(如用户ID、请求参数等) 异常的调用栈 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdexcept> #include <source_location> void logException (const std::exception& e, const std::source_location& loc = std::source_location::current()) { std::cerr << "Exception at " << loc.file_name () << ":" << loc.line () << " in " << loc.function_name () << std::endl; std::cerr << "Error: " << e.what () << std::endl; } void processData (int value) { try { if (value < 0 ) { throw std::invalid_argument ("Value must be non-negative" ); } } catch (const std::exception& e) { logException (e); throw ; } }
异常处理与测试 在实际项目中,应该为异常处理编写专门的测试:
测试异常抛出 :测试函数是否在预期条件下抛出正确的异常测试异常安全 :测试函数在抛出异常时是否保持异常安全测试异常恢复 :测试系统在遇到异常时是否能够正确恢复测试异常传播 :测试异常是否能够正确传播到适当的处理点总结 异常处理是C++编程中的重要组成部分,它提供了一种结构化的方式来处理错误,使得错误处理代码与正常业务逻辑分离,提高了代码的可读性和可维护性。
通过本章的学习,你应该掌握:
异常处理的基本语法 :抛出和捕获异常异常安全保证 :基本异常安全、强异常安全和无异常保证异常传播 :异常如何在调用栈中传播,栈展开过程异常与RAII的结合 :如何使用RAII确保异常安全自定义异常类 :如何创建和使用自定义异常类异常处理的最佳实践 :只在特殊情况下使用异常,捕获适当的异常类型,使用异常层次结构等异常处理的性能考虑 :异常处理的性能开销,性能优化策略异常处理的测试 :测试异常抛出,测试异常安全,模拟异常C++标准库中的异常 :标准异常层次结构,标准异常的使用实际项目中的异常处理策略 :分层异常处理,异常日志,异常处理与测试异常处理是一把双刃剑,它既可以使程序更加健壮和可靠,也可能导致性能下降和代码复杂性增加。在实际项目中,应该根据具体情况,合理使用异常处理机制,遵循最佳实践,以达到代码质量和性能的平衡。
通过不断学习和实践,你会逐渐掌握异常处理的精髓,成为一名更加优秀的C++程序员。