60、C++ 类模板:从基础到实践

C++ 类模板:从基础到实践

1. 简单类模板

在 C++ 中,类模板是一种强大的工具,它允许我们创建通用的类定义,这些类可以处理不同的数据类型。下面以一个数组类模板为例,该模板会对索引值进行边界检查,以确保其合法性。

template <typename T>
class Array
{
private:
  T* elements;                              // Array of type T
  size_t size;                              // Number of array elements

public:
  explicit Array(size_t arraySize);         // Constructor
  Array(const Array& array);                // Copy Constructor
  ~Array();                                 // Destructor
  T& operator[](size_t index);              // Subscript operator
  const T& operator[](size_t index) const;  // Subscript operator-const arrays
  Array& operator=(const Array& rhs);       // Assignment operator
  size_t getSize() { return size; }         // Accessor for size
};

在这个模板中, T 是一个类型参数,它代表数组元素的类型。当我们实例化这个模板时, T 会被具体的类型所替代,比如 int double* string 等。

2. 类模板的函数成员定义

如果将类模板的函数成员定义放在模板体内,它们在模板的任何实例中都将隐式内联,就像普通类一样。但有时我们希望将成员定义在模板体外,尤其是当它们包含大量代码时。

2.1 构造函数模板

构造函数在模板体外定义时,其名称必须由类模板名称限定。以下是构造函数的定义:

template <typename T>
Array<T>::Array(size_t arraySize) : size {arraySize}, elements {new T[arraySize]}
{}

为了处理内存分配失败的情况,我们可以将构造函数的主体放在 try-catch 块中:

template <typename T>
Array<T>::Array(size_t arraySize) try : size {arraySize}, elements {new T[arraySize]}
{}
catch (std::bad_alloc& )
{
  std::cerr << "memory allocation failed for Array object.\n";
}

复制构造函数需要创建一个与参数对象大小相同的数组,并将参数对象的数据成员复制到新对象中:

template <typename T>
inline Array<T>::Array(const Array& array)
try : size {array.size}, elements {new T[array.size]}
{
  for (size_t i {}; i < size; ++i)
    elements[i] = array.elements[i];
}
catch (std::bad_alloc&)
{
  std::cerr << "memory allocation failed for Array object copy.\n";
}
2.2 析构函数模板

析构函数必须释放元素数组的内存:

template <typename T> inline Array<T>::~Array()
{
  delete[] elements;
}
2.3 下标运算符模板

下标运算符函数需要确保不能使用非法的索引值。如果索引值超出范围,我们可以抛出一个异常:

template <typename T> inline T& Array<T>::operator[](size_t index)
{
  if (index >=size) throw std::out_of_range {"Index too large: " + std::to_string(index)};

  return elements[index];
}

template <typename T> inline const T& Array<T>::operator[](size_t index) const
{
  if (index >=size) throw std::out_of_range {"Index too large: " + std::to_string(index)};

  return elements[index];
}
2.4 赋值运算符模板

赋值运算符允许将一个 Array<T> 对象赋值给另一个。为了简单起见,我们让左操作数的大小与右操作数相同:

template <typename T> inline Array<T>& Array<T>::operator=(const Array& rhs)
try
{
  if (&rhs != this)                         // If lhs != rhs...
  {                                         // ...do the assignment...
    if (elements)                           // If lhs array exists
      delete[] elements;                    // release the free store memory

    size = rhs.size;
    elements = new T[rhs.size];
    for (size_t i {}; i < size; ++i)
      elements[i] = rhs.elements[i];
  }
  return *this;                             // ... return lhs
}
catch (std::bad_alloc&)
{
  std::cerr << "memory allocation failed for Array assignment.\n";
}
3. 类模板的实例化

编译器会根据使用模板生成的对象类型定义来实例化类模板。例如:

Array<int> data {40};

当编译这个语句时,会发生两件事:创建 Array<int> 类的定义,以及生成构造函数的定义。

需要注意的是,编译器只会编译程序中使用的成员函数,因此最终生成的类可能不是模板参数简单替换后的完整类。

以下是一个使用类模板的完整示例:

// Array.h
#ifndef ARRAY_H
#define ARRAY_H
#include <stdexcept>                        // For standard exception types
#include <string>                           // For to_string()

template <typename T>
class Array
{
private:
  T* elements;                              // Array of type T
  size_t size;                              // Number of array elements

public:
  explicit Array(size_t arraySize);         // Constructor
  Array(const Array& array);                // Copy Constructor
  ~Array();                                 // Destructor
  T& operator[](size_t index);              // Subscript operator
  const T& operator[](size_t index) const;  // Subscript operator-const arrays
  Array& operator=(const Array& rhs);       // Assignment operator
  size_t getSize() { return size; }         // Accessor for size
};

template <typename T>
Array<T>::Array(size_t arraySize) try : size {arraySize}, elements {new T[arraySize]}
{}
catch (std::bad_alloc& )
{
  std::cerr << "memory allocation failed for Array object.\n";
}

template <typename T>
inline Array<T>::Array(const Array& array)
try : size {array.size}, elements {new T[array.size]}
{
  for (size_t i {}; i < size; ++i)
    elements[i] = array.elements[i];
}
catch (std::bad_alloc&)
{
  std::cerr << "memory allocation failed for Array object copy.\n";
}

template <typename T> inline Array<T>::~Array()
{
  delete[] elements;
}

template <typename T> inline T& Array<T>::operator[](size_t index)
{
  if (index >=size) throw std::out_of_range {"Index too large: " + std::to_string(index)};

  return elements[index];
}

template <typename T> inline const T& Array<T>::operator[](size_t index) const
{
  if (index >=size) throw std::out_of_range {"Index too large: " + std::to_string(index)};

  return elements[index];
}

template <typename T> inline Array<T>& Array<T>::operator=(const Array& rhs)
try
{
  if (&rhs != this)                         // If lhs != rhs...
  {                                         // ...do the assignment...
    if (elements)                           // If lhs array exists
      delete[] elements;                    // release the free store memory

    size = rhs.size;
    elements = new T[rhs.size];
    for (size_t i {}; i < size; ++i)
      elements[i] = rhs.elements[i];
  }
  return *this;                             // ... return lhs
}
catch (std::bad_alloc&)
{
  std::cerr << "memory allocation failed for Array assignment.\n";
}

#endif

// Box.h
#ifndef BOX_H
#define BOX_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} {}
  Box() = default;

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

// Ex16_01.cpp
#include "Box.h"
#include "Array.h"
#include <iostream>
#include <iomanip>

int main()
{
  const size_t nValues {50};
  Array<double> values {nValues};                // Class constructor instance created
  try
  {
    for (size_t i {}; i < nValues; ++i)
      values[i] = i + 1;                         // Member function instance created

    std::cout << "Sums of pairs of elements:";
    size_t lines {};
    for (size_t i {nValues - 1} ; i >=0 ; i--)
      std::cout << (lines++ % 5 == 0 ? "\n" : "")
      << std::setw(5) << values[i] + values[i - 1];
  }
  catch (const std::out_of_range& ex)
  {
    std::cerr << "\nout_of_range exception object caught! " << ex.what() << std::endl;
  }

  try
  {
    const size_t nBoxes {10};
    Array<Box> boxes {nBoxes};                   // Template instance created
    for (size_t i {} ; i <= nBoxes ; ++i)        // Member instance created in loop
      std::cout << "Box volume is " << boxes[i].volume() << std::endl;
  }
  catch (const std::out_of_range& ex)
  {
    std::cerr << "\nout_of_range exception object caught! " << ex.what() << std::endl;
  }
}
4. 总结
  • 类模板允许我们创建通用的类定义,提高代码的复用性。
  • 函数成员可以在模板体内或体外定义,体外定义时需要遵循特定的语法。
  • 实例化类模板时,编译器会根据程序的使用情况生成相应的代码。

以下是一个简单的流程图,展示了类模板实例化的过程:

graph TD;
    A[定义对象类型] --> B[创建类定义];
    B --> C[生成构造函数定义];
    D[使用成员函数] --> E[生成成员函数代码];

在测试自己的类模板时,要确保所有函数成员都被生成和测试,并且要考虑模板在不同类型上的表现。

C++ 类模板:从基础到实践

5. 类模板实例化的注意事项

类模板的实例化过程中有一些需要特别注意的地方。

首先,类模板只有在需要创建特定模板类型的对象时才会被隐式实例化。仅仅声明一个指向对象类型的指针不会导致模板实例化。例如:

Array<string>* pObject; 

上述代码只是定义了一个指向 Array<string> 类型的指针,并没有创建 Array<string> 类型的对象,因此不会创建模板实例。而下面的代码则会触发模板实例化:

Array<std::string*> pMessages {10};

这里定义了一个 Array<std::string*> 对象,编译器会创建类模板的实例,并生成相应的构造函数定义。

另外,每次使用不同的类型参数定义变量时,都会定义一个新的类并包含在程序中。但如果创建之前已经创建过的类型的对象,则不需要新的模板实例,编译器会使用之前创建的实例。

6. 类模板的更多应用示例

下面通过一个更复杂的示例来展示类模板的应用。假设我们要创建一个 Stack 类模板,用于实现栈的基本操作。

// Stack.h
#ifndef STACK_H
#define STACK_H
#include <stdexcept>
#include <vector>

template <typename T>
class Stack
{
private:
    std::vector<T> elements;

public:
    void push(const T& value);
    void pop();
    T top() const;
    bool empty() const;
};

template <typename T>
void Stack<T>::push(const T& value)
{
    elements.push_back(value);
}

template <typename T>
void Stack<T>::pop()
{
    if (empty())
        throw std::out_of_range("Stack is empty");
    elements.pop_back();
}

template <typename T>
T Stack<T>::top() const
{
    if (empty())
        throw std::out_of_range("Stack is empty");
    return elements.back();
}

template <typename T>
bool Stack<T>::empty() const
{
    return elements.empty();
}

#endif
// main.cpp
#include "Stack.h"
#include <iostream>

int main()
{
    Stack<int> intStack;
    intStack.push(10);
    intStack.push(20);
    std::cout << "Top element: " << intStack.top() << std::endl;
    intStack.pop();
    std::cout << "Top element after pop: " << intStack.top() << std::endl;
    return 0;
}

在这个示例中,我们定义了一个 Stack 类模板,它使用 std::vector 来存储元素。通过模板,我们可以创建不同类型的栈,如 Stack<int> Stack<double> 等。

7. 类模板与异常处理

在类模板的实现中,异常处理是非常重要的。前面的数组类模板和栈类模板都使用了异常处理来处理可能出现的错误情况,如内存分配失败、越界访问等。

以下是一个表格,总结了常见的异常情况及处理方式:
| 异常情况 | 处理方式 | 示例代码 |
| ---- | ---- | ---- |
| 内存分配失败 | 捕获 std::bad_alloc 异常并输出错误信息 |

template <typename T>
Array<T>::Array(size_t arraySize) try : size {arraySize}, elements {new T[arraySize]}
{}
catch (std::bad_alloc& )
{
    std::cerr << "memory allocation failed for Array object.\n";
}
``` |
| 越界访问 | 抛出 `std::out_of_range` 异常并附带错误信息 |
```cpp
template <typename T> inline T& Array<T>::operator[](size_t index)
{
    if (index >=size) throw std::out_of_range {"Index too large: " + std::to_string(index)};
    return elements[index];
}
``` |

#### 8. 总结与展望
类模板是 C++ 中非常强大的特性,它允许我们编写通用的代码,提高代码的复用性和可维护性。通过本文的介绍,我们了解了类模板的基本定义、函数成员的定义、实例化过程以及异常处理等方面的知识。

在实际应用中,我们可以根据具体需求创建各种类模板,如容器类模板、算法类模板等。同时,要注意类模板的实例化规则和异常处理,确保代码的健壮性。

未来,随着 C++ 标准的不断发展,类模板的功能可能会更加丰富和强大。我们可以期待更多的模板元编程技术和优化,进一步提升代码的性能和灵活性。

以下是一个流程图,展示了类模板使用的整体流程:
```mermaid
graph TD;
    A[定义类模板] --> B[包含模板头文件];
    B --> C[实例化类模板创建对象];
    C --> D[使用对象成员函数];
    D --> E{是否出现异常};
    E -- 是 --> F[捕获并处理异常];
    E -- 否 --> G[继续使用对象];

总之,掌握类模板的使用对于 C++ 开发者来说是非常重要的,希望本文能帮助你更好地理解和应用类模板。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值