C++基础:Stanford CS106L学习笔记 9 类模板(Class Templates)

目录

      • [9.1 类模板声明](#9.1 类模板声明)
      • [9.2 实现类模板](#9.2 实现类模板)
      • [9.3 类模板的"怪癖"](#9.3 类模板的"怪癖")
        • [9.3.1 **`typename`**​**vs.**​**`class`**](#9.3.1 typenamevs.class)
        • [9.3.2 默认参数](#9.3.2 默认参数)
        • [9.3.3 无类型参数](#9.3.3 无类型参数)
      • [9.4 const 正确性](#9.4 const 正确性)
        • [9.4.1 const方法](#9.4.1 const方法)
        • [9.4.2 const接口](#9.4.2 const接口)
        • [9.4.3 const重载](#9.4.3 const重载)
        • [9.4.4 const\_cast](#9.4.4 const_cast)
        • [9.4.5 mutable](#9.4.5 mutable)

编写一个int_vector:

cpp 复制代码
class int_vector {
public:
  void push_back(int i);
  size_t size() const;
  int& operator[](size_t index) const;

  /* Other methods and implementation hidden */
};

double_vector将包含与int_vectorstring_vector非常相似的逻辑(唯一显著的区别在于数据类型的变化)。这不仅使得编写工作繁琐,而且如果类接口需要更改,维护起来也很困难。

模板 为这个问题提供了一种解决方案。我们将使用一个或多个模板参数为我们的类声明一个模板,这些参数在我们使用该模板时会被替换为特定的类型。

模板:保持逻辑,改变类型。

9.1 类模板声明

我们可以将我们的int_vectordouble_vectorstring_vector合并成一个单一的vector类模板,如下所示:

cpp 复制代码
template <typename T>    // vector 是一个模板,它接收一个类型 T 的名称。
class vector {
public:
  void push_back(const T& v);    // vector被实例化时,T被替换
  size_t size() const;
  T& operator[](size_t index) const;

  /* Other methods and implementation hidden */
};

这个模板看起来几乎和普通类完全一样,只是我们在它前面加上了template 。这种语法引入了一个单一的模板参数T,它代表一个类型名称。然后,当我们编写vector<int> v;编译器会通过将每个T的实例都文本替换为int实例化 该模板,从而生成一个类声明。对于给定的模板参数配置,实例化只会发生一次 ------ 如果我们在同一个文件中再次使用vector,则会重用已实例化的模板。如果我们编写

vector<int> v1;

vector<double> v2;

vector<std::string> v3;

编译器会生成三个不同的模板,形成三个完全不同类型。

它们的​运行时和编译时类型完全不同 ​。(Java不是,Java中ArrayList<int>ArrayList<double>分享​相同的运行时类型​)

模板VS类型

最终结果与我们手动写出一个int_vectordouble_vectorstring_vector的结果相同。优点如下:

  • 编译器自动生成这些类的能力克服了前面提到的手动编写类的局限性。
  • 消除了冗余。
  • 如果模板发生更改,编译器会在编译时获取最新的更改。

核心思想:模板用来自动生成代码。

注意,​模板类并非类 ​。模板只有在填充了所有模板参数后才会成为类。从表示法上来说,vector 是一个类模板,而 vector是一个实际的类。实例化的一个结果是,vector和 vector是本质上不同的类型。尽管它们是从同一个模板实例化而来,但它们的区别就如同我们手动编写的 int_vector 和 double_vector 之间的区别一样。例如,一个期望接收 vector的函数无法接受 vector。要编写一个能够接受任何vector的函数,我们需要将该函数本身定义为模板。这一点将在下一章中讨论。

实例化的另一个后果是,我们可以预料到使用模板的程序会更大,编译速度也更慢。程序会更大,是因为会生成更多(冗余的)代码并包含在最终的可执行文件中,就像一个声明并使用int_vectordouble_vectorstring_vector的程序,会比只包含int_vector的程序更大。编译时间会更长,因为编译器必须多进行一次传递来实例化模板。

9.2 实现类模板

上面的vector模板声明 了三个方法:push_backsizeoperator[]。我们要在哪里定义这些方法呢?

cpp 复制代码
#include "vector.h"

template <typename T>
void vector<T>::push_back(const T& v) { /* ... */ }    // 注意!涂黄的部分<T>不能省略!

template <typename T>
size_t vector<T>::size() const { /* ... */ }

template <typename T>
T& vector<T>::operator[](size_t index) const { /* ... */ }

如前所述,将定义放入像这样的.cpp文件中可以缩短编译时间。当另一个文件想要使用vector时,只需包含vector.h即可 ------ 无需再编译vector.cpp。这被称为分离编译。但遗憾的是,​模板无法进行分离编译:我们无法轻松地将其代码拆分到.h文件和.cpp文件中。

因此:编译器在任何包含模板的地方都必须看到整个模板。换句话说,包含vector.h也应该包含它的定义。因此,类模板通常以仅头文件库的形式分发:它们将模板的实现完全放在一个文件中。以上面的vector模板为例,我们可以通过以下三种方式来实现一个模板:

1、在.h文件中内联编写定义。

cpp 复制代码
template <typename T>
class vector {
public:
  void push_back(const T& v) { /* ... */ }
  size_t size() const { /* ... */ }
  T& operator[](size_t index) const { /* ... */ }

  /* Other methods and implementation hidden */
};

2、在声明下方编写定义。

cpp 复制代码
template <typename T>
class vector {
public:
  void push_back(const T& v);
  size_t size() const;
  T& operator[](size_t index) const;

  /* Other methods and implementation hidden */
};

template <typename T>
void vector<T>::push_back(const T& v) { /* ... */ }

template <typename T>
size_t vector<T>::size() const { /* ... */ }

template <typename T>
T& vector<T>::operator[](size_t index) const { /* ... */ }

3、从.h文件中包含一个.cpp文件。这与您通常会做的相反!

cpp 复制代码
// vector.h
template <typename T>
class vector {
public:
  void push_back(const T& v);
  size_t size() const;
  T& operator[](size_t index) const;

  /* Other methods and implementation hidden */
};

#include "vector.cpp"

// vector.cpp
template <typename T>
void vector<T>::push_back(const T& v) { /* ... */ }

template <typename T>
size_t vector<T>::size() const { /* ... */ }

template <typename T>
T& vector<T>::operator[](size_t index) const { /* ... */ }

无模板的类:

模板类:

这些方法存在一个问题,那就是它们可能会在不经意间增加编译时间,因为每个包含vector.h的文件最终都会单独编译相同的定义。但在实际应用中,这通常不是什么问题,而且大多数模板库(例如 g++ 编译器对标准模板库头文件如<vector><map>的实现)都采用仅头文件库的形式。

例如,<vector>的 g++ 源代码可以在一个仅含头文件的库中找到,该库名为 <bits/stl_vector.h>,其中包含实际的 vector 模板声明。

还有第四种不太常用的方法可以解决这个问题,同时仍能享受分离编译的好处。我们可以像处理类时通常做的那样,将.h文件和.cpp文件分开,然后​.cpp文件中提前显式实例化模板:

vector.cpp文件

cpp 复制代码
#include "vector.h"

/* Definitions of the vector methods */

// Explicit instantiation:

template class vector<int>;
template class vector<double>;
template class vector<std::string>;

模板类语法会显式实例化模板,这样包含vector.h的另一个文件就能创建vectorintdoublestd::string类型,并使用其方法。

例如,尝试对vector或任何其他未实例化的类型执行相同操作,将会导致编译器错误。这样做的好处是缩短编译时间并减小编译后程序的大小,但代价是缺乏一定的灵活性 ------ 我们必须提前在.cpp文件中指定模板实例化,以编译相关的定义。

9.3 类模板的"怪癖"

9.3.1 typename vs. class

在阅读模板代码时,你可能会看到用class来代替typename

cpp 复制代码
template <typename T>
class vector {};

template <class T>
class vector {};

这两种形式是完全相同的,并且可以互换使用。它们之间的区别是 C++ 历史遗留下来的 ------ 最初,class被用来指代任何类型的名称,后来为了可读性,这一用法扩展到了typename

9.3.2 默认参数

可以为模板参数指定一个默认参数。如果未指定该参数,则将使用默认参数类型。例如,在std::vector(以及许多其他容器数据类型)的定义中,可以提供一个分配器类型来改变容器中元素的分配方式。下面的示例展示了std::vector如何使用Allocator模板参数为10个类型为T的元素分配空间。

cpp 复制代码
template <typename T, typename Allocator = std::allocator<T>>
class std::vector {
  vector() : _alloc(), _data(_alloc.allocate(10)) {}
  ~vector() { _alloc.deallocate(_data, _size); }

private:
  Allocator _alloc;
  T* _data;
  size_t _size = 0;
  size_t _capacity = 10;
};

如果未指定Allocator,则会使用std::allocator,它通过new来分配对象,并通过delete来释放对象。就std::vector而言,这使得该数据类型的用户能够指定元素数据的分配位置和分配方式。

9.3.3 无类型参数

与其他支持泛型编程的语言不同,模板参数并没有被限制为必须引用特定类型。例如,它们可以是intsize_tfloat或任何其他编译时常量。比如,考虑std::array

cpp 复制代码
template <typename T, size_t N>
struct std::array {
  /* Other public methods and functionality */
private:
  T[N] _data;
};

在这种情况下,std::array 会在其内存布局中为N 个类型为T 的元素预留空间!这有可能带来性能优势,因为不需要通过堆分配来为这N 个元素预留空间,如下例所示:

cpp 复制代码
std::vector<int> vec { 1, 2, 3, 4, 5};
std::array<int, 5> fiveArray;
std::array<int, 10> tenArray;

请注意,对于 std::array,其_data 字段直接嵌入到栈上对象的内存布局中。还要注意,与 vector 不同,array 的大小在编译时是固定的,并且更改 N 的值会产生不同的类型!将 std::array<int, 5> 赋值给 std::array<int, 10 > 是无效的,原因与将vector<int>赋值给vector<double>相同。

9.4 const 正确性

前面文章提到过的const:

如果一个变量是const,其引用也得是const

见2.4

范围for循环的const auto&

见4.2

const与指针

见5.2.1

const迭代器

见6.4.1

9.4.1 const方法
cpp 复制代码
template <typename T>    // vector 是一个模板,它接收一个类型 T 的名称。
class Vector {
public:
  size_t size();
  bool empty();
  T& operator[] (size_t index); 
  T& at(size_t index);
  void push_back(const T& elem);
};

错误:

cpp 复制代码
void printVec(const Vector<int>& v) {
        for (size_t i = 0; i < v.size(); i++) {
                std::cout << v.at(i) << " ";
        }
        std::cout << std::endl;
}
// Compiler: "No such method size!"

为什么?

  • 通过将 v 声明为 const,我们保证不会修改 v。
  • 编译器无法确定像 size 和 at 这样的方法是否会修改 v。
  • 记住,成员函数可以访问成员变量。

怎么修复?

cpp 复制代码
/////////////////////////////////////////////////////////////////////////
// .h文件
template<class T> 
class Vector {
public:
// 加const
        size_t size() const;
        bool empty() const;
        T& operator[] (size_t index); 
        T& at(size_t index) const;
        void push_back(const T& elem);
};
//////////////////////////////////////////////////////////////////////////
// .cpp文件

void printVec(const Vector<int>& v) {
        for (size_t i = 0; i < v.size(); i++) {
                std::cout << v.at(i) << " ";
        }
        std::cout << std::endl;
}
// Compiler: "OK!"

const的作用:告诉编译器保证不会在这个方法内部修改这个对象。

确保在实现中也加上 const,否则编译器会报错。

cpp 复制代码
template <class T>
size_t Vector<T>::size() const {
        return logical_size;
}
// Other methods...
9.4.2 const接口
cpp 复制代码
// .cpp
template <class T>
size_t Vector<T>::size() const {  // 这是一个const成员函数
    this->logical_size = 106;     // 错误:试图修改成员变量
    return logical_size;
}

这个错误的原因是在const成员函数中试图修改类的成员变量。

在 C++ 中,被声明为const的成员函数承诺不会修改类的任何成员变量,编译器会对这一点进行严格检查。

常量接口:

  • 标记为常量的对象只能使用常量接口
  • 常量接口是指对象中为常量的函数
cpp 复制代码
template<class T> 
class Vector {
public:
        size_t size() const;
        bool empty() const;
        
        T& operator[] (size_t index); 
        T& at(size_t index) const;    // 有两处错误
        void push_back(const T& elem);
};
cpp 复制代码
T& at(size_t index) const;

void oops(const Vector<int>& v) {
        v.at(0) = 42;    // 由于v是const,所以我们不能修改它
}
cpp 复制代码
template<class T> 
class Vector {
public:
        size_t size() const;
        bool empty() const;
        
        T& operator[] (size_t index); 
        const T& at(size_t index) const;    // 还有一处错误
        void push_back(const T& elem);
};
9.4.3 const重载

让我们定义 at 方法的两个版本

一个版本用于常量实例调用

另一个用于非常量实例调用

cpp 复制代码
////////////////////////////////////////////////////////////////
// .h
template<class T> 
class Vector {
public:
    const T&at(size_t index) const;
    T&at(size_t index);
};

////////////////////////////////////////////////////////////////
// .cpp
template <class T>
const T& Vector<T>::at(size_t index) const {
        return elems[index];
}

template <class T>
T& Vector<T>::at(size_t index) {
        return elems[index];
}

再加一个函数findElement呢?

cpp 复制代码
template <typename T>
T& Vector<T>::findElement(const T& value) {
        for (size_t i = 0; i < logical_size; i++) {
                if (elems[i] == elem) return elems[i];
        }
        throw std::out_of_range("Element not found");
}
template <typename T>
const T& Vector<T>::findElement(const T& value) const {
        for (size_t i = 0; i < logical_size; i++) {
                if (elems[i] == elem) return elems[i];
        }
        throw std::out_of_range("Element not found");
}
// 太繁琐了!!!!!
9.4.4 const_cast

类型转换(casting):将一种类型转换为另一种类型的过程

在 C++ 中有许多类型转换的方法,const_cast 允许我们 "去除" 变量的常量性

用法:const_cast < 目标类型 >(表达式)

那么这有什么用呢?

cpp 复制代码
template <typename T>
T& Vector<T>::findElement(const T& value) {
        for (size_t i = 0; i < logical_size; i++) {
                if (elems[i] == elem) return elems[i];
        }
        throw std::out_of_range("Element not found");
}
template <typename T>
const T& Vector<T>::findElement(const T& value) const {
        return const_cast<Vector<T>&>(*this).findElement(value);
}


何时用const_cast?

简短回答:几乎从不

const_cast 告诉编译器:"别担心,我能搞定"。如果你需要一个可变值,一开始就不要加 const。const_cast 的有效用法少之又少。

9.4.5 mutable

const_cast 使整个对象变得可修改,还有更细粒度的吗?

和const_cast一样,mutable会规避const的保护机制,请谨慎使用!

cpp 复制代码
struct MutableStruct {
  int dontTouchThis;
  mutable double iCanChange;
};

const MutableStruct cm;
// cm.dontTouchThis = 42;   // ❌ Not allowed, cm is const
cm.iCanChange= 3.14;  // ✅ Ok, iCanChange is mutable
cpp 复制代码
struct CameraRay {
  Point origin;
  Direction direction;
  mutable Color debugColor;
}

void renderRay(constCameraRay& ray) {
  ray.debugColor= Color.Yellow; // Show debug ray
  /* Rendering logic goes here ... */
}

使用案例:存储调试信息

cpp 复制代码
structCameraRay {
  Point origin;
  Direction direction;
  mutable ColordebugColor;
}

void renderRay(constCameraRay& ray) {
  ray.debugColor= Color.Yellow; // Show debug ray
  /* Rendering logic goes here ... */
}
相关推荐
m0_689618282 小时前
拓扑变换让机器人抓得又稳、又柔、又灵活
人工智能·笔记·学习·机器人
小年糕是糕手2 小时前
【C++同步练习】类和对象(三)
开发语言·jvm·c++·程序人生·考研·算法·改行学it
车载测试工程师2 小时前
CAPL学习-SOME/IP交互层-底层API函数
学习·tcp/ip·以太网·capl·canoe
代码游侠2 小时前
学习笔记——Linux内核链表
linux·运维·笔记·学习·算法·链表
sheeta19982 小时前
LeetCode 每日一题笔记 日期:2025.12.14 题目:2147.分隔长廊的方案数
linux·笔记·leetcode
Fcy6482 小时前
C++ set和multiset的使用
开发语言·c++·stl·map·multimap
八个程序员2 小时前
c++常见问题1——跳出代码
开发语言·c++
初晴や2 小时前
第一章:计算机基础知识
c++·计算机基础知识、
阿蒙Amon2 小时前
JavaScript学习笔记:8.日期和时间
javascript·笔记·学习