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,一经查实,立即删除!

相关文章

LeetCode 34在排序数组中查找元素的第一个和最后一个位置

LeetCode 34在排序数组中查找元素的第一个和最后一个位置 给你一个按照非递减顺序排列的整数数组nums&#xff0c;和一个目标值target。请你找出给定目标值在数组中的开始位置和结束位置。 如果数组中不存在目标值target&#xff0c;返回 [-1, -1]。 你必须设计并实现时间复…

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

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

【.net core】【sqlsugar】批量更新方法

官方文档&#xff1a;单表更新、更新数据 - SqlSugar 5x - .NET果糖网 泛型类中增加 //更新单个实体 public async Task<int> Update(TEntity entity) {//IgnoreColumns(ignoreAllNullColumns: true)&#xff1a;忽略设置为跳过的列return await _db.Updateable(entity…

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

【数学建模】钻井问题

已知 12口井的坐标位置如下: x[0.50,1.41,3.00,3.37,3.40,4.72,4.72,5.43,7.57,8.38,8.98, 9.50]; y[2.00,3.50,1.50,3.51,5.50,2.00,6.24,4.10,2.01,4.50,3.41,0.80];设平面有n个点 P i P_i Pi​(表旧井井位),其坐标为 ( a i , b i ) , i 1 , 2 , … , n (a_i,b_i),i1,2,…,…

嵌入式学习57-ARM7(字符设备驱动框架led)

kernel 内核 printk 内核打印 cat /proc/devices mknod ? 查看指令 gcc -oapp hello.c

动态库作用举例

1.定义解析 符号地址&#xff1a; 符号地址是指代码中定义的函数、变量或其他标识符的内存地址。在程序编译和链接的过程中&#xff0c;这些符号会被编译器和链接器分配一个具体的内存地址。 每个符号在程序的执行过程中都有一个唯一的地址&#xff0c;用于指示它在内存中的位…

fastapi写一个上传的接口

首先&#xff0c;确保您已经在 Python 环境中安装了 FastAPI。 安装环境&#xff1a; pip install fastapi uvicorn让我们创建一个图片上传的接口&#xff1a; from fastapi import FastAPI, File, UploadFile from fastapi.responses import JSONResponse import shutil im…

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

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

AirServer投屏软件

AirServer下载:https://souurl.cn/7xWmKW AirServer是一款功能强大的屏幕镜像接收器&#xff0c;它适用于Mac和PC&#xff0c;允许用户接收来自iOS、Android、Mac和Windows等设备的AirPlay和Google Cast流。这款软件可以让用户将手机或平板电脑的屏幕内容无线投射到电脑上&…

2024.4.19力扣每日一题——准时抵达会议现场的最小跳过休息次数

2024.4.19 题目来源我的题解方法一 动态规划浮点数精度方法二 动态规划不考虑浮点数精度问题 题目来源 力扣每日一题&#xff1b;题序&#xff1a;1883 我的题解 方法一 动态规划浮点数精度 参考官方题解。 用 f[i][j]表示经过了 dist[0]到 dist[i−1]的 i 段道路&#xff0…

ORACLE错误提示概述

OceanBase分布式数据库-海量数据 笔笔算数 保存起来方便自己查看错误代码。 ORA-00001: 违反唯一约束条件 (.) ORA-00017: 请求会话以设置跟踪事件 ORA-00018: 超出最大会话数 ORA-00019: 超出最大会话许可数 ORA-00020: 超出最大进程数 () ORA-00021: 会话附属于其它某些进程…

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表示以谁的身份去登录。…

笔记:Python顺序结构 练习题

文章目录 前言一、什么是顺序结构&#xff1f;二、练习题1.题目2.填空题3.简答题4.编程题 总结 前言 本次笔记旨在帮助读者加深对Python编程语言中顺序结构和选择题的理解。在学习Python编程过程中&#xff0c;掌握程序的基本结构以及条件语句的使用至关重要。通过本次练习题&…

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…