59、C++ 异常处理与类模板详解

C++ 异常处理与类模板详解

1. 异常类定义

在 C++ 中,标准异常类可作为自定义异常类的基类。所有标准异常类都以 exception 为基类,因此了解 exception 类的成员很有必要,因为这些成员会被其他异常类继承。 exception 类在 <exception> 头文件中定义如下:

class exception
{
public:
  exception() noexcept;                               // 默认构造函数
  exception(const exception&) noexcept;               // 拷贝构造函数
  exception& operator=(const exception&) noexcept;    // 赋值运算符
  virtual ~exception();                               // 析构函数
  virtual const char* what() const noexcept;          // 返回消息字符串
};

上述代码是 exception 类的公共接口规范,具体实现可能包含额外的非公共成员。其他标准异常类也是如此。 noexcept 表明这些成员函数不会抛出异常,析构函数默认是 noexcept 的。注意,该类没有数据成员。 what() 函数返回的以空字符结尾的字符串在函数定义体中定义,且依赖于具体实现。此函数被声明为虚函数,因此在 exception 的派生类中也是虚函数。

一个接收基类参数的 catch 块可以匹配任何派生类异常类型。例如,使用 exception& 类型的参数可以捕获任何标准异常。也可以使用 logic_error& runtime_error& 类型的参数来捕获从这些类派生的异常。下面是一个在 main() 函数中使用函数 try 块和 catch 块捕获异常的示例:

int main()
try
{
  // main 函数的代码...
}
catch(exception& ex)
{
  std::cout << typeid(ex).name() << " caught in main: " << ex.what() << std::endl;
}

这个 catch 块会捕获所有以 exception 为基类的异常,并输出异常类型和 what() 函数返回的消息。

2. logic_error 和 runtime_error 类

logic_error runtime_error 类在从 exception 类继承的成员基础上,各自只添加了两个构造函数。例如, logic_error 类的定义如下:

class logic_error : public exception
{
public:
  explicit logic_error(const string& what_arg);
  explicit logic_error(const char* what_arg);
};

runtime_error 的定义类似,除 system_error 外的所有子类也都有接受字符串或 const char* 参数的构造函数。 system_error 类添加了一个 std::error_code 类型的数据成员来记录错误代码,其构造函数用于指定错误代码。

3. 使用标准异常

在代码中使用标准库定义的异常类有诸多好处。可以通过两种方式使用标准异常类型:一是在代码中抛出标准类型的异常,二是将标准异常类作为自定义异常类型的基类。如果要抛出标准异常,应确保在符合其用途的情况下抛出。例如,不应随意抛出 bad_cast 异常,因为它有特定的用途。但可以直接使用从 logic_error runtime_error 派生的一些异常类。以下是在 Box 类构造函数中抛出 range_error 异常的示例:

Box::Box(double lv, double wv, double hv) : length {lv}, width {wv}, height {hv}
{
  if(lv <= 0.0 || wv <= 0.0 || hv <= 0.0)
    throw std::range_error("Zero or negative Box dimension.");
}

当然,源文件需要包含定义 range_error 类的 <stdexcept> 头文件。如果构造函数的任何参数为零或负数,就会抛出 range_error 异常。

4. 派生自定义异常类

从标准异常类派生自定义类的一个重要优点是,自定义类与标准异常类属于同一异常家族,这使得可以在同一个 catch 块中捕获标准异常和自定义异常。例如,如果自定义异常类从 logic_error 派生,那么使用 logic_error& 类型参数的 catch 块可以捕获自定义异常和以 logic_error 为基类的标准异常。使用 exception& 类型参数的 catch 块可以捕获所有以 exception 为基类的标准异常和自定义异常。

可以将 Trouble 异常类及其派生类简单地融入标准异常家族,只需从 exception 类派生 Trouble 类,修改后的类定义如下:

class Trouble : public std::exception
{
public:
  Trouble(const char* pStr = "There's a problem") noexcept;
  virtual ~Trouble();
  virtual const char* what() const noexcept;

private:
  const char* message;
};

该类提供了基类中虚函数 what() 的自定义实现,用于显示类对象的消息。同时,为每个成员函数添加了异常规范,确保它们不会抛出异常。对于从 Trouble 派生的 MoreTrouble BigTrouble 类的成员函数,也需要进行类似的更新。

还可以从 std::range_error 派生一个异常类,以便 what() 函数返回更具体的字符串,用于标识引发异常的问题。以下是 dimension_error 类的定义:

#ifndef DIMENSION_ERROR_H
#define DIMENSION_ERROR_H
#include<stdexcept>                         // 用于派生异常类
#include <string>;                          // 用于字符串类型
using std::string;
using std::range_error;

class dimension_error : public range_error
{
public:
  using range_error::range_error;           // 继承基类构造函数

  dimension_error(std::string str, int dim) :
                                  std::range_error {str + std::to_string(dim)} {}
};
#endif

using 指令使类继承基类的构造函数,因此 dimension_error 类包含以下两个构造函数:

explicit dimension_error( const std::string& what_arg): std::range_error {what_arg} {}
explicit dimension_error( const char* what_arg): std::range_error {what_arg} {}

这允许以与基类对象相同的方式创建 dimension_error 对象。额外的构造函数提供了一个额外的参数,用于指定引发异常的维度值。它将第二个构造函数参数 dim 的字符串表示形式附加到第一个参数的字符串对象上,形成一个新的字符串对象,并调用基类构造函数。

以下是在 Box 类定义中使用 dimension_error 类的示例:

// Box.h
#ifndef BOX_H
#define BOX_H
#include <algorithm>                        // 用于 min() 函数模板
#include "Dimension_error.h"

class Box
{
protected:
  double length {1.0};
  double width {1.0};
  double height {1.0};

public:
  Box(double lv, double wv, double hv) : length {lv}, width {wv}, height {hv}
  {
    if(lv <= 0.0 || wv <= 0.0 || hv <= 0.0)
      throw dimension_error {"Zero or negative Box dimension.", std::min(lv, std::min(wv, hv))};
  }

  double volume() const { return length*width*height; }
};
#endif

如果 Box 类构造函数的任何参数为零或负数,将抛出 dimension_error 异常。构造函数使用 <algorithm> 头文件中的 min() 模板函数来确定最小的维度参数。

以下是一个演示 dimension_error 类使用的示例代码:

// Ex15_08.cpp
// 使用异常类
#include <iostream>
#include "Box.h"                            // 包含 Box 类
#include "Dimension_error.h"                // 包含 dimension_error 类

int main()
try
{
  Box box1 {1.0, 2.0, 3.0};
  std::cout << "box1 volume is " << box1.volume() << std::endl;
  Box box2 {1.0, -2.0, 3.0};
  std::cout << "box1 volume is " << box2.volume() << std::endl;
}
catch (std::exception& ex)
{
  std::cout << "Exception caught in main(): " << ex.what() << std::endl;
}

该示例的输出如下:

box1 volume is 6
Exception caught in main(): Zero or negative Box dimension.-2.000000

main() 函数的主体是一个 try 块,其 catch 块会捕获所有以 std::exception 为基类的异常。输出显示,当维度为负数时, Box 类构造函数会抛出 dimension_error 异常对象,并且 dimension_error 类从 range_error 继承的 what() 函数会输出在构造函数调用中形成的字符串。

5. 异常处理总结

异常是 C++ 编程的重要组成部分,许多运算符会抛出异常,标准库也广泛使用异常来表示错误。因此,即使不打算定义自己的异常类,也需要很好地理解异常的工作原理。以下是异常处理的重要要点总结:
- 异常是用于在程序中发出错误信号的对象。
- 可能抛出异常的代码通常包含在 try 块中,以便在程序中检测和处理异常。
- 处理 try 块中可能抛出的异常的代码放在一个或多个紧跟 try 块的 catch 块中。
- try 块及其 catch 块可以嵌套在另一个 try 块中。
- 接收基类类型参数的 catch 块可以捕获派生类类型的异常。
- 参数指定为省略号的 catch 块可以捕获任何类型的异常。
- 如果异常没有被任何 catch 块捕获,则调用 std::terminate() 函数,该函数会调用 std::abort()
- 标准库在 <stdexcept> 头文件中定义了一系列标准异常类型,这些类型派生自 <exception> 头文件中定义的 std::exception 类。
- 函数的 noexcept 规范表明该函数不会抛出异常。
- 构造函数的函数 try 块可以包含初始化列表和构造函数体。
- uncaught_exception() 函数允许检测析构函数是否因异常抛出而被调用。

6. 异常处理练习

以下是一些练习,帮助巩固所学的异常处理知识:
1. 从 std::exception 类派生一个名为 CurveBall 的自定义异常类,用于表示任意错误。编写一个函数,该函数大约 25% 的时间抛出此异常(可以通过生成 1 到 20 之间的随机数,如果数字为 5 或更小,则抛出异常)。定义一个 main() 函数,调用该函数 1000 次,并记录和显示抛出异常的次数。
2. 定义另一个异常类 TooManyExceptions 。当在练习 1 的 CurveBall 异常的 catch 块中捕获的异常数量超过 10 时,抛出此类型的异常。
3. 在前一个示例的代码中实现终止处理程序,以便在抛出 TooManyExceptions 异常时显示一条消息。
4. 稀疏数组是一种大多数元素值为零或为空的数组。定义一个用于存储 double 类型元素的一维稀疏数组类,只存储非零元素。潜在元素数量应作为构造函数参数指定,例如可以使用 SparseArray values {100}; 定义一个最多存储 100 个元素的稀疏数组。为 SparseArray 类实现下标运算符,以便可以使用数组表示法检索或存储元素。如果下标运算符函数中超出合法索引范围,则抛出一个标识错误下标的异常。创建一个示例,演示 SparseArray 类的操作,包括捕获和处理下标运算符函数抛出的异常。

下面是异常处理流程的 mermaid 流程图:

graph TD;
    A[开始] --> B[执行可能抛出异常的代码];
    B --> C{是否抛出异常};
    C -- 是 --> D[跳转到匹配的 catch 块];
    D --> E[处理异常];
    E --> F[继续执行后续代码];
    C -- 否 --> F;
    F --> G[结束];
7. 类模板概述

类模板是编译器用于创建类的模板,是生成新类类型的强大机制。标准库的很大一部分是基于模板定义构建的,特别是标准模板库,它包含许多类模板和函数模板。

8. 理解类模板

类模板与函数模板的思想相同,是一种参数化类型,是创建一类类类型的“配方”,使用一个或多个参数。每个参数的参数通常(但不总是)是一个类型。当定义一个具有类模板指定类型的变量时,编译器会使用模板和类型规范中提供的模板参数来创建类的定义。可以使用类模板生成任意数量的不同类。需要注意的是,类模板本身不是类,只是创建类的“配方”,这也是定义类模板存在许多限制的原因。

类模板有一个名称,就像普通类一样,并且有一个或多个参数。类模板在命名空间内必须是唯一的,即在定义模板的命名空间中不能有另一个具有相同名称和参数列表的模板。当为模板的每个参数提供参数时,会从类模板生成一个类定义。

编译器从模板生成的每个类称为模板的实例。首次使用模板类型定义变量时,会创建模板的一个实例;后续定义的相同类型的变量将使用首次创建的实例。也可以在不定义变量的情况下创建类模板的实例。如果模板未用于生成类,编译器不会对源文件中的类模板进行任何处理。

类模板有很多应用,最常见的是用于定义容器类,即可以包含一组特定类型对象并以特定方式组织的类。在容器类中,数据的组织方式与存储的对象类型无关。例如,已经有使用 std::vector std::array 类模板定义顺序组织数据的容器的经验。

9. 定义类模板

类模板的定义看起来可能比实际更复杂,主要是因为定义它们的符号和定义语句中散布的参数。类模板的定义与普通类类似,但细节决定成败。类模板以 template 关键字开头,后面是尖括号内的模板参数列表。模板类定义以 class 关键字开头,后面是类模板名称,定义体放在花括号内。与普通类一样,整个定义以分号结尾。类模板的一般形式如下:

template <template parameter list>
class ClassName
{
  // 模板类定义...
};

ClassName 是模板的名称。编写模板体的代码与编写普通类的代码类似,只是一些成员声明和定义将根据尖括号内的模板参数来编写。要从模板创建类,必须为列表中的每个参数指定参数,这与函数模板不同,函数模板大多数情况下编译器可以从上下文推断模板参数。

10. 模板参数

模板参数列表可以包含任意数量的参数,这些参数可以分为两种类型:类型参数和非类型参数。与类型参数对应的参数始终是一个类型,如 int std::string Box ;非类型参数的参数可以是整数类型的字面量(如 200)、整数常量表达式、对象的指针或引用、函数指针或空指针。类型参数比非类型参数更常用,因此先介绍类型参数,非类型参数的讨论将在后面进行。

还有第三种可能的类模板参数,即参数本身可以是一个模板,其参数必须是类模板的实例。由于这部分内容比较高级,本书不做详细讨论。

类型参数可以使用 class 关键字或 typename 关键字在参数名称之前声明(例如 typename T )。建议使用 typename ,因为 class 往往暗示类类型,而类型参数不一定是类类型。 T 通常用作类型参数名称(如果有多个类型参数,则使用 T1 T2 等),因为它简洁且在模板定义中易于识别,但也可以使用任何想要的名称。

下面是类模板参数类型的表格总结:
| 参数类型 | 描述 | 示例 |
| ---- | ---- | ---- |
| 类型参数 | 参数为类型 | int, std::string, Box |
| 非类型参数 | 可以是整数类型字面量、整数常量表达式、对象指针或引用、函数指针或空指针 | 200, 指针变量 |

以下是类模板实例化流程的 mermaid 流程图:

graph TD;
    A[定义类模板] --> B[提供模板参数];
    B --> C[编译器生成类定义];
    C --> D[创建类的实例];
    D --> E[使用类的实例];

通过以上内容,我们对 C++ 中的异常处理和类模板有了较为全面的了解。异常处理帮助我们更好地处理程序中的错误,而类模板则为我们提供了一种强大的机制来生成新的类类型。在实际编程中,合理运用异常处理和类模板可以提高代码的健壮性和可维护性。

C++ 异常处理与类模板详解(续)

11. 类型参数与非类型参数

类型参数在类模板中更为常见,它允许我们创建可以处理不同数据类型的类。例如,在标准库中的 std::vector 就是一个使用类型参数的类模板,它可以存储不同类型的元素,如 std::vector<int> 存储整数, std::vector<std::string> 存储字符串。

而非类型参数则提供了更多的灵活性。非类型参数可以是整数类型的常量、指针、引用等。以下是一个使用非类型参数的简单类模板示例:

template <int N>
class Array {
private:
    int data[N];
public:
    int& operator[](int index) {
        return data[index];
    }
    const int& operator[](int index) const {
        return data[index];
    }
    int size() const {
        return N;
    }
};

在这个示例中, N 是一个非类型参数,它指定了数组的大小。使用时可以这样创建对象:

Array<5> arr;

这里创建了一个大小为 5 的整数数组。

12. 静态成员的初始化

类模板的静态成员需要特殊的初始化方式。静态成员属于类模板的所有实例,而不是某个特定的实例。以下是一个包含静态成员的类模板示例:

template <typename T>
class MyClass {
public:
    static int count;
    MyClass() {
        count++;
    }
};

template <typename T>
int MyClass<T>::count = 0;

在这个示例中, count 是一个静态成员,用于记录创建的对象数量。注意,静态成员的初始化需要在类模板外部进行,并且要指定模板参数。

13. 类模板的部分特化

类模板的部分特化允许我们为模板的某些特定情况提供特殊的实现。部分特化只针对模板参数的一部分进行特化。以下是一个简单的部分特化示例:

template <typename T, typename U>
class Pair {
public:
    Pair(T t, U u) : first(t), second(u) {}
    T first;
    U second;
};

template <typename T>
class Pair<T, int> {
public:
    Pair(T t, int u) : first(t), second(u) {
        // 特殊处理
    }
    T first;
    int second;
};

在这个示例中, Pair 类模板有两个类型参数 T U 。部分特化版本针对 U int 的情况提供了特殊的实现。

14. 嵌套类

类模板中可以嵌套其他类。嵌套类可以访问外部类模板的成员,并且可以使用外部类模板的模板参数。以下是一个包含嵌套类的类模板示例:

template <typename T>
class Outer {
public:
    class Inner {
    public:
        void print(T value) {
            std::cout << "Value: " << value << std::endl;
        }
    };
};

使用时可以这样创建嵌套类的对象:

Outer<int>::Inner inner;
inner.print(10);
15. 类模板总结

类模板是 C++ 中非常强大的特性,它为我们提供了一种灵活的方式来创建通用的类。通过使用类模板,我们可以编写可复用性更高的代码,减少代码的重复。以下是类模板的一些重要特点总结:
- 类模板是参数化类型,用于创建一类类类型。
- 模板参数可以是类型参数或非类型参数。
- 静态成员需要在类模板外部进行初始化。
- 可以对类模板进行部分特化,为特定情况提供特殊实现。
- 类模板中可以嵌套其他类。

下面是类模板使用流程的 mermaid 流程图:

graph TD;
    A[定义类模板] --> B[确定模板参数类型];
    B --> C{参数类型};
    C -- 类型参数 --> D[指定具体类型];
    C -- 非类型参数 --> E[提供常量值];
    D --> F[编译器生成类];
    E --> F;
    F --> G[创建类的对象];
    G --> H[使用对象的成员];
16. 综合应用建议

在实际编程中,异常处理和类模板可以结合使用,以提高代码的健壮性和可维护性。例如,在类模板的成员函数中,可以使用异常处理来处理可能出现的错误。以下是一个结合异常处理和类模板的示例:

template <typename T>
class SafeArray {
private:
    T* data;
    int size;
public:
    SafeArray(int s) : size(s) {
        data = new T[size];
    }
    ~SafeArray() {
        delete[] data;
    }
    T& operator[](int index) {
        if (index < 0 || index >= size) {
            throw std::out_of_range("Index out of range");
        }
        return data[index];
    }
};

在这个示例中, SafeArray 是一个类模板,用于存储数组。 operator[] 函数中使用了异常处理,如果索引超出范围,将抛出 std::out_of_range 异常。

通过合理运用异常处理和类模板,我们可以编写出更加健壮、高效和可维护的 C++ 代码。在面对复杂的编程任务时,这些技术可以帮助我们更好地组织代码,提高开发效率。

希望通过以上内容,你对 C++ 中的异常处理和类模板有了更深入的理解,并且能够在实际编程中灵活运用它们。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值