第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() {
// 测试std::bad_alloc
try {
while (true) {
new int[1000000000];
}
} catch (const std::bad_alloc& e) {
std::cout << "bad_alloc: " << e.what() << std::endl;
}

// 测试std::out_of_range
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;
}

// 测试std::invalid_argument
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>

// C++11之前的异常规格说明(已废弃)
void func() throw(std::runtime_error) {
throw std::runtime_error("Error");
}

// C++11及以后的noexcept说明符
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>

// 使用RAII管理文件资源
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;
}

// 即使发生异常,FileHandler的析构函数也会被调用,文件会被关闭
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;
}

异常处理的性能考虑

异常处理会带来一定的性能开销,主要包括:

  1. 异常表:编译器会为每个函数生成异常表,记录可能抛出异常的位置和对应的处理代码
  2. 栈展开:当异常抛出时,需要沿着调用栈向上查找匹配的catch块,这一过程称为栈展开
  3. 异常对象的创建和销毁:异常对象需要在堆上分配内存,使用完毕后需要销毁

减少异常处理的性能开销

  1. 只在特殊情况下使用异常:不要将异常用于常规的控制流
  2. 使用 noexcept 说明符:对于不会抛出异常的函数,使用 noexcept 说明符,这样编译器可以进行优化
  3. 避免在异常处理中执行复杂操作:异常处理代码应该简洁,只处理必要的逻辑
  4. 使用移动语义:对于自定义异常类,实现移动构造函数和移动赋值运算符,减少异常对象的复制开销

异常处理的应用场景

1. 资源管理

使用异常处理来管理资源的分配和释放,确保资源在发生错误时能够被正确释放。

2. 错误恢复

在某些情况下,程序可以从异常中恢复,继续执行。例如,网络连接失败时,可以尝试重新连接。

3. 错误报告

异常可以携带详细的错误信息,帮助开发者和用户了解错误的原因。

4. 分层错误处理

在大型应用程序中,可以在不同的层次处理不同类型的异常,提高代码的可维护性。

总结

异常处理是C++中一种重要的错误处理机制,它允许我们:

  1. 分离错误处理代码:将正常的业务逻辑与错误处理代码分离,使代码更加清晰
  2. 提供详细的错误信息:通过异常对象携带详细的错误信息,便于调试和处理
  3. 确保资源释放:结合RAII技术,确保资源在发生异常时能够被正确释放
  4. 实现错误传播:异常可以沿着调用栈向上传播,直到找到匹配的catch块
  5. 提高代码的健壮性:通过捕获和处理异常,使程序能够更加健壮地运行

然而,异常处理也有一些缺点:

  1. 性能开销:异常处理会带来一定的性能开销
  2. 代码复杂性:过度使用异常会增加代码的复杂性
  3. 调试困难:异常的传播路径可能比较复杂,增加调试的难度

因此,我们应该合理使用异常处理,在需要的地方使用,避免滥用。通过遵循异常处理的最佳实践,我们可以充分发挥异常处理的优势,编写更加健壮和可靠的C++程序。

异常处理是C++的重要特性之一,也是成为优秀C++程序员的必备知识。通过不断学习和实践,你会逐渐掌握异常处理的使用技巧,并能够在实际项目中灵活应用。