news 2025/12/17 15:13:46

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++基础:Stanford CS106L学习笔记 9 类模板(Class Templates)

目录

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

编写一个int_vector:
classint_vector{public:voidpush_back(inti);size_tsize()const;int&operator[](size_t index)const;/* Other methods and implementation hidden */};

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

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

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

9.1 类模板声明

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

template<typenameT>// vector 是一个模板,它接收一个类型 T 的名称。classvector{public:voidpush_back(constT&v);// vector被实例化时,T被替换size_tsize()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[]。我们要在哪里定义这些方法呢?

#include"vector.h"template<typenameT>voidvector<T>::push_back(constT&v){/* ... */}// 注意!涂黄的部分<T>不能省略!template<typenameT>size_tvector<T>::size()const{/* ... */}template<typenameT>T&vector<T>::operator[](size_t index)const{/* ... */}

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

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

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

template<typenameT>classvector{public:voidpush_back(constT&v){/* ... */}size_tsize()const{/* ... */}T&operator[](size_t index)const{/* ... */}/* Other methods and implementation hidden */};

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

template<typenameT>classvector{public:voidpush_back(constT&v);size_tsize()const;T&operator[](size_t index)const;/* Other methods and implementation hidden */};template<typenameT>voidvector<T>::push_back(constT&v){/* ... */}template<typenameT>size_tvector<T>::size()const{/* ... */}template<typenameT>T&vector<T>::operator[](size_t index)const{/* ... */}

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

// vector.htemplate<typenameT>classvector{public:voidpush_back(constT&v);size_tsize()const;T&operator[](size_t index)const;/* Other methods and implementation hidden */};#include"vector.cpp"// vector.cpptemplate<typenameT>voidvector<T>::push_back(constT&v){/* ... */}template<typenameT>size_tvector<T>::size()const{/* ... */}template<typenameT>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文件

#include"vector.h"/* Definitions of the vector methods */// Explicit instantiation:templateclassvector<int>;templateclassvector<double>;templateclassvector<std::string>;

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

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

9.3 类模板的"怪癖"

9.3.1typenamevs.class

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

template<typenameT>classvector{};template<classT>classvector{};

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

9.3.2 默认参数

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

template<typenameT,typenameAllocator=std::allocator<T>>classstd::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

template<typenameT,size_t N>structstd::array{/* Other public methods and functionality */private:T[N]_data;};

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

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方法
template<typenameT>// vector 是一个模板,它接收一个类型 T 的名称。classVector{public:size_tsize();boolempty();T&operator[](size_t index);T&at(size_t index);voidpush_back(constT&elem);};

错误:

voidprintVec(constVector<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。
  • 记住,成员函数可以访问成员变量。

怎么修复?

/////////////////////////////////////////////////////////////////////////// .h文件template<classT>classVector{public:// 加constsize_tsize()const;boolempty()const;T&operator[](size_t index);T&at(size_t index)const;voidpush_back(constT&elem);};//////////////////////////////////////////////////////////////////////////// .cpp文件voidprintVec(constVector<int>&v){for(size_t i=0;i<v.size();i++){std::cout<<v.at(i)<<" ";}std::cout<<std::endl;}// Compiler: “OK!“

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

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

template<classT>size_tVector<T>::size()const{returnlogical_size;}// Other methods...
9.4.2 const接口
// .cpptemplate<classT>size_tVector<T>::size()const{// 这是一个const成员函数this->logical_size=106;// 错误:试图修改成员变量returnlogical_size;}

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

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

常量接口:

  • 标记为常量的对象只能使用常量接口
  • 常量接口是指对象中为常量的函数
template<classT>classVector{public:size_tsize()const;boolempty()const;T&operator[](size_t index);T&at(size_t index)const;// 有两处错误voidpush_back(constT&elem);};
T&at(size_t index)const;voidoops(constVector<int>&v){v.at(0)=42;// 由于v是const,所以我们不能修改它}
template<classT>classVector{public:size_tsize()const;boolempty()const;T&operator[](size_t index);constT&at(size_t index)const;// 还有一处错误voidpush_back(constT&elem);};
9.4.3 const重载

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

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

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

////////////////////////////////////////////////////////////////// .htemplate<classT>classVector{public:constT&at(size_t index)const;T&at(size_t index);};////////////////////////////////////////////////////////////////// .cpptemplate<classT>constT&Vector<T>::at(size_t index)const{returnelems[index];}template<classT>T&Vector<T>::at(size_t index){returnelems[index];}

再加一个函数findElement呢?

template<typenameT>T&Vector<T>::findElement(constT&value){for(size_t i=0;i<logical_size;i++){if(elems[i]==elem)returnelems[i];}throwstd::out_of_range("Element not found");}template<typenameT>constT&Vector<T>::findElement(constT&value)const{for(size_t i=0;i<logical_size;i++){if(elems[i]==elem)returnelems[i];}throwstd::out_of_range("Element not found");}// 太繁琐了!!!!!
9.4.4 const_cast

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

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

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

那么这有什么用呢?

template<typenameT>T&Vector<T>::findElement(constT&value){for(size_t i=0;i<logical_size;i++){if(elems[i]==elem)returnelems[i];}throwstd::out_of_range("Element not found");}template<typenameT>constT&Vector<T>::findElement(constT&value)const{returnconst_cast<Vector<T>&>(*this).findElement(value);}


何时用const_cast?

简短回答:几乎从不

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

9.4.5 mutable

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

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

structMutableStruct{intdontTouchThis;mutabledoubleiCanChange;};constMutableStruct cm;// cm.dontTouchThis = 42; // ❌ Not allowed, cm is constcm.iCanChange=3.14;// ✅ Ok, iCanChange is mutable
structCameraRay{Point origin;Direction direction;mutableColor debugColor;}voidrenderRay(constCameraRay&ray){ray.debugColor=Color.Yellow;// Show debug ray/* Rendering logic goes here ... */}

使用案例:存储调试信息

structCameraRay{Point origin;Direction direction;mutableColordebugColor;}voidrenderRay(constCameraRay&ray){ray.debugColor=Color.Yellow;// Show debug ray/* Rendering logic goes here ... */}
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/17 1:41:46

【大模型】-LangChain--stream流式同步异步

文章目录1.同步stream流2.异步astream流3.异步astream流json输出4.异步事件astream_events流5.异步多线程1.同步stream流 import os from langchain_community.chat_models import ChatTongyios.environ["DASHSCOPE_API_KEY"] "sk-秘钥" llm ChatTongyi…

作者头像 李华
网站建设 2025/12/17 1:41:44

兜兜英语每日短语:逃单篇

1. &#x1f37d;️&#x1f4a8; dine and dash&#xff08;吃霸王餐 / 吃完就跑&#xff09; 英文&#xff1a;Nine diners in Chongqing dined and dashed, leaving one person behind. 中文&#xff1a;重庆 9 名食客吃霸王餐后逃单&#xff0c;只留下一人 “背锅”&#x…

作者头像 李华
网站建设 2025/12/17 1:41:43

计算机毕业设计springboot汽车智慧检修系统 基于SpringBoot的智能汽车故障预测与维修管理平台 融合IoT的SpringBoot车辆健康监测与维修决策系统

计算机毕业设计springboot汽车智慧检修系统a93520rj &#xff08;配套有源码 程序 mysql数据库 论文&#xff09; 本套源码可以在文本联xi,先看具体系统功能演示视频领取&#xff0c;可分享源码参考。 当汽车从“机械”进化为“数据孤岛”&#xff0c;每一次打火、每一脚刹车都…

作者头像 李华
网站建设 2025/12/14 16:47:31

python3

import osdef traverse_files(path):# 遍历当前路径下的所有文件和文件夹for item in os.listdir(path):# 拼接完整路径item_path os.path.join(path, item)if os.path.isfile(item_path):# 是文件则输出路径print("文件:", item_path)elif os.path.isdir(item_path…

作者头像 李华
网站建设 2025/12/14 16:46:35

【3D图像技术分析与实现】Apple Vision Pro三维成像技术栈深度解析

作为空间计算的标杆产品&#xff0c;Apple Vision Pro的三维成像能力是其实现虚实融合体验的核心基石。它并未依赖单一技术路径&#xff0c;而是通过“多传感器硬件阵列专用芯片算力全栈软件框架”的协同架构&#xff0c;构建了兼具精度、实时性与沉浸感的三维感知系统。本文将…

作者头像 李华
网站建设 2025/12/14 16:43:09

经典算法题详解之统计重复个数(三)

算法我们设计一个哈希表 recall&#xff1a;哈希表 recall 以 s2 字符串的下标 index 为索引&#xff0c;存储匹配至第 s1cnt 个 s1 的末尾&#xff0c;当前匹配到第 s2cnt 个 s2 中的第 index 个字符时&#xff0c; 已经匹配过的 s1 的个数 s1cnt 和 s2 的个数 s2cnt 。我们在…

作者头像 李华