C++从入门到实战(十三)C++函数模板与类模板初阶讲解

C++从入门到实战(十三)C++函数模板与类模板初阶讲解

  • 前言
  • 一、为什么需要模板
    • 1. 函数重载的问题
    • 2. 泛型编程和模板的作用
  • 二、函数模板
    • 2.1 函数模板格式
    • 2.2 函数模板的原理
    • 2.3 函数模板的实例化
      • (1)隐式实例化:
      • (2)显式实例化:
    • 2.4 模板参数的匹配原则
      • 1. 非模板函数与同名函数模板共存
      • 2. 调用时的优先选择规则
      • 3. 类型转换规则差异
  • 三、类模板
    • 3.1 类模板是什么?
    • 3.2 类模板的定义格式
    • 3.3 类模板的实例化:从 “模具” 生成具体类
    • 3.4 类模板的注意事项
      • 1.模板参数声明不能省略
      • 2. 声明和定义不能分离到.h 和.cpp
      • 3. 支持多个模板参数


前言

  • 在上一篇博客中,我们围绕 C/C++ 内存管理展开讨论,深入解析了内存分布模型、C 与 C++ 内存管理的核心差异,并初步认识了 C++ 中new与delete的基本用法,为理解 C++ 内存管理体系打下基础。
  • 从本文开始,我们将暂别内存管理主题,转而聚焦 C++ 模板编程的核心模块 ——函数模板初阶

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482


一、为什么需要模板

1. 函数重载的问题

在C++里,要是你想实现交换两个变量值的功能,对不同类型的变量(像intdoublechar),就得写不同的Swap函数

  • 就像下面这样
void Swap(int& left, int& right){int temp = left;left = right;right = temp;}void Swap(double& left, double& right){double temp = left;left = right;right = temp;}void Swap(char& left, char& right){char temp = left;left = right;right = temp;}

不过,这种函数重载的做法存在一些弊端

  • 代码复用率低:这些重载函数只是处理的变量类型不同,代码的逻辑是一样的。要是以后有新的类型,就得手动再写对应的Swap函数。
  • 可维护性差:如果其中一个Swap函数出了问题,可能所有重载函数都得检查一遍,因为它们的代码逻辑本质相同,一处出错可能其他地方也有类似问题。

2. 泛型编程和模板的作用

为了解决上面的问题,C++引入了泛型编程的概念

  • 泛型编程的目标是编写和具体类型无关的通用代码,以此实现代码复用。而模板就是泛型编程的基础。

可以把模板想象成一个模具。当你需要不同类型的具体代码时,就往这个模具里填入不同的“材料”(也就是类型),这样就能得到不同“材料”的“铸件”(也就是具体类型的代码)。

在这里插入图片描述

比如,使用模板来实现Swap函数:

现在看一下,后面我们会详细讲解

template <typename T>
void Swap(T& left, T& right) {T temp = left;left = right;right = temp;
}

这里的template <typename T>就声明了一个模板,T是一个类型参数,它代表任意类型。在使用这个Swap函数时,编译器会根据传入的变量类型,自动用具体类型替换T,生成对应的代码。这样就不用为每种类型都写一个单独的函数,既提高了代码复用率,也让代码更易于维护。

在这里插入图片描述

二、函数模板

  • 函数模板就像是一个函数家族,它和具体的类型没关系。在使用的时候,会根据传入的实参类型,生成特定类型的函数版本。

2.1 函数模板格式

函数模板的定义格式是:template<typename T1, typename T2,......,typename Tn>,后面接着返回值类型、函数名和参数列表。

  • 比如Swap函数模板:
template<typename T>
void Swap( T& left,  T& right)
{T temp = left;left = right;right = temp;
}

这里的typename是用来定义模板参数的关键字,也能用class,但不能用struct代替class

#include <iostream>
using namespace std;template<typename T>
void Swap( T& left, T& right)
{T temp = left;left = right;right = temp;
}
int main() {int a = 5, b = 10;cout << "Before swap: a = " << a << ", b = " << b << endl;Swap(a, b);  // 调用模板函数,传入int类型的参数cout << "After swap: a = " << a << ", b = " << b << endl;return 0;
}

在这里插入图片描述

2.2 函数模板的原理

  • 函数模板就像一个蓝图,它本身不是真正的函数,而是让编译器根据使用方式生成特定类型函数的模具。

  • 这样就把原本我们要做的重复工作交给了编译器。

  • 在编译阶段,编译器会根据传入的实参类型,推演出对应的类型,然后生成专门处理该类型的代码
    在这里插入图片描述

  • 例如用double类型使用模板时,编译器会把T确定为double类型,生成处理double类型的代码

2.3 函数模板的实例化

  • 用不同类型的参数使用函数模板,就叫做函数模板的实例化。
  • 分为隐式实例化和显式实例化

(1)隐式实例化:

让编译器根据实参来推导出模板参数的实际类型。比如Add函数模板:

template<class T>
T Add(const T& left, const T& right)
{return left + right;
}

#include <iostream>
using namespace std;template<class T>
T Add(const T& left, const T& right)
{return left + right;
}int main()
{int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;cout << Add(a1, a2) << endl;cout << Add(d1, d2) << endl;
}

在这里插入图片描述

  • 当我们写成这样时
 int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;Add(a1, d1);
  • 编译器会报错,因为编译器无法确定T是int还是double
  • 处理方式:需要自己强制转换
    在这里插入图片描述
  • 在模板里,编译器一般不会进行类型转换,怕出问题

(2)显式实例化:

在函数名后的<>里指定模板参数的实际类型。

  • 比如:
int main(void)
{int a = 10;double b = 20.0;// 显式实例化Add<int>(a, b);return 0;
}

如果类型不匹配,编译器会尝试隐式类型转换,转换不成功就会报错

#include <iostream>
using namespace std;template<class T>
T Add(const T& left, const T& right)
{return left + right;
}int main(void)
{int a = 10;double b = 20.0;// 显式实例化cout << Add<int>(a, b) << endl;return 0;
}

在这里插入图片描述

2.4 模板参数的匹配原则

1. 非模板函数与同名函数模板共存

  • 在 C++ 里,一个非模板函数和同名的函数模板能够同时存在
  • 而且函数模板可以实例化为与非模板函数相同功能的函数。
#include <iostream>// 专门处理 int 类型的加法非模板函数
int Add(int left, int right) {return left + right;
}// 通用加法函数模板
template<class T>
T Add(T left, T right) {return left + right;
}int main() {// 调用 Add(1, 2),会和非模板函数匹配std::cout << "调用 Add(1, 2) 的结果: " << Add(1, 2) << std::endl;// 调用 Add<int>(1, 2),会调用编译器特化的 Add 版本std::cout << "调用 Add<int>(1, 2) 的结果: " << Add<int>(1, 2) << std::endl;return 0;
}

在这里插入图片描述

在上述代码中,定义了一个专门处理 int 类型的非模板 Add 函数,以及一个通用的 Add 函数模板。

  • 当调用 Add(1, 2) 时,由于实参类型是 int,编译器会优先选择非模板函数。而调用 Add<int>(1, 2) 时,明确指定了使用函数模板,编译器会对模板进行实例化,生成处理 int 类型的函数。

2. 调用时的优先选择规则

当非模板函数和同名函数模板其他条件相同时,调用时会优先选择非模板函数

  • 不过,要是模板能产生更匹配的函数,就会选择模板。下面通过代码来进一步说明:
#include <iostream>// 专门处理 int 类型的加法非模板函数
int Add(int left, int right) {return left + right;
}// 通用加法函数模板,可处理不同类型的参数
template<class T1, class T2>
auto Add(T1 left, T2 right) {return left + right;
}int main() {// 调用 Add(1, 2),和非模板函数完全匹配std::cout << "调用 Add(1, 2) 的结果: " << Add(1, 2) << std::endl;// 调用 Add(1, 2.0),模板函数能生成更匹配的版本std::cout << "调用 Add(1, 2.0) 的结果: " << Add(1, 2.0) << std::endl;return 0;
}

在这段代码中,调用 Add(1, 2) 时,实参类型都是 int,与非模板函数完全匹配,所以会调用非模板函数

  • 而调用 Add(1, 2.0) 时,实参类型分别是 intdouble,非模板函数无法直接匹配,此时模板函数能生成更匹配的版本,编译器就会选择模板函数进行实例化并调用。

在这里插入图片描述

3. 类型转换规则差异

模板函数不允许自动类型转换,而普通函数可以进行自动类型转换。

#include <iostream>// 普通加法函数
int Add(int left, int right) {return left + right;
}// 函数模板
template<class T>
T Add(T left, T right) {return left + right;
}int main() {int a = 1;double b = 2.0;// 普通函数可以进行自动类型转换std::cout << "普通函数调用 Add(a, b) 的结果: " << Add(a, b) << std::endl;// 模板函数不允许自动类型转换,下面这行代码会报错// std::cout << "模板函数调用 Add(a, b) 的结果: " << Add(a, b) << std::endl;// 若要使用模板函数,需手动进行类型转换std::cout << "模板函数调用 Add(a, static_cast<int>(b)) 的结果: " << Add(a, static_cast<int>(b)) << std::endl;return 0;
}

在上述代码中,调用普通函数 Add(a, b) 时,编译器会自动将 double 类型的 b 转换为 int 类型。

  • 而调用模板函数 Add(a, b) 时,由于模板函数不允许自动类型转换,会导致编译错误。若要使用模板函数,需要手动进行类型转换,如 Add(a, static_cast<int>(b))

三、类模板

3.1 类模板是什么?

  • 想象你要做一个 “栈”(Stack)类,用来存储数据。

  • 如果数据可能是整数、小数、字符等不同类型,难道要为每种类型写一个独立的Stack类吗

  • 类模板就是解决这个问题的 “通用模具”:它定义一个与类型无关的类,通过传入具体类型,生成针对该类型的专属类

3.2 类模板的定义格式

template <class T1, class T2, ..., class Tn> // 模板参数列表,T是类型占位符
class 类模板名 {// 类的成员(变量/函数)可以使用T1、T2等模板参数// 例如:成员变量类型是T,成员函数参数类型是T&
};
#include <iostream>
using namespace std;template <typename T> // 等价于template <class T>,T代表任意类型
class Stack {
public:// 构造函数:初始化数组和容量Stack(size_t capacity = 4) {_array = new T[capacity]; // 数组元素类型是T_capacity = capacity;_size = 0;}// 声明成员函数(参数类型是T&)void Push(const T& data); // 向栈中添加数据private:T* _array; // 存储数据的数组,类型是T*size_t _capacity; // 容量size_t _size; // 已存储元素数量
};// 类外定义成员函数时,需要再次指定模板参数
template <class T> // 必须重复模板参数声明
void Stack<T>::Push(const T& data) { // 用Stack<T>表明这是T类型的成员函数if (_size < _capacity) {_array[_size] = data; // 存入数据,类型是T_size++;}
}

3.3 类模板的实例化:从 “模具” 生成具体类

类模板本身不是真正的类,必须通过实例化指定具体类型,才能生成可用的类

  • 语法:类模板名<具体类型> 对象名;
  • 显式指定类型:必须在类名后用<>明确写出类型(不能像函数模板那样隐式推导)。
  • 生成专属类:每个不同的类型实例化,都会生成一个独立的类
    例如上面代码中的
int main() {Stack<int> st1;    // 实例化出存储int的栈类,生成Stack<int>类Stack<double> st2; // 实例化出存储double的栈类,生成Stack<double>类st1.Push(10);      // 向int栈中添加数据st2.Push(3.14);    // 向double栈中添加数据return 0;
}
  • Stack是模板名,不是类型;Stack才是具体的类型(就像 “模具压出的产品”)。
  • 每个实例化的类(如Stack<int>、Stack<double>)都是独立的,互不干扰。

3.4 类模板的注意事项

1.模板参数声明不能省略

  • 在类外定义成员函数时,必须像template <class T>一样重复声明模板参数,否则编译器不知道T是什么。

2. 声明和定义不能分离到.h 和.cpp

  • 模板需要在编译阶段根据实例化的类型生成代码,如果声明在.h、定义在.cpp,编译器可能找不到定义,导致链接错误。

3. 支持多个模板参数

  • 可以定义多个类型参数,例如:
template <class T, class U> // 两个类型参数T和U
class Pair {T first;U second;
};
Pair<int, double> p(10, 3.14); // 实例化时传入两个类型

以上就是这篇博客的全部内容,下一篇我们将继续探索C++中STL更多精彩内容。

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482

非常感谢您的阅读,喜欢的话记得三连哦

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/219.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

游戏引擎学习第261天:切换到静态帧数组

game_debug.cpp: 将ProfileGraph的尺寸初始化为相对较大的值 今天的讨论主要围绕性能分析器&#xff08;Profiler&#xff09;以及如何改进它的可用性展开。当前性能分析器已经能够正常工作&#xff0c;但我们希望通过一些改进&#xff0c;使其更易于使用&#xff0c;特别是在…

《Python星球日记》 第36天:线性代数基础

名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏:《Python星球日记》,限时特价订阅中ing 目录 一、标量、向量、矩阵的基本概念1. 标量2. 向量3. 矩阵二、矩阵运算1. 矩阵加法2. 矩阵乘法3. 矩…

【SF顺丰】顺丰开放平台API对接(注册、API测试篇)

1.注册开发者账号 注册地址&#xff1a;顺丰企业账户中心 2.登录开发平台 登录地址&#xff1a;顺丰开放平台 3.开发者对接 点击开发者对接 4.创建开发对接应用 开发者应用中“新建应用”创建应用&#xff0c;最多创建应用限制数量5个 注意&#xff1a;需要先复制保存生产校验…

【Linux】进程地址空间

&#x1f4dd;前言&#xff1a; 这篇文章我们来讲讲进程地址空间&#xff1a; &#x1f3ac;个人简介&#xff1a;努力学习ing &#x1f4cb;个人专栏&#xff1a;Linux &#x1f380;CSDN主页 愚润求学 &#x1f304;其他专栏&#xff1a;C学习笔记&#xff0c;C语言入门基础…

【Java学习】反射

目录 反射类 一、泛型参数 二、反射类类型 三、实例化 1.实例化材料 2.结构信息可使用化 四、使用 1.Class —类完整结构信息 1.1Class<类>实例化 1.2Class<类>实例获取 1.2.1Class类静态获取&#xff1a; 1.2.2信息类静态获取 1.2.3信息类非静态获取 …

MVC、MVP、MVVM三大架构区别

1、MVC架构 M&#xff08;Model&#xff09;&#xff1a;主要处理数据的存储、获取、解析。 V&#xff08;View&#xff09;&#xff1a;即Fragement、Activity、View等XML文件 C&#xff08;Controller&#xff09;&#xff1a;主要功能为控制View层数据的显示&#xff0c;…

科创大赛——知识点复习【c++】——第一篇

目录 输入 一、cin 二、scanf 三、gets 四、getchar 五、fgets 输出 一、cout 二、printf 基本数据类型 一&#xff0c;数据类型有哪些&#xff1f; 二&#xff0c;整型&#xff08;Integer Types&#xff09; 1&#xff0c;修饰符 2&#xff0c;整型数据的数据范…

java学习之数据结构:四、树(代码补充)

这部分主要是用代码实现有序二叉树、树遍历、删除节点 目录 1.构建有序二叉树 1.1原理 1.2插入实现 2.广度优先遍历--队列实现 3.深度优先遍历--递归实现 3.1先序遍历 3.2中序遍历 3.3后序遍历 4.删除 4.1删除叶子节点 4.2删除有一棵子树的节点 4.3删除有两棵子树的节…

基于 HTML 和 CSS 实现的 3D 翻转卡片效果

一、引言 在网页设计中&#xff0c;为了增加用户的交互体验和视觉吸引力&#xff0c;常常会运用一些独特的效果。本文将详细介绍一个基于 HTML 和 CSS 实现的 3D 翻转卡片效果&#xff0c;通过对代码的剖析&#xff0c;让你了解如何创建一个具有立体感的卡片&#xff0c;在鼠标…

PHP数组排序深度解析:sort()、rsort()、asort()、arsort()、ksort()、krsort() 的适用场景与性能对比

在PHP开发中&#xff0c;数组排序是日常操作的核心技能之一。无论是处理用户数据、产品列表&#xff0c;还是分析日志信息&#xff0c;合理的排序方法能显著提升代码的效率和可维护性。PHP提供了多种数组排序函数&#xff08;如 sort()、rsort()、asort() 等&#xff09;&#…

C++ 中二级指针的正确释放方法

C 中二级指针的正确释放 一、什么是二级指针&#xff1f; 简单说&#xff0c;二级指针就是指向指针的指针。 即&#xff1a; int** p;它可以指向一个 int*&#xff0c;而 int* 又指向一个 int 类型的变量。 常见应用场景 动态二维数组&#xff08;例如 int** matrix&#x…

Linux 进程基础(二):操作系统

目录 一、什么是操作系统&#xff1a;用户和电脑之间的「翻译官」&#x1f310; OS 的层状结构&#x1f9e9; 案例解析&#xff1a;双击鼠标的「跨层之旅」 二、操作系统的必要性探究&#xff1a;缺乏操作系统的环境面临的挑战剖析&#x1f511; OS 的「管理者」属性&#xff1…