C++ 模板详解——template<class T>

一. 前言

在我们学习C++时,常会用到函数重载。而函数重载,通常会需要我们编写较为重复的代码,这就显得臃肿,且效率低下。重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数。此外,代码的可维护性比较低,一个出错可能会导致所有的重载均出错。

二. 什么是C++模板

泛型编程思想的引用

如果我们现在需要写一个交换两个值的函数,我们学习了C++的函数重载和引用,那么写法大致如下:

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;
}

但是这样写并不是最完美的,如果这是个项目,后面更新内容要新加更多种类型的话,还要我们手动去添加匹配类型的函数呢

  • 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
  • 代码的可维护性比较低,一个出错可能所有的重载均出错

 当时祖师爷也很苦恼,最后他运用他那天才的大脑想了个方法:告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码

就像做月饼的模子一样,我们放入不同颜色的材料,就能得到形状相同但颜色不同的月饼。
如果在C++中,也能够存在这样一个模具,通过给这个模具填充不同颜色的材料(类型),从而得到形状相同但颜色不同的月饼(生成具体类型的代码),那将会大大减少代码的冗余。巧的是前人早已将树栽好,我们只需在此乘凉。
 

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础

三. C++模板的分类

三. 函数模板

函数模板概念

程序员定义一种形式的函数,这种函数可以用不同的数据类型操作,但具有相同的功能逻辑。通过使用函数模板,可以大大减少代码的重复,并提高代码的复用性和灵活性。

函数模板定义格式

template <typename Type>
ReturnType FunctionName(Parameters) {// 函数体
}
//template <typename Type> 是模板声明,指出这是一个模板,并定义了一个类型参数 Type。typename 可以替换为 class,两者在这里作用相同。
//ReturnType 是函数返回的数据类型,它可以依赖于模板类型。
//FunctionName 是函数的名称。
//Parameters 是函数接受的参数,这些参数的类型可以是模板类型 Type。
示例

代码实例: 

template<typename T>
void Swap(T& left, T& right)
{{T temp = left;left = right;right = temp;}}int main()
{int LeftI = 2;int RightI = 3;cout << LeftI << RightI << endl;Swap(LeftI,RightI);cout << LeftI << RightI << endl;double LeftD = 2.5;double RightD = 3.5;cout << LeftD << RightD << endl;Swap(LeftD, RightD);cout << LeftD << RightD << endl;char LeftC = 'A';char RightC = 'B';cout << LeftC << RightC << endl;Swap(LeftC, RightC);cout << LeftC << RightC << endl;return 0;
}

 我们通过这个函数模版,分别传入不同数据类型的参数,通过结果的观察可以发现这个函数模版可以根据不同的类型去做一个自动推导,继而去起到一个交换的功能。 

函数模板的原理

请问上述真的是调用一个函数吗?

当然不是,这里我们三次Swap不是调用同一个函数,这里可以通过反汇编观察到:Swap根据不同的类型通过模板定制出专属的类型的函数,然后再调用

可以发现,在进行汇编代码查看的时候,被调用的函数模版生成了三个不同的函数,它们有着不同的函数地址

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。
所以其实模板就是将本来应该我们做的重复的事情交给了编译器。 

 在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用int类型使用函数模板时,编译器通过对实参类型的推演,将T确定为int类型,然后产生一份专门处理int类型的代码,对于double,字符类型也是如此

对于一些常用的函数如swap,库里面已经定义好了模板,我们只需要直接用即可

 

函数模板的实例化

用不同类型的参数使用模板时,称为模板的实例化。模板实例化分为隐式实例化和显示实例化。

隐式实例化

隐式实例化发生在模板代码被实际使用时。编译器根据模板使用时提供的具体类型参数自动生成模板的一个实例。这意味着编译器在遇到模板函数或类模板的特定类型的调用或声明时,自动生成必要的代码。

例如,给定一个模板函数:

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

当你调用这个函数: 

int x = 1, y = 2;
Swap(x, y);  // 编译器将隐式地为int类型实例化Swap模板

 编译器自动为 int 类型参数创建 Swap 函数的实例,这个过程称为隐式实例化。

显式实例化

显式实例化是你明确告诉编译器为特定类型生成模板的实例,而不是让编译器根据需要来决定。通过显式实例化,编译器会在实例化的地方生成模板的代码,而不管模板是否会被用到,这常用于减少编译时间和控制模板的实例化位置。

//在函数名后的<>中指定模板参数的实际类型
template void Swap<int>(int&, int&);  // 显式地实例化int类型的Swap模板

这段代码不调用 Swap 函数,但它告诉编译器生成处理 int 类型的 Swap 函数的代码。

显式实例化确保模板只被实例化一次,这对于减少编译时间和解决链接问题是有帮助的,尤其是在大型项目中或者模板定义与实例化在不同的编译单元时。

总结来说,隐式实例化基于代码中的使用自动进行,而显式实例化基于程序员的指示进行。

 例如,还是Swap模板函数:

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

 调用Swap函数

int a1 = 10, a2 = 20;
double b1 = 10.0, b2 = 20.0;
//显示实例化
Swap<int>(a1,a2);
Swap<double>(b1,b2);Swap<double>(a1,b2);//使用显示实例化时,如果传入的参数类型与模板参数类型不匹配,
//编译器会尝试进行隐式类型转换,显式指定T为double,int1将隐式转换为double.
//如果无法转换成功,则编译器将会报错。

外传 注意:以下使用情况可能一不小心就会出错

template<class T>
T Add(const T& left, const T& right)
{return left + right;
}int main()
{int int1 = 10, int2 = 20;double double1 = 10.0, double2 = 20.0;Add(int1, double2); //err 编译器推不出来//该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
//通过实参int1将T推演为int,通过实参double2将T推演为double类型,但模板参数列表中只有
//一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错
//注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅}

在函数模板调用时,每个模板参数都必须是明确的,编译器将尝试从每个实参推导出对应的模板参数类型。在这个例子中,编译器将尝试通过 int1 推导出 Tint 类型,同时又通过 double2 推导出 Tdouble 类型。显然,这两种类型是冲突的,因为模板参数列表中只有一个 T,并且在C++模板中,编译器通常不会自动进行类型转换来匹配模板参数。

解决方法

方法1: 使用相同类型的参数

确保调用 Add 函数时使用相同类型的参数:

int main() {int int1 = 10, int2 = 20;Add(int1, int2);  // 正确,T被推导为intdouble double1 = 10.0, double2 = 20.0;Add(double1, double2);  // 正确,T被推导为double
}

方法2: 显式指定模板参数

显式指定模板参数 T 的类型,这将导致非模板参数类型的隐式转换:

int main() {int int1 = 10;double double2 = 20.0;Add<double>(int1, double2);  // 显式指定T为double,int1将隐式转换为double
}

方法3: 修改函数模板以接受两个不同的类型参数

修改 Add 函数模板以接受两个不同的类型参数,并明确指定返回类型:

template<class T1, class T2>
auto Add(const T1& left, const T2& right) -> decltype(left + right) {return left + right;
}int main() {int int1 = 10;double double2 = 20.0;auto result = Add(int1, double2);  // result的类型将是T1和T2之和的类型,即double
}

模板参数的匹配原则

一. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

#include <iostream>
using namespace std;
//专门用于int类型加法的非模板函数
int Add(const int& x, const int& y)
{return x + y;
}
//通用类型加法的函数模板
template<typename T>
T Add(const T& x, const T& y)
{return x + y;
}
int main()
{int a = 10, b = 20;int c = Add(a, b); //调用非模板函数,编译器不需要实例化int d = Add<int>(a, b); //调用编译器实例化的Add函数return 0;
}

二. 对于非模板函数和同名的函数模板,如果其他条件都相同,在调用时会优先调用非模板函数,而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么选择模板

//专门用于int类型加法的非模板函数
int Add(const int& x, const int& y)
{return x + y;
}
//通用类型加法的函数模板
template<typename T1, typename T2>//这里传了两个模板,所以能传入两种不同类型的参数
T1 Add(const T1& x, const T2& y)
{return x + y;
}
int main()
{int a = Add(10, 20); //与非模板函数完全匹配,不需要函数模板实例化int b = Add(2.2, 2); //函数模板可以生成更加匹配的版本,编译器会根据实参生成更加匹配的Add函数return 0;
}

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

#include <iostream>
using namespace std;
template<typename T>
T Add(const T& x, const T& y)
{return x + y;
}
int main()
{int a = Add(2, 2.2); //模板函数不允许自动类型转换,不能通过编译return 0;
}

因为模板函数不允许自动类型转换,所以不会将2自动转换为2.0,或是将2.2自动转换为2。 

模板支持多个模板参数

template<class K, class V> //两个模板参数
void Func(const K& key, const V& value)
{cout << key << ":" << value << endl;
}
int main()
{Func(1, 1); //K和V均intFunc(1, 1.1);//K是int,V是doubleFunc<int, char>(1, 'A'); //多个模板参数也可指定显示实例化不同类型
}

总结:通过使用函数模板,可以写出更通用、更灵活的代码,同时减少重复和错误。编译时的类型检查提供了比C语言中的宏更高的类型安全性。函数模板在C++标准库中被广泛使用,例如在STL(标准模板库)的各种算法和容器中。这使得STL非常强大,因为它不仅能够处理几乎任何类型的数据,还保持了代码的紧凑和高效。

四. 类模板

类模板的概念

程序员定义一个蓝图,用于生成具体化的类,这些类可以处理多种数据类型。与函数模板类似,类模板目的在于提高代码的复用性、减少冗余,并提升类型安全性。通过类模板,可以创建出适用于任何数据类型的类结构,而无需对每种类型编写专门的代码。

类模板的定义格式 

template<class T1, class T2, ..., class Tn>
class 类模板名
{// 类内成员定义
};

例如:

template<class T>
class Score
{
public:void Print(){cout << "数学:" << _Math << endl;cout << "语文:" << _Chinese << endl;cout << "英语:" << _English << endl;}
private:T _Math;T _Chinese;T _English;
};

 注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。

template<class T>
class Score
{
public:void Print();
private:T _Math;T _Chinese;T _English;
};
//类模板中的成员函数在类外定义,需要加模板参数列表
template<class T>
void Score<T>::Print()
{cout << "数学:" << _Math << endl;cout << "语文:" << _Chinese << endl;cout << "英语:" << _English << endl;
}

外传:注意:类模板,是不支持,声明,定义,测试分开写的,会出现链接编译错误,如下:

解决办法:

类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后面根<>,然后将实例化的类型放在<>中即可。类模板名字不是真正的类,而实例化的结果才是真正的类

template <class T>
class ExamScores {
private:vector<T> scores;  // 存储成绩的动态数组public:// 向成绩列表中添加一个成绩void addScore(T score) {scores.push_back(score);}// 打印所有成绩void printScores() const {cout << "成绩列表: ";for (T score : scores) {cout << score << " ";}cout << endl;}
};int main() {ExamScores<double> myScoresD; // 创建一个存储double类型成绩的实例// 添加成绩myScoresD.addScore(90.5);myScoresD.addScore(87.2);myScoresD.addScore(78.6);// 打印double类型成绩myScoresD.printScores();ExamScores<int> myScoresI; // 创建一个存储int类型成绩的实例myScoresI.addScore(90);myScoresI.addScore(80);myScoresI.addScore(78);// 打印int类型成绩myScoresI.printScores();ExamScores<std::string> myScoresC;  // 创建一个存储std::string类型成绩的实例myScoresC.addScore("优秀");myScoresC.addScore("良好");myScoresC.addScore("不及格");// 打印成绩myScoresC.printScores();return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

文章解读与仿真程序复现思路——电力自动化设备EI\CSCD\北大核心《考虑碳市场风险的热电联产虚拟电厂低碳调度》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

Java作业6-Java类的基本概念三

编程1 import java.util.*;abstract class Rodent//抽象类 {public abstract String findFood();//抽象方法public abstract String chewFood(); } class Mouse extends Rodent {public String findFood(){ return "大米"; }public String chewFood(){ return "…

IDEA 编码格式设置 UTF-8

IDEA 编码格式设置 UTF-8 1.文件编码设置为UTF-8 Editor > File Encodings 2.编译编码设置为utf-8 Build&#xff0c;Execution&#xff0c;Deployment > Complier > Java Complier 按图中设置&#xff1a;-encoding utf-8

Dynamic Wallpaper for Mac:动态壁纸让桌面更生动

Dynamic Wallpaper for Mac是一款为苹果电脑用户精心设计的动态壁纸软件&#xff0c;它以其丰富的功能和精美的壁纸库&#xff0c;为用户带来了更加生动和个性化的桌面体验。 Dynamic Wallpaper for Mac v17.8中文版下载 这款软件支持多种动态壁纸&#xff0c;用户可以根据自己…

PTA L2-047 锦标赛

题目 解析 把每一场比赛看作满二叉树的一个节点&#xff0c;父节点递归遍历子节点的结果&#xff0c;进行试填。 代码 #include <bits/stdc.h>using i64 long long;struct Node {int win, lose; };void solve() {int k;std::cin >> k;int siz (1 << k);…

【YOLOv8改进[Backbone]】使用MobileNetV3助力YOLOv8网络结构轻量化并助力涨点

目录 一 MobileNetV3 1 面向块搜索的平台感知NAS和NetAdapt 2 反向残差和线性瓶颈 二 使用MobileNetV3助力YOLOv8 1 整体修改 ① 添加MobileNetV3.py文件 ② 修改ultralytics/nn/tasks.py文件 ③ 修改ultralytics/utils/torch_utils.py文件 2 配置文件 3 训练 其他 …

如何查看项目中使用的Qt版本

如何查看项目中使用的Qt版本 1.点击左下角电脑按钮查看Qt版本。 2.点击左侧栏项目按钮查看Qt版本。

apipost、postman等工具上传图片测试flask、fastapi的文件api接口

参考&#xff1a;https://blog.csdn.net/qq_15821487/article/details/119354129 https://www.cnblogs.com/wyxjava/p/16076176.html 选择from-data&#xff0c;下拉选择file上传文件发送即可

MySQL-数据库基础

一、背景与基本使用 首先是登录方式&#xff0c;一般用 mysql -h 127.0.0.1 -P 3306 -u root -p mysql也是一种网络服务。 当然我们在本地登录时可以省去主机ip和端口号。 -h表示我们要登录mysql在哪个ip的主机上&#xff0c; -P表示端口号。 -u表示以谁的身份去登录。…

MyBatis使用PageHelper分页插件

1、不使用PageHelper分页插件 模块名&#xff1a;mybatis-012-page CarMapper接口package org.example.mapper;import org.apache.ibatis.annotations.Param; import org.example.pojo.Car;import java.util.List;public interface CarMapper {/*** 分页查询* param startInd…

【爬虫】多线程爬取图片

多线程爬虫 多线程爬虫概述1.1 多线程的优势1.2 多线程的挑战 设计多线程爬虫1.1 项目设计1.2 项目流程1.3注意事项 总结 多线程爬虫概述 在当今信息爆炸的时代&#xff0c;网络爬虫&#xff08;Web Scraper&#xff09;已成为获取和分析网络数据的重要工具。而多线程爬虫&…

贪吃蛇游戏源码(VS编译环境)

贪吃蛇游戏源码&#xff08;VS编译环境&#xff09; &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;C语言&#x1f353; &#x1f33c;文章目录&#x1f33c; 1. Snake.h 头文件 2. Snake.c 源文件 3. Test.c 头文件 1. Snake.h 头…

Dubbo元数据中心

元数据中心为 Dubbo 中的两类元数据提供了存取能力&#xff1a;地址发现元数据、服务运维元数据。 一、地址发现元数据 Dubbo3 中引入了应用级服务发现机制用来解决异构微服务体系互通与大规模集群实践的性能问题&#xff0c;应用级服务发现将全面取代 2.x 时代的接口级服务发…

C/C++易错知识点(4):static修饰变量和函数

static是C/C中一个非常容易混淆的语法&#xff0c;在不同的地方针对不同的对象有不同的效果。 它在大型项目中有至关重要的作用&#xff0c;需要我们详细研究。 1.变量 所有static修饰的变量的生命周期都是自调用它起到程序结束&#xff0c;期间这些变量都只会初始化一次 ①…

七牛云配置,图片上传、查看的使用(备忘)

修改配置文档 修改新创建的空间的地区名 访问设置为 公开&#xff0c;不然会有访问时间限制 检查 上传和查看的链接是否正确。

STL容器搜索:当直接访问STL容器时,如何执行有效和正确的搜索?

掌握STL容器搜索技巧:在C中实现高效和准确的数据访问 一、简介二、std::vector, std::deque, std::list三、std::map, std::multimap, std::set, std::multiset四、std::string六、总结 一、简介 本文主要了解如何在直接访问c容器时高效地进行搜索。在STL容器中搜索&#xff0…

5_vscode+valgrind+gdb调试程序

需求 项目程序, 读取串口数据, 出现程序崩溃问题valgrind 可以调试定位内存问题: 内存泄漏,非法地址访问,越界访问等内存问题vscode gdb 可视化调试效果, 比命令行简单快捷很多期望使用vscode valgrind gdb 调试程序内存异常, 崩溃退出的问题 环境准备 sudo apt install v…

【个人博客搭建】(5)Sqlsugar实体创建数据库数据

1、在appsettings.json文件中配置SqlServer数据库连接字符串信息。&#xff08;后续考虑添加MySQL数据库等类型&#xff09; "DBS": [/*对应下边的 DBTypeMySql 0,SqlServer 1,*/{"ConnId": "plateau.poetize.2024","DBType": 1,&qu…

14.基础乐理-音级、基本音级、变化音级

音级&#xff1a; 乐音体系中的每一个音&#xff0c;都叫 音级。 基本音级&#xff1a; 基本音级是 CDEFGAB 它们七个&#xff0c;在钢琴上使用白键展示的&#xff0c;没有任何升降号、没有任何重升重降号的。 变化音级&#xff1a; 除了 CDEFGAB 这七个音&#xff0c;都叫变化…

vue 下载文件 处理后台返回的文件流

1. 下载文件很常见&#xff0c;下载成各种格式的也很常见&#xff0c;本质就是后台返回一个文件流&#xff0c;我们前端去处理一下就行&#xff0c;但是如果因为某些条件&#xff0c;没有返回文件流&#xff0c;返回告诉你&#xff0c;文件出现错误了&#xff0c;那我们就需要把…