【C++】异常

一、C++异常概念

异常是 C++ 中处理程序运行时错误的标准化方式,允许程序在检测到无法处理的错误时 中断当前执行流程,并通过 栈展开(Stack Unwinding) 将控制权转移到能够处理该错误的代码块。

异常机制涉及三个关键操作:抛出异常(throw)捕获异常(catch)定义受保护代码(try)

  • throw

    • 用于抛出异常对象(可以是任意类型,但建议使用标准异常类或自定义异常类)。
    • 抛出后,当前函数停止执行,控制权转移到最近的匹配的 catch 块。
  • try

    • 定义受保护的代码块,其内部代码可能抛出异常。
    • 如果 try 块中抛出异常,程序会跳过后续代码,直接进入匹配的 catch 块。
  • catch

    • 捕获特定类型的异常(按类型匹配)。
    • 可以有多个 catch 块,按声明顺序匹配。
    • catch(...) 捕获所有未匹配的异常(类似 default 分支)。
try
{
 // 保护的标识代码
}catch( ExceptionName e1 )
{
 // catch 块
}catch( ExceptionName e2 )
{
 // catch 块
}catch( ExceptionName eN )
{
 // catch 块
}

二、异常的用法

2.1 异常的抛出和捕获

异常机制是 C++ 中处理运行时错误的核心方式。它的核心思想是通过 抛出异常捕获异常 来分离正常逻辑和错误处理逻辑。

1. 异常的抛出

异常是通过throw语句抛出的,抛出的对象可以是任何类型(基本类型、类类型等)。抛出异常后,程序的控制流会立即从当前执行点跳转到最近的匹配的catch块。

throw SomeException();  // 抛出一个异常对象

2. 异常的匹配

当异常被抛出后,程序会在调用栈中查找与之匹配的catch块。匹配的原则是:

  • 类型匹配catch块中的异常类型必须与抛出的异常对象的类型匹配。
  • 最近匹配:如果有多个catch块能够匹配抛出的异常类型,程序会选择离抛出点最近的那个catch块来处理异常。
try {
    // 可能抛出异常的代码
    throw MyException();
} catch (MyException& e) {
    // 处理 MyException 类型的异常
} catch (std::exception& e) {
    // 处理 std::exception 类型的异常
} catch (...) {
    // 捕获所有其他类型的异常
}

3. 异常对象的拷贝

当异常被抛出时,抛出的异常对象会被拷贝到一个临时对象中。这是因为抛出的异常对象可能是一个临时对象,或者可能在抛出后被销毁。因此,catch块中捕获的是这个临时对象的拷贝。

try {
    throw MyException();  // 抛出异常对象
} catch (MyException e) {  // 捕获异常对象的拷贝
    // 处理异常
}

4. catch(...) 捕获所有异常

catch(...)是一个特殊的catch块,它可以捕获任何类型的异常。然而,由于它没有指定异常类型,因此在catch(...)块中无法直接访问异常对象,也无法知道具体的异常类型。

try {
    // 可能抛出异常的代码
} catch (...) {
    // 捕获所有类型的异常
}

5. 派生类异常与基类捕获

在实际应用中,抛出的异常对象可以是派生类对象,而catch块可以使用基类来捕获。这是多态性的一种体现,允许我们通过基类引用来捕获派生类异常。

class BaseException : public std::exception {
    // 基类异常
};

class DerivedException : public BaseException {
    // 派生类异常
};

try {
    throw DerivedException();  // 抛出派生类异常
} catch (BaseException& e) {  // 使用基类捕获
    // 处理基类或派生类异常
}

6. 栈展开(Stack Unwinding)

在C++中,异常处理机制通过trycatchthrow关键字来实现。当程序抛出异常时,系统会沿着函数调用链向上查找匹配的catch块来处理异常。这个过程称为栈展开(Stack Unwinding)。

异常栈展开的匹配原则
举例说明:

double Division(int a, int b)
{
	 // 当b == 0时抛出异常
	 if (b == 0)
		 throw "Division by zero condition!";
	 else
		 return ((double)a / (double)b);
}
void Func()
{
	 int len, time;
	 cin >> len >> time;
	 cout << Division(len, time) << endl;
}
int main()
{
	 try {
	 	Func();
	 }
	 catch (const char* errmsg) {
	 	cout << errmsg << endl;
	 }
	 catch(...){
		 cout<<"unkown exception"<<endl; 
	 }
	 return 0;
}
  1. 检查throw是否在try块内部

    • Division函数中的throw语句执行时,系统首先检查throw是否在try块内部。
    • Division函数中,throw不在try块内部,因此异常会向上传递到调用Division的函数,即Func函数。
  2. 在调用函数的栈中查找匹配的catch

    • Func函数也没有try-catch块,因此异常继续向上传递到main函数。
    • main函数中,Func函数的调用被包含在try块中,因此系统会检查是否有匹配的catch块。
  3. 匹配catch块并处理异常

    • main函数中的第一个catch块捕获const char*类型的异常,因此异常被捕获并输出"Division by zero condition!"
    • 如果异常类型不匹配,系统会继续检查下一个catch(...)块,捕获所有其他类型的异常。
  4. 继续执行catch块后的代码

    • 一旦异常被捕获并处理,程序会继续执行catch块后的代码。

2.2 重新抛出

在C++中,异常的重新抛出是一种常见的异常处理模式。当一个catch块捕获到异常后,可能无法完全处理该异常,或者需要在处理异常的同时执行一些清理操作(如释放资源),然后再将异常传递给更外层的调用链函数进行处理。这种机制可以通过在catch块中使用throw;语句来实现。

举例说明:

   double Division(int a, int b)
{
    if (b == 0)
    {
        throw "Division by zero condition!";
    }
    return (double)a / (double)b;
}
   void Func()
{
    int* array = new int[10];
    try {
        int len, time;
        cin >> len >> time;
        cout << Division(len, time) << endl;
    }
    catch (...)
    {
        cout << "delete []" << array << endl;
        delete[] array;
        throw; // 重新抛出异常
    }
    // 如果没有异常,继续执行
    cout << "delete []" << array << endl;
    delete[] array;
}
int main()
{
    try {
        Func();
    }
    catch (const char* errmsg) {
        cout << errmsg << endl;
    }
    catch(...) {
        cout << "unkown exception" << endl; 
    }
    return 0;
}

异常重新抛出的机制

  1. 捕获异常并执行清理操作

  2. 重新抛出异常throw;只能在catch块中使用,它会将当前捕获的异常原封不动地传递给更外层的调用链函数。

  3. 外层函数处理异常

2.3 异常安全

异常安全是指程序在抛出异常时,能够正确地管理资源、保持数据一致性,并避免资源泄漏等问题。在C++中,异常安全是一个重要的设计目标,尤其是在涉及资源管理(如内存、文件句柄、锁等)

1. 构造函数与异常

构造函数负责对象的构造和初始化。如果在构造函数中抛出异常,可能会导致对象构造不完整或未完全初始化,从而引发未定义行为。

问题:

  • 如果构造函数在初始化过程中抛出异常,对象可能处于部分构造状态。
  • 如果对象是动态分配的(例如通过new),构造函数抛出异常会导致内存泄漏,因为析构函数不会被调用。

解决方案:

  • 避免在构造函数中执行可能抛出异常的操作。
  • 如果必须执行可能抛出异常的操作,使用 RAII(资源获取即初始化) 技术来管理资源,确保资源在异常发生时能够正确释放。
class MyClass {
public:
    MyClass() {
        ptr = new int[100];  // 可能抛出异常
        // 如果这里抛出异常,ptr 不会被释放
    }
    ~MyClass() {
        delete[] ptr;
    }
private:
    int* ptr;
};

改进方案:使用智能指针(如std::unique_ptr)来管理资源。

#include <memory>
class MyClass {
public:
    MyClass() : ptr(std::make_unique<int[]>(100)) {
        // 如果这里抛出异常,ptr 会自动释放
    }
private:
    std::unique_ptr<int[]> ptr;
};

2. 析构函数与异常

析构函数负责资源的清理。如果在析构函数中抛出异常,可能会导致资源泄漏(如内存泄漏、文件句柄未关闭等)。

问题:

  • C++标准规定,如果析构函数抛出异常且未被捕获,程序会调用std::terminate,导致程序终止。
  • 如果析构函数在栈展开(stack unwinding)过程中抛出异常,程序行为将不可预测。

解决方案:

  • 析构函数中不要抛出异常。
  • 如果析构函数必须执行可能抛出异常的操作,确保捕获并处理异常,避免异常传播到析构函数外部。
class MyClass {
public:
    ~MyClass() {
        try {
            // 可能抛出异常的操作
        } catch (...) {
            // 捕获并处理异常
        }
    }
};

3. 资源管理与异常

在C++中,异常经常会导致资源泄漏问题,例如:

  • newdelete之间抛出异常,导致内存泄漏。
  • lockunlock之间抛出异常,导致死锁。

问题示例:

void unsafeFunction() {
    int* ptr = new int[100];
    // 可能抛出异常的操作
    delete[] ptr;  // 如果异常抛出,delete 不会执行
}

解决方案:
使用 RAII(Resource Acquisition Is Initialization) 技术,将资源管理与对象的生命周期绑定。RAII的核心思想是:

  • 在构造函数中获取资源。
  • 在析构函数中释放资源。
  • 使用栈对象或智能指针来管理资源。

示例:使用智能指针

#include <memory>
void safeFunction() {
    auto ptr = std::make_unique<int[]>(100);  // RAII:资源由智能指针管理
    // 可能抛出异常的操作
    // 无需手动释放资源,智能指针会在作用域结束时自动释放
}

示例:使用RAII管理锁

#include <mutex>
std::mutex mtx;

void safeFunction() {
    std::lock_guard<std::mutex> lock(mtx);  // RAII:锁在构造时加锁,析构时解锁
    // 可能抛出异常的操作
    // 无需手动解锁,lock_guard 会在作用域结束时自动解锁
}

4. 异常安全级别

异常安全通常分为三个级别:

  1. 基本保证(Basic Guarantee):如果抛出异常,程序处于有效状态,没有资源泄漏,但对象的状态可能被修改。
  2. 强保证(Strong Guarantee):如果抛出异常,程序状态回滚到操作之前的状态(类似于事务)。
  3. 无异常保证(No-throw Guarantee): 操作保证不会抛出异常。

示例:强保证

class MyClass {
public:
    void strongGuarantee() {
        auto temp = data;  // 创建临时副本
        temp.modify();      // 修改临时副本
        std::swap(data, temp);  // 交换数据,确保强保证
    }
private:
    Data data;
};

2.4 异常规范

异常规范(Exception Specification)是C++中用于声明函数可能抛出的异常类型的机制。它的目的是让函数的调用者知道该函数可能抛出哪些异常,从而更好地处理异常情况。然而,C++11之后,异常规范的使用方式发生了变化,throw(type)语法被弃用,取而代之的是noexcept关键字。

1. 异常规格说明

在C++98/03中,可以通过throw(type)语法来指定函数可能抛出的异常类型。

void func() throw(int, const char*);  // 表示 func 可能抛出 int 和 const char* 类型的异常

void fun() throw(A,B,C,D);// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
  • 如果函数抛出了未在throw(type)列表中声明的异常类型,程序会调用std::unexpected(),默认行为是终止程序。
  • 这种机制在实际使用中并不灵活,因此在C++11中被弃用。

2. throw() 表示不抛异常

在C++98/03中,throw()表示函数不会抛出任何异常。

void func() throw();  // 表示 func 不会抛出任何异常
  • 如果函数抛出了异常,程序会调用std::unexpected(),默认行为是终止程序。
  • 在C++11中,throw()noexcept取代。

3. 无异常接口声明

如果函数没有异常规格说明,则表示该函数可能抛出任何类型的异常。

void func();  // 可能抛出任何类型的异常

4. C++11 的 noexcept

C++11引入了noexcept关键字,用于替代throw(),表示函数不会抛出异常。

void func() noexcept;  // 表示 func 不会抛出异常
  • 如果noexcept函数抛出了异常,程序会调用std::terminate(),终止程序。
  • noexcept还可以接受一个布尔参数,表示函数是否可能抛出异常。
void func() noexcept(true);   // 不会抛出异常
void func() noexcept(false);  // 可能抛出异常

三、 自定义异常体系

在实际项目中,尤其是大型项目中,异常管理是一个非常重要的部分。如果每个开发者都随意抛出异常,外层的调用者将难以处理这些异常,导致代码的可维护性和健壮性下降。因此,很多公司会定义一套自定义的异常体系,通过继承基类异常来统一管理异常类型。这样,所有异常都派生自同一个基类,调用者只需捕获基类异常即可处理所有可能的异常情况。

1. 自定义异常体系的设计

自定义异常体系通常包括一个基类异常和多个派生类异常。基类异常提供通用的异常信息(如错误消息、错误码等),派生类异常则用于表示具体的异常类型。

示例:服务器开发中的异常继承体系

#include <iostream>
#include <string>
using namespace std;

// 基类异常
class Exception {
protected:
    string _errmsg;  // 错误消息
    int _id;         // 错误码
    // list<StackInfo> _traceStack;  // 调用栈信息(可选)
public:
    Exception(const string& errmsg, int id) : _errmsg(errmsg), _id(id) {}

    virtual const string& GetErrorMessage() const {
        return _errmsg;
    }

    virtual int GetErrorId() const {
        return _id;
    }

    virtual ~Exception() {}  // 虚析构函数,确保正确释放派生类对象
};

// 派生类异常:SQL 异常
class SqlException : public Exception {
public:
    SqlException(const string& errmsg, int id) : Exception(errmsg, id) {}
};

// 派生类异常:缓存异常
class CacheException : public Exception {
public:
    CacheException(const string& errmsg, int id) : Exception(errmsg, id) {}
};

// 派生类异常:HTTP 服务器异常
class HttpServerException : public Exception {
public:
    HttpServerException(const string& errmsg, int id) : Exception(errmsg, id) {}
};

2. 异常的使用

在项目中,所有异常都派生自基类Exception。调用者只需捕获基类异常即可处理所有可能的异常情况。

示例:抛出和捕获异常

void StartServer() {
    // 模拟服务器启动过程中抛出异常
    throw HttpServerException("Failed to start HTTP server", 500);
}

int main() {
    try {
        StartServer();  // 可能抛出派生类异常
    } catch (const Exception& e) {  // 捕获基类异常
        cout << "Error: " << e.GetErrorMessage() << endl;
        cout << "Error ID: " << e.GetErrorId() << endl;
    } catch (...) {  // 捕获未知异常
        cout << "Unknown Exception" << endl;
    }
    return 0;
}
Error: Failed to start HTTP server
Error ID: 500

四、标准库异常体系

C++ 标准库提供了一套标准的异常类,定义在 <exception> 头文件中。这些异常类以父子类层次结构组织起来,形成了一个完整的异常体系。标准库异常体系的核心是基类 std::exception,其他异常类都直接或间接继承自它。

1. 标准异常体系的结构

以下是 C++ 标准库异常体系的主要类层次结构:

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::overflow_error
    ├── std::underflow_error
    ├── std::range_error
    └── std::system_error
  • 标准异常体系:C++ 标准库提供了一套完整的异常类层次结构,基类是 std::exception
  • 常见异常类
    • std::bad_alloc:内存分配失败。
    • std::logic_error:程序逻辑错误。
    • std::runtime_error:运行时错误。
  • 自定义异常:可以通过继承标准异常类来实现自定义异常,与标准异常体系兼容。

2. 基类:std::exception

std::exception 是所有标准异常类的基类。它提供了一个虚函数 what(),用于返回异常的描述信息。

成员函数: virtual const char* what() const noexcept;
返回一个描述异常的 C 风格字符串。

#include <iostream>
#include <exception>
using namespace std;

int main() {
    try {
        throw exception();
    } catch (const exception& e) {
        cout << "Exception: " << e.what() << endl;
    }
    return 0;
}
Exception: std::exception

3. 常见标准异常类

(1) std::bad_alloc
  • 用途:当动态内存分配失败时抛出(例如 new 操作失败)。
  • 继承自std::exception
#include <iostream>
#include <new>
using namespace std;

int main() {
    try {
        int* ptr = new int[1000000000000];  // 尝试分配超大内存
    } catch (const bad_alloc& e) {
        cout << "Bad allocation: " << e.what() << endl;
    }
    return 0;
}
Bad allocation: std::bad_alloc
(2) std::logic_error
  • 用途:表示程序逻辑错误,通常是由于程序员的错误导致的。
  • 继承自std::exception
  • 常见派生类
    • std::domain_error:数学函数参数超出定义域。
    • std::invalid_argument:函数接收到无效参数。
    • std::length_error:试图创建超出最大长度的对象。
    • std::out_of_range:访问超出范围的值(例如数组下标越界)。
#include <iostream>
#include <stdexcept>
using namespace std;

int main() {
    try {
        throw invalid_argument("Invalid argument!");
    } catch (const logic_error& e) {
        cout << "Logic error: " << e.what() << endl;
    }
    return 0;
}
Logic error: Invalid argument!
(3) std::runtime_error
  • 用途:表示运行时错误,通常是由于外部因素导致的。
  • 继承自std::exception
  • 常见派生类
    • std::overflow_error:算术运算溢出。
    • std::underflow_error:算术运算下溢。
    • std::range_error:计算结果超出有效范围。
    • std::system_error:系统相关的错误。
#include <iostream>
#include <stdexcept>
using namespace std;

int main() {
    try {
        throw runtime_error("Runtime error!");
    } catch (const runtime_error& e) {
        cout << "Runtime error: " << e.what() << endl;
    }
    return 0;
}
Runtime error: Runtime error!

4. 自定义异常与标准异常的结合

在实际项目中,可以将自定义异常类继承自标准异常类(如 std::exception 或其派生类),以便与标准异常体系兼容。

#include <iostream>
#include <exception>
using namespace std;

// 自定义异常类,继承自 std::exception
class MyException : public exception {
public:
    const char* what() const noexcept override {
        return "My custom exception!";
    }
};

int main() {
    try {
        throw MyException();
    } catch (const exception& e) {
        cout << "Caught exception: " << e.what() << endl;
    }
    return 0;
}
Caught exception: My custom exception!

五、异常的优缺点

5.1 C++ 异常的优点

  1. 清晰的错误信息
    异常对象可以携带丰富的错误信息,包括错误类型、错误描述、调用栈信息等。相比传统的错误码方式,异常能够更清晰、更准确地展示错误信息,帮助开发者快速定位和修复问题。

    try {
        throw runtime_error("File not found!");
    } catch (const exception& e) {
        cout << "Error: " << e.what() << endl;  // 输出错误信息
    }
    
    Error: File not found!
    
  2. 简化错误处理逻辑
    在传统的错误码方式中,深层的函数返回错误码时,调用链中的每一层都需要检查并传递错误码,直到最外层才能处理错误。这种方式会导致代码冗余且难以维护。
    传统错误码方式:

    int ConnectSql() {
        if (/* 用户名密码错误 */) return 1;
        if (/* 权限不足 */) return 2;
        return 0;  // 成功
    }
    
    int ServerStart() {
        int ret = ConnectSql();
        if (ret < 0) return ret;  // 传递错误码
        int fd = socket();
        if (fd < 0) return errno;  // 传递错误码
        return 0;
    }
    
    int main() {
        if (ServerStart() < 0) {
            // 处理错误
        }
        return 0;
    }
    

    异常方式:

    void ConnectSql() {
        if (/* 用户名密码错误 */) throw runtime_error("Invalid username or password");
        if (/* 权限不足 */) throw runtime_error("Permission denied");
    }
    
    void ServerStart() {
        ConnectSql();  // 无需检查返回值
        int fd = socket();
        if (fd < 0) throw runtime_error("Socket creation failed");
    }
    
    int main() {
        try {
            ServerStart();
        } catch (const exception& e) {
            cout << "Error: " << e.what() << endl;  // 直接处理错误
        }
        return 0;
    }
    
  3. 与第三方库兼容
    许多第三方库(如 Boost、gtest、gmock)使用异常机制。如果项目中需要使用这些库,通常也需要使用异常。

  4. 便于单元测试
    异常机制非常适合单元测试和白盒测试。测试框架可以通过捕获异常来验证代码的正确性和鲁棒性。

  5. 适用于特殊场景
    某些场景下,异常是唯一的选择:

    • 构造函数:构造函数没有返回值,无法通过返回值表示错误。

    • 运算符重载:例如 T& operator[],如果索引越界,只能通过异常或终止程序来处理。

      class Vector {
      public:
          int& operator[](size_t pos) {
              if (pos >= size) throw out_of_range("Index out of range");
              return data[pos];
          }
      private:
          int* data;
          size_t size;
      };
      

5.2 C++ 异常的缺点

  1. 执行流混乱
    异常会导致程序的执行流在运行时突然跳转,这会给调试和代码分析带来困难。开发者需要仔细跟踪异常抛出的路径,以确保程序的正确性。

  2. 性能开销
    异常机制会带来一定的性能开销,尤其是在异常抛出和捕获时。虽然现代硬件的性能已经足够强大,这种开销通常可以忽略不计,但在性能敏感的系统中仍需谨慎使用。

  3. 资源管理困难
    C++ 没有垃圾回收机制,资源需要手动管理。异常可能导致资源泄漏(如内存泄漏、文件句柄未关闭)或死锁等问题。为了解决这个问题,必须使用 RAII(资源获取即初始化)技术来管理资源。

    void unsafeFunction() {
        int* ptr = new int[100];
        throw runtime_error("Error occurred");  // 内存泄漏
        delete[] ptr;  // 不会执行
    }
    
    void safeFunction() {
        unique_ptr<int[]> ptr = make_unique<int[]>(100);  // RAII
        throw runtime_error("Error occurred");  // 内存自动释放
    }
    
  4. 标准库异常体系不完善
    C++ 标准库的异常体系设计得不够完善,导致许多项目都自定义自己的异常体系。这种混乱使得不同项目之间的异常处理方式不一致,增加了学习和使用的成本。

  5. 需要规范使用
    异常机制如果使用不当,会导致代码难以维护。例如:

    • 随意抛出异常,导致外层调用者难以处理。
    • 异常类型不统一,增加了捕获和处理的复杂性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

XiYang077

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值