C++标准异常类:stdexcept深度解析
C++标准库通过 <stdexcept> 头文件提供了一系列标准的异常类。这些类都派生自基类 std::exception (定义在 <exception> 头文件中),并为常见的程序错误情况提供了具体的异常类型。使用这些标准异常类可以使错误处理更加规范和易于理解。
std::exception基类
在深入 <stdexcept> 之前,回顾一下 std::exception:
namespace std {
class exception {
public:
exception() noexcept;
exception(const exception&) noexcept;
exception& operator=(const exception&) noexcept;
virtual ~exception() noexcept;
virtual const char* what() const noexcept;
};
}
- what(): 这是一个虚函数,返回一个C风格字符串(const char*),描述异常的具体信息。派生类通常会重写此函数以提供更具体的错误消息。
<stdexcept>中的异常类层次结构
<stdexcept> 中定义的异常类形成了一个层次结构,它们都直接或间接地继承自 std::exception。
主要的两个分支是:
- std::logic_error: 表示程序逻辑中的错误。这类错误理论上可以在程序运行前通过代码检查发现。
- std::runtime_error: 表示只有在程序运行时才能检测到的错误。
1. std::logic_error及其派生类
std::logic_error 本身可以被抛出,或者使用其更具体的派生类。
#include <stdexcept> // For std::logic_error and its derivatives
#include <string>
// Constructor for std::logic_error and its children typically takes a const char* or std::string
// std::logic_error(const std::string& what_arg);
// std::logic_error(const char* what_arg);
其主要派生类包括:
- std::domain_error: 当传递给函数的参数值不在函数预期的有效域内时抛出。
- 例如:计算一个负数的平方根,如果数学库设计为只接受非负数,则可能抛出此异常。
- #include <cmath> // For std::sqrt
#include <stdexcept>
#include <iostream>
double calculate_sqrt(double val) {
if (val < 0) {
throw std::domain_error("Argument to sqrt must be non-negative.");
}
return std::sqrt(val);
} - std::invalid_argument: 当传递给函数的参数无效时抛出。这通常比 domain_error 更通用,指参数本身不符合某种规范,但不一定是数学域的问题。
- 例如:一个函数期望一个非空字符串,但收到了一个空字符串。
- #include <string>
#include <stdexcept>
#include <iostream>
void process_string(const std::string& s) {
if (s.empty()) {
throw std::invalid_argument("Input string cannot be empty.");
}
// ... process s ...
} - std::length_error: 当尝试创建一个超出其最大允许长度的对象时抛出。
- 例如:尝试创建一个过大的 std::string 或 std::vector。
- #include <vector>
#include <stdexcept>
#include <iostream>
void create_large_vector(size_t sz) {
try {
std::vector<int> v;
v.reserve(sz); // May throw if sz is excessively large
// Or, more directly:
if (sz > v.max_size()) { // Check against max_size()
throw std::length_error("Requested vector size exceeds maximum allowed size.");
}
v.resize(sz); // This might throw std::bad_alloc or std::length_error
} catch (const std::length_error& e) {
std::cerr << "Length error: " << e.what() << std::endl;
} catch (const std::bad_alloc& e) {
std::cerr << "Allocation error: " << e.what() << std::endl;
}
} - std::out_of_range: 当试图访问一个容器元素,而使用的索引或键超出了有效范围时抛出。
- 例如:std::vector::at() 或 std::map::at() 在索引/键不存在时会抛出此异常。
- #include <vector>
#include <stdexcept>
#include <iostream>
void access_vector_element(const std::vector<int>& vec, size_t index) {
try {
int val = vec.at(index); // .at() performs bounds checking
std::cout << "Element at index " << index << " is " << val << std::endl;
} catch (const std::out_of_range& e) {
std::cerr << "Out of range error: " << e.what() << std::endl;
}
}
2. std::runtime_error及其派生类
std::runtime_error 本身可以被抛出,或者使用其更具体的派生类。
#include <stdexcept> // For std::runtime_error and its derivatives
#include <string>
// Constructor for std::runtime_error and its children typically takes a const char* or std::string
// std::runtime_error(const std::string& what_arg);
// std::runtime_error(const char* what_arg);
其主要派生类包括:
- std::range_error: 当内部计算的结果无法用目标类型表示(即发生范围溢出)时抛出。
- 例如:一个数学计算产生了超出目标类型(如 double)表示范围的值。
- #include <stdexcept>
#include <iostream>
#include <cmath> // For std::exp
double safe_exp(double val) {
double result = std::exp(val);
if (std::isinf(result) && val > 0) { // Check if exp overflowed to infinity
throw std::range_error("Result of exp() is too large to be represented.");
}
return result;
} - std::overflow_error: 当算术上溢发生时抛出。这通常指计算结果大于目标类型能表示的最大值。
- 与 range_error 类似,但更侧重于算术运算本身的上溢。
- #include <stdexcept>
#include <iostream>
#include <limits> // For std::numeric_limits
int add_with_overflow_check(int a, int b) {
if ((b > 0 && a > std::numeric_limits<int>::max() - b) ||
(b < 0 && a < std::numeric_limits<int>::min() - b)) {
throw std::overflow_error("Arithmetic overflow detected during addition.");
}
return a + b;
} - std::underflow_error: 当算术下溢发生时抛出。这通常指计算结果(在绝对值上)小于目标类型能表示的最小正规化数,且可能导致精度损失或变为零。
- #include <stdexcept>
#include <iostream>
double divide_with_underflow_check(double a, double b) {
if (b != 0.0 && std::abs(a / b) < std::numeric_limits<double>::min() && a != 0.0) {
// This check is simplistic; proper underflow detection can be complex
// throw std::underflow_error("Arithmetic underflow detected during division.");
}
if (b == 0.0 && a != 0.0) throw std::runtime_error("Division by zero");
if (b == 0.0 && a == 0.0) return 0.0; // Or throw, depending on convention
return a / b;
} - 注意:现代浮点数通常有非正规化数,使得下溢处理更平滑,直接抛出 std::underflow_error 的场景可能不那么常见,除非显式检查并抛出。
- std::system_error (C++11): 这个异常类定义在 <system_error> 头文件中,但它也属于运行时错误的一种。它用于报告来自操作系统或其他底层接口的错误。它通常包含一个错误码 (std::error_code) 和一个描述性字符串。
- 虽然不直接在 <stdexcept> 中定义,但概念上与 runtime_error 相关。
如何选择和使用标准异常
- 优先使用最具体的异常类型: 如果错误情况符合某个特定派生类的描述(如 std::out_of_range),则应优先使用该类型。这使得 catch 块可以更精确地捕获和处理特定类型的错误。
- 提供有意义的 what() 消息: 在构造异常对象时,传递一个清晰、简洁且信息丰富的字符串,说明错误的原因。这个消息对调试非常有帮助。
- throw std::invalid_argument("User ID '" + userId + "' not found.");
- 按继承层次捕获异常: 在 try-catch 块中,如果需要捕获多种相关的异常,应该将派生类异常的 catch 块放在基类异常的 catch 块之前。否则,基类的 catch 块会先捕获到派生类的异常对象。
- try {
// ... code that might throw ...
} catch (const std::out_of_range& e) {
// Handle out_of_range specifically
std::cerr << "Specific: Out of range: " << e.what() << std::endl;
} catch (const std::logic_error& e) {
// Handle other logic errors
std::cerr << "Logic error: " << e.what() << std::endl;
} catch (const std::runtime_error& e) {
// Handle runtime errors
std::cerr << "Runtime error: " << e.what() << std::endl;
} catch (const std::exception& e) {
// Handle any other standard exception
std::cerr << "Standard exception: " << e.what() << std::endl;
} catch (...) {
// Handle any other non-standard exception (ellipsis catch-all)
std::cerr << "Unknown exception caught." << std::endl;
} - 自定义异常: 如果标准异常类不能充分描述你的特定错误情况,可以从 std::exception 或其适当的派生类(如 std::runtime_error)派生出自定义异常类。
- class MyCustomError : public std::runtime_error {
public:
MyCustomError(const std::string& message) : std::runtime_error(message) {}
// Optionally add more members or methods specific to this error
};
// throw MyCustomError("A very specific problem occurred.");
示例代码
#include <iostream>
#include <vector>
#include <string>
#include <stdexcept> // Key header for these exceptions
// Function that might throw various stdexcept exceptions
void process_data(int val, const std::vector<std::string>& data, size_t index) {
if (val == 0) {
throw std::invalid_argument("Input value 'val' cannot be zero.");
}
if (val < 0) {
throw std::domain_error("Input value 'val' must be positive for this operation.");
}
if (data.empty() && index == 0) {
// This might be okay or an error depending on context.
// For demonstration, let's say it's an error if we expect data.
}
if (index >= data.size()) {
// Constructing a more informative message
std::string err_msg = "Index " + std::to_string(index) +
" is out of range for data size " + std::to_string(data.size());
throw std::out_of_range(err_msg);
}
if (data.at(index).length() > 1000) { // Assuming a hypothetical length limit
throw std::length_error("String at index is too long.");
}
// Simulate a runtime condition that might fail
if (val == 42) { // Arbitrary condition for runtime_error
throw std::runtime_error("Encountered an unexpected runtime condition with value 42.");
}
std::cout << "Processing data with val=" << val << " at index=" << index << ": " << data.at(index) << std::endl;
}
int main() {
std::vector<std::string> my_data = {"apple", "banana", "cherry"};
try {
process_data(10, my_data, 1); // Should succeed
// process_data(0, my_data, 0); // Throws std::invalid_argument
// process_data(-5, my_data, 0); // Throws std::domain_error
// process_data(10, my_data, 5); // Throws std::out_of_range
std::vector<std::string> large_string_data = {std::string(2000, 'x')};
// process_data(10, large_string_data, 0); // Throws std::length_error
process_data(42, my_data, 0); // Throws std::runtime_error
} catch (const std::invalid_argument& e) {
std::cerr << "Caught invalid_argument: " << e.what() << std::endl;
} catch (const std::domain_error& e) {
std::cerr << "Caught domain_error: " << e.what() << std::endl;
} catch (const std::out_of_range& e) {
std::cerr << "Caught out_of_range: " << e.what() << std::endl;
} catch (const std::length_error& e) {
std::cerr << "Caught length_error: " << e.what() << std::endl;
} catch (const std::logic_error& e) { // Catches any other logic_error
std::cerr << "Caught other logic_error: " << e.what() << std::endl;
} catch (const std::runtime_error& e) { // Catches runtime_error and its derivatives (if any not caught above)
std::cerr << "Caught runtime_error: " << e.what() << std::endl;
} catch (const std::exception& e) { // Catches any other std::exception
std::cerr << "Caught other std::exception: " << e.what() << std::endl;
} catch (...) {
std::cerr << "Caught an unknown exception." << std::endl;
}
return 0;
}
总结
<stdexcept> 提供的标准异常类是C++错误处理机制的重要组成部分。它们为常见的错误场景定义了一致的、可识别的类型,使得代码的健壮性和可维护性得以提高。
- std::logic_error 及其派生类(domain_error, invalid_argument, length_error, out_of_range)用于报告通常可以在编码阶段避免的逻辑错误。
- std::runtime_error 及其派生类(range_error, overflow_error, underflow_error)用于报告通常在运行时才能检测到的错误。
通过合理地抛出和捕获这些标准异常,开发者可以编写出更清晰、更易于调试和维护的C++代码。
下一篇:C语言设计如何处理返回值与错误码