C++学习笔记---028
C++之抛异常基础知识
前言:
前面篇章学习了C++智能指针的知识认识和应用,接下来继续学习,C++关于抛异常的基础知识。
/知识点汇总/
1、C++抛异常介绍
1.1、抛异常概念
在C++中,异常处理是一种重要的错误管理机制,它允许程序在运行时遇到错误时能够优雅地处理这些错误,而不是简单地终止程序执行。C++通过try、catch和throw关键字来实现异常处理。
1.2、抛出异常 (throw)关键字介绍
throw关键字用于抛出一个异常。它可以抛出C++中几乎任何类型的对象,但通常建议抛出从标准异常类派生的对象,因为这些类提供了丰富的错误信息和类型安全。
#include <iostream>
#include <stdexcept> // 包含标准异常类
void mightGoWrong() {
// 假设这里有一些可能出错的代码
throw std::runtime_error("Something went wrong!");
}
int main() {
try {
mightGoWrong();
} catch (const std::runtime_error& e) {
std::cerr << "Caught an exception: " << e.what() << std::endl;
}
return 0;
}
在这个例子中,mightGoWrong函数抛出了一个std::runtime_error异常,当这个异常被抛出时,程序的控制流会立即跳转到try块后面的第一个与之匹配的catch块。catch块中的代码可以处理这个异常,比如记录日志、清理资源或给用户一个友好的错误消息。
1.3、捕获异常 (catch)关键字介绍
catch块用于捕获并处理异常。如上例所示,你可以捕获特定类型的异常,并使用该类型的对象来访问异常的详细信息(如错误信息)。
C++允许你有多个catch块来捕获不同类型的异常,或者一个通用的catch(…)块来捕获所有类型的异常。
try {
// 可能抛出异常的代码
} catch (const std::runtime_error& e) {
// 处理运行时错误
} catch (const std::invalid_argument& e) {
// 处理无效参数错误
} catch (...) {
// 捕获所有其他类型的异常
}
2、 RAII资源管理
2.1、RAII简化资源管理
在异常发生时,确保所有已分配的资源都被正确释放是很重要的。C++通过RAII(Resource Acquisition Is Initialization)原则来简化资源管理。RAII建议将资源的获取(如动态内存分配、文件句柄、数据库连接等)封装在对象的构造函数中,并在对象的析构函数中释放这些资源。这样,当异常发生时,对象的析构函数会自动被调用,从而释放资源。
class ResourceHolder {
public:
ResourceHolder() {
// 分配资源
}
~ResourceHolder() {
// 释放资源
}
// 可能抛出异常的函数
void mightGoWrong() {
throw std::runtime_error("Error");
}
};
void useResource() {
ResourceHolder holder;
try {
holder.mightGoWrong();
} catch (...) {
// 异常处理,但holder的析构函数会自动被调用
}
// holder的析构函数在这里或更早(如果异常被抛出)会被调用
}
2.2、RAII介绍
RAII(Resource Acquisition Is
Initialization)是一种在C++中广泛使用的资源管理技术。它的基本思想是利用对象的生命周期来自动管理资源,包括资源的获取和释放。RAII的核心在于将资源的获取(如动态内存分配、文件句柄、网络连接等)放在对象的构造函数中,而将资源的释放放在对象的析构函数中。这样,当对象的生命周期结束时(例如,对象离开作用域或被删除时),其析构函数会被自动调用,从而确保资源被正确释放,避免了资源泄漏等问题。
2.3、RAII的优点
1.自动资源管理:RAII通过对象的生命周期自动管理资源,减少了显式调用资源释放函数的需要,降低了出错的可能性。
2.异常安全:在构造函数中获取的资源,即使在构造函数抛出异常的情况下,也能保证在对象的析构函数中正确释放,从而提高了程序的异常安全性。
3.简化代码:通过封装资源的管理细节,RAII使得代码更加简洁、清晰,易于理解和维护。
2.4、RAII的缺点
1.资源生命周期的局限性: 如果资源的生命周期无法完全由对象的构造函数和析构函数来管理,那么使用RAII可能会导致资源泄漏或过早释放。例如,当资源的生命周期由外部因素控制时,使用RAII可能不合适。
2.资源获取和释放的成本: 对于某些资源,其获取和释放操作可能非常耗时或复杂。在这种情况下,频繁地创建和销毁RAII对象可能会导致性能问题。虽然这不是RAII本身的缺点,但在选择是否使用RAII时需要考虑到这一点。
3.跨线程资源管理的复杂性: 如果资源需要在多个线程之间共享或跨线程使用,使用RAII可能会导致线程安全问题。在这种情况下,需要采用其他线程安全的资源管理方式,如互斥锁或原子操作。
4.异常处理的限制: RAII无法直接处理构造函数或析构函数中抛出的异常。虽然可以在析构函数中捕获异常并进行处理,但析构函数本身不能抛出异常(因为这可能会导致未定义行为)。因此,在RAII的析构函数中处理异常时需要格外小心。
5.灵活性不足: 在某些情况下,RAII可能过于严格,限制了程序员对资源管理的灵活性。例如,当需要在对象的生命周期内多次获取和释放同一资源时,RAII可能不是最佳选择。
需要熟悉C++的构造函数、析构函数、拷贝构造函数、赋值运算符等概念,并理解RAII的工作原理。
2.5、RAII的实现
RAII通常通过定义一个类来实现,这个类封装了需要管理的资源。类的构造函数负责资源的获取,析构函数负责资源的释放。此外,还可以根据需要定义拷贝构造函数、赋值运算符等,以确保资源的正确管理(注意,对于某些资源,如文件句柄,可能需要禁止拷贝和赋值操作,以避免资源被意外共享或释放多次)。
以下是一个简单的RAII示例,用于管理动态分配的内存:
#include <iostream>
class ScopedPointer {
private:
int* ptr; // 指向动态分配的内存
public:
// 构造函数,分配内存
explicit ScopedPointer(int* p = nullptr) : ptr(p) {}
// 析构函数,释放内存
~ScopedPointer() {
delete ptr; // 注意:这里假设ptr是非空的,并且只被释放一次
}
// 禁止拷贝构造函数和赋值运算符,因为简单实现会导致资源被意外共享或释放多次
ScopedPointer(const ScopedPointer&) = delete;
ScopedPointer& operator=(const ScopedPointer&) = delete;
// 可以添加其他成员函数来操作指针,例如解引用等
// 但需要注意,这些操作必须确保不会违反RAII原则
};
int main() {
{
ScopedPointer sp(new int(42)); // 分配内存并初始化
// ... 在这里可以使用sp指向的内存 ...
// 当离开这个作用域时,sp的析构函数会被调用,从而释放内存
}
// 此时,sp已经不再指向任何有效的内存,尝试访问它将是不安全的
return 0;
}
注意:
上面的ScopedPointer示例是非常基础的,并且存在一些限制(如禁止拷贝和赋值)。在实际应用中,通常会使用更复杂的类(如std::unique_ptr、std::shared_ptr等)来管理资源,这些类已经内置了RAII机制,并提供了更丰富的功能和更好的性能。
3、标准异常库
3.1、标准异常库概念介绍
C++标准异常库提供了一系列用于处理运行时错误的异常类,这些类都继承自std::exception类或其派生类。这些异常类用于报告程序中出现的各种问题,从而允许开发者以更优雅的方式处理错误,而不是简单地终止程序。
3.2、继承体系
C++标准异常库中的异常类通常遵循一个继承体系,其中std::exception是所有标准异常的基类。从std::exception派生出两大类异常:逻辑错误(std::logic_error)和运行时错误(std::runtime_error)。
3.2.1、逻辑错误(std::logic_error)
这类错误通常是由于程序逻辑上的缺陷导致的,可以在程序开发阶段通过代码审查或测试来避免。
常见的逻辑错误异常包括:
逻辑错误异常 | 属性 |
---|---|
std::invalid_argument | 当函数接收到无效的参数时抛出 |
std::out_of_range | 当尝试访问容器(如std::vector、std::map等)的超出范围的索引时抛出 |
std::length_error | 当容器的大小超过其最大允许大小时抛出 |
std::domain_error | 当数学函数接收到其定义域之外的参数时抛出 |
3.2.2、运行时错误(std::runtime_error)
这类错误是由程序运行时的事件引起的,通常难以在开发阶段预测或避免。
常见的运行时错误异常包括:
运行错误异常 | 属性 |
---|---|
std::range_error | 当算术运算的结果超出其可表示的范围时抛出 |
std::overflow_error | 当算术运算发生上溢时抛出 |
std::underflow_error | 当算术运算发生下溢时抛出 |
3.3、异常处理
在C++中,异常处理通过try、catch和throw关键字来实现。当try块中的代码抛出异常时,程序会立即跳转到紧随其后的catch块(如果有的话),并查找与抛出的异常类型相匹配的catch块来执行。
try {
// 可能抛出异常的代码
throw std::runtime_error("An error occurred");
} catch (const std::runtime_error& e) {
// 处理运行时错误
std::cerr << "Caught runtime error: " << e.what() << std::endl;
} catch (...) {
// 捕获所有其他类型的异常
std::cerr << "Caught unknown exception" << std::endl;
}
3.4、异常类的特性
1.所有的异常类都提供了构造函数,允许在抛出异常时传递一个描述性的字符串。
2.每个异常类都重写了std::exception类的what()成员函数,该函数返回一个指向错误描述字符串的指针。
3.异常的抛出和捕获是基于类型的,但也可以通过基类来捕获派生类的异常,实现多态性。
4、自定义异常
4.1、说明
除了使用标准异常库中的异常类外,C++还允许开发者定义自己的异常类。自定义异常类通常通过继承std::exception或其派生类来实现,并可能添加额外的成员变量和成员函数来提供额外的错误信息或处理逻辑。
4.2、实现
在C++中,自定义异常通常是通过继承自std::exception类(或其派生类)来实现的。以下是一个自定义异常的简单样例,我们将创建一个名为MyCustomException的类,该类继承自std::exception,并添加了一个额外的成员变量来存储错误消息。
#include <iostream>
#include <exception>
#include <string>
// 自定义异常类
class MyCustomException : public std::exception {
private:
std::string message; // 用于存储错误消息的字符串
public:
// 构造函数,接收一个字符串作为错误消息
explicit MyCustomException(const std::string& msg) : message(msg) {}
// 重写what()函数,返回错误消息的c_str()
virtual const char* what() const noexcept override {
return message.c_str();
}
};
// 一个可能抛出MyCustomException的函数
void mightThrowMyCustomException() {
// 假设这里有一些逻辑,如果满足某些条件,则抛出异常
throw MyCustomException("Something went wrong!");
}
int main() {
try {
mightThrowMyCustomException(); // 调用可能抛出异常的函数
} catch (const MyCustomException& e) {
// 捕获并处理MyCustomException
std::cerr << "Caught MyCustomException: " << e.what() << std::endl;
} catch (...) {
// 捕获所有其他类型的异常
std::cerr << "Caught unknown exception" << std::endl;
}
return 0;
}
小结:
1.在这个例子中,MyCustomException类继承自std::exception,并定义了一个私有成员变量message来存储错误消息。我们还提供了一个构造函数,它接受一个std::string类型的参数并将其赋值给message。最重要的是,我们重写了std::exception的what()函数,以便在捕获异常时能够返回自定义的错误消息。
2.在main函数中,我们调用了mightThrowMyCustomException()函数,该函数可能会抛出MyCustomException。我们使用try-catch块来捕获并处理这种异常。如果捕获到MyCustomException,则输出错误消息;如果捕获到其他类型的异常,则输出一条通用的错误消息。
3.这个样例展示了如何在C++中定义和使用自定义异常。通过这种方式,你可以为程序中的特定错误情况提供更详细和有针对性的错误处理。
5、总结:异常的抛出、捕获
涉及到三个关键字:try、catch、throw。
1.throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
2.catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
3.try: try块中放置可能抛出异常的代码,try 块中的代码被称为保护代码,块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
4.基本语法:
try
{
throw 异常值;
}
catch(异常类型1 异常值1)
{
处理异常的代码1;
}
catch(异常类型2 异常值2)
{
处理异常的代码2;
}
catch(...)//任何异常都捕获
{
处理异常的代码3;
}
6、注意事项
1.抛出异常时,程序的控制流会立即离开当前的函数(包括所有嵌套的函数),直到找到匹配的catch块为止。
2.抛出异常时,对象的析构函数会被调用,以清理局部对象的资源。但是,如果构造函数在初始化过程中抛出异常,那么已经成功构造的局部对象的析构函数也会被调用。
3.在构造函数或析构函数中抛出异常时需要特别小心,因为这可能会导致资源泄露或程序崩溃。
4.使用异常进行正常的控制流(如循环控制)通常是不推荐的,因为这会降低代码的可读性和可维护性。异常应该用于处理异常情况,即那些在正常程序执行中不期望发生的情况。