C++必修:模版的入门到实践

news2024/11/18 9:44:42

✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C++学习
贝蒂的主页:Betty’s blog

1. 泛型编程

首先让我们来思考一个问题,如何实现一个交换函数?

void swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

相信大家很快就能写出上面这段代码,但是如果要求这个交换函数支持字符型,浮点型…呢?这一点我们可以利用C++中函数重载实现。

void swap(char& x, char& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
void swap(double& x, double& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

但是我们以后如果还需支持字符串,或者其他自定义类型,难道还要在添加一个swap函数吗?这样做不仅麻烦,而且也会使代码变得冗余。

为了解决这个问题,C++就引入了模版这个概念,并且依次延伸出了泛型编程的思想。**泛型编程:**编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

模版简单来说就是像一个道具模子,根据不同的需求,可以产生不同的形态。

img

而模版即可以分为函数模版,又可以分为类模版

2. 函数模版

2.1. 函数模版的用法

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。其具体语法如下:

template<typename T1, typename T2, …, typename Tn>

返回值类型 函数名(参数列表) { }

然后我们就可以用模版定义一个针对不同类型的交换函数:

template<typename T>
void Swap(T& x, T& y)
{
	T tmp = x;
	x = y;
	y = tmp;
}
int main()
{
	int a = 1, b = 2;
	cout << "交换前:" << a << "," << b << endl;
	Swap(a, b);//调用整型
	cout << "交换后:" << a << "," << b << endl<<endl;

	double c = 1.1, d = 2.2;
	cout << "交换前:" << c << "," << d << endl;
	Swap(c, d);//调用浮点型
	cout << "交换后:" << c << "," << d << endl;
	return 0;
}
  • 注意:typename是用来定义模板参数关键字,也可以使用class

img

2.2. 函数模版的原理

学会了如何使用函数模版之后,我们就可能会有一个疑问:那就是不同的类型调用的函数模版是同一个函数吗?这时我们继续利用上面代码,通过调用汇编来观察一下:

img

img

通过反汇编观察我们可以知道,当调用实参类型不同时,调用的函数也不同。当然也是符合逻辑的,不同类型的大小不同,调用的函数栈帧大小也就不同,自然也不可能调用同一个函数。

那么函数模版到底是如何调用的呢?其实也非常简单,函数模版就是一个蓝图,根据不同的参数类型生成对应的函数,只不过这件事我们将它交给了编译器来做了。

img

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

2.3. 函数模版的实例化

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

2.3.1. 隐式实例化

**隐式实例化:**让编译器根据实参推演模板参数的实际类型。

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	//编译器自动推导类型
	cout << (Add(3, 5)) << endl;
	return 0;
}
2.3.2. 显示实例化

但是有些场景下就不能利用隐式实例化的方式调用函数模版,比如说:

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a = 1;
	double b = 1.1;
	cout << (Add(a, b)) << endl;
	return 0;
}

这时编译器就会报错,因为编译器不知道该将T推演成int还是double。为了解决这个问题,就需要让编译器提前知道该将T推演成什么类型,常见有两种解决方法:

一种方法就是先强制类型转换:

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a = 1;
	double b = 1.1;
	cout << (Add(a, (int)b)) << endl;
	return 0;
}

还要一种方式就是显示实例化:在函数名后的<>中指定模板参数的实际类型

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a = 1;
	double b = 1.1;
	cout << (Add<int>(a, b)) << endl;//指定T实例化为int型
	return 0;
}
  • 如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错

除了以上场景需要显式实例化外,还有一种场景也需要我们显式实例化:

template <class T1, class T2>
void Add(T1 x)
{
	T2 y = 10;
	cout << a + b<< endl;
}

因为参数只有一个,所以无法推演T2的类型,因此必须要显式实例化。当然还有一种方式就是给缺省值:

template <class T1, class T2 = int>//默认为int
void Add(T1 x)
{
	T2 y = 10;
	cout << a + b<< endl;
}

2.4. 函数模版的匹配规则

  1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
// 专门处理int的加法函数
int Add(int left, int right)
{
	cout << "int Add(int left, int right)" << endl;
	return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
	cout << "T Add(T left, T right)" << endl;
	return left + right;
}
void Test()
{
	Add(1, 2); // 与非模板函数匹配,编译器不需要特化
	Add<int>(1, 2); // 调用编译器特化的Add版本
}

img

  1. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板.
// 专门处理int的加法函数
int Add(int left, int right)
{
	cout << "int Add(int left, int right)" << endl;
	return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
	cout << "T1 Add(T1 left, T2 right)" << endl;
	return left + right;
}
void Test()
{
	Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
	Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函
}

img

3. 类模版

类模版与函数模版类似,只不过作用对象为类。

3.1. 类模版的用法

template<class T1, class T2, …, class Tn>

class 类模板名

{

​ // 类内成员定义

}

template<class T>
class AA
{
public:
	AA(T a1, T a2)
		:_a1(a1),_a2(a2)
	{
		;
	}
	T Add();
private:
	T _a1;
	T _a2;
};
  • 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
//类外实现
template<class T>
T AA<T>::Add()
{
	return _a1 + _a2;
}

3.2. 类模版的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。所以利用类模版创建对象时必须要对类模版先显式实例化。

void Test()
{
	//创建对象时必须要先实例化
	AA<int> p1(1,2);
	AA<double> p2(1.1,2.2);
}

4. 非类型模版参数

模板参数分为类型形参非类型形参

**类型形参:**出现在模板参数列表中,跟在class或者typename之类的参数类型名称。

**非类型形参:**就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

下面来介绍一种非类型形参的应用场景:

// 定义一个模板类型的静态数组
template<class T, size_t N = 10>
class array
{
public:
	T& operator[](size_t index)
	{
		return _array[index];
	}
	const T& operator[](size_t index)const
	{
		return _array[index];
	}
	size_t size()const
	{
		return _size;
	}
	bool empty()const
	{
		return 0 == _size;
	}
private:
	T _array[N];//定义数组
	size_t _size;
};

通过非类型形参我们实现一些我们以前无法实现的场景,比如说通过传参来控制静态数组的大小。

void Test()
{
	array<int, 10> a;//开辟一个10个整型大小的数组
	array<double, 20> b;//开辟一个20个浮点型大小的数组
}
  • 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
  • 非类型的模板参数必须在编译期就能确认结果。

5. 模版的特化

5.1. 特化的概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板。

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}
	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}
private:
	int _year;
	int _month;
	int _day;
};

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
	return left < right;
}
int main()
{
	cout << Less(1, 2) << endl; // 1.可以比较,结果正确
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl; // 2.可以比较,结果正确
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 3.可以比较,结果错误
	return 0;
}

为什么第3次会出错呢?主要因为地址是一个十六进制的整型,而每次编译时地址都是随机的,所以比较结果也是随机的,为了解决这个问题我们就引入了特化。

即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化类模板特化

5.2. 函数模版的特化

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号 < >
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
	cout << "bool Less(T left, T right)" << endl;
	return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
	cout << "bool Less<Date*>(Date* left, Date* right)" << endl;

	return *left < *right;
}
int main()
{
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl; 
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; 
	return 0;
}

img

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。

bool Less(Date* left, Date* right)
{
	return *left < *right;
}

该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。

5.3. 类模版的特化

类模版的特化与函数模版特化类似,我们直接以代码为例

template<class T1, class T2>
class Data
{
public:
	Data() 
	{ 
		cout << "Data<T1, T2>" << endl; 
	}
private:
	T1 _d1;
	T2 _d2;
};
template<>
class Data<int, char>
{
public:
	Data() 
	{
		cout << "Data<int, char>" << endl;
	}
private:
	int _d1;
	char _d2;
};
void Test()
{
	Data<int, int> d1;
	Data<int, char> d2;
}

img

5.4. 特化的方式

5.4.1. 全特化

全特化即将模板参数列表中所有的参数都确定化,无论是函数模版还是类模版都是类似的。

//函数模版的全特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
	cout << "bool Less<Date*>(Date* left, Date* right)" << endl;

	return *left < *right;
}
//类模版的全特化
template<>
class Data<int, char>
{
public:
	Data() 
	{
		cout << "Data<int, char>" << endl;
	}
private:
	int _d1;
	char _d2;
};
void Test()
{
	Data<int, int> d1;
	Data<int, char> d2;
}
5.4.2. 偏特化

偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:

template<class T1, class T2>
class Data
{
public:
	Data() 
	{ 
		cout << "Data<T1, T2>" << endl; 
	}
private:
	T1 _d1;
	T2 _d2;
};

偏特化有以下两种表现方式:

  • 部分特化:将模板参数类表中的一部分参数特化。
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
	Data() { cout << "Data<T1, int>" << endl; }
private:
	T1 _d1;
	int _d2;
};
  • 参数更进一步的限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
	Data(const T1& d1, const T2& d2)
		: _d1(d1)
		, _d2(d2)
	{
		cout << "Data<T1&, T2&>" << endl;
	}
private:
	const T1& _d1;
	const T2& _d2;
};

然后我们通过具体调用来观察一下:

void test2()
{
	Data<double, int> d1; // 调用特化的int版本
	Data<int, double> d2; // 调用基础的模板
	Data<int*, int*> d3; // 调用特化的指针版本
	Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}

img

6. 模版的分离编译

分离编译:一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
那么如果我们在实现模版时可不可以进行分离编译呢?假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:

// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T & left, const T & right)
{
	return left + right;
}
// main.cpp
#include"a.h"
int main()
{
	Add(1, 2);
	Add(1.0, 2.0);
	return 0;
}

img

为什么会出现这种情况呢?

主要是因为编译器在编译阶段对于多个源文件是分开单独编译的,在a.cpp文件中并没有对函数Add的实例化,所以函数模版并没有生成对应的函数,所以在最后链接找不到该函数,所以出现链接报错。

img

为了解决这个问题,我们可以选择在a.cpp加入具体声明让其在编译阶段实例化:

//在a.cpp文件加入
template<>
int Add(const int& left, const int& right)//对于int实例化的声明
{
	return left + right;
}
template<>
double Add(const int& double, const int& double)//对于double实例化的声明
{
	return left + right;
}

但是不同类型我们都需要声明一遍,这样大大增加我们写代码的成本,所以并不推荐这种方法。

为了避免出现这类问题,一般来说我们的模版并不推荐分离编译,将声明与定义放在一个文件.h或者.hpp即可。

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

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

相关文章

(超详细)YOLOV7改进-Soft-NMS(支持多种IoU变种选择)

1.在until/general.py文件最后加上下面代码 2.在general.py里面找到这代码&#xff0c;修改这两个地方 3.之后直接运行即可

对撞指针技巧

对撞指针技巧 我们以LeetCode的一道题目来讲解一下对撞指针&#xff1b; LeetCode第27题移除元素&#xff0c;链接如下&#xff1a; https://leetcode.cn/problems/remove-element 如果使用快慢指针 如果使用快慢指针&#xff0c;将会有大量的后面元素赋值给前面元素的操作…

面向二级及二级以下医院的云HIS系统源码,涵盖患者、费用、医嘱、电子病历等核心业务功能。

云HIS系统源码&#xff0c;二级医院信息管理系统源码&#xff0c;电子病历系统 云HIS系统&#xff0c;采用云计算、互(物)联网、大数据、人工智能等现代信息技术&#xff0c;融合医保、医疗、医药全方位的管理和服务&#xff0c;为各类中小型医疗机构&#xff0c;包括二级及二…

【Python机器学习实战】 | 基于K近邻算法和一般线性回归算法对电视剧播放数据进行回归预测

&#x1f3a9; 欢迎来到技术探索的奇幻世界&#x1f468;‍&#x1f4bb; &#x1f4dc; 个人主页&#xff1a;一伦明悦-CSDN博客 ✍&#x1f3fb; 作者简介&#xff1a; C软件开发、Python机器学习爱好者 &#x1f5e3;️ 互动与支持&#xff1a;&#x1f4ac;评论 &…

unity-特效-雷达扫描效果

使用后处理方式制作 using System; using System.Collections; using System.Collections.Generic; using UnityEngine;public class GlobalScanEffect : MonoBehaviour {public float startScanRange 0;public float maxScanRange 20;public float scanWidth 3;public flo…

数据库期末复习知识点

A卷 1. 选择题(30) 2. 判断范式(10) 判断到第三范式 3. 程序填空(20) 4. 分析填空(15) 5. 写SQL(25) 5一题 恶性 B卷 1. 单选(30) 2. 填空 (20) 3. 程序填空(20) 4. 写SQL(30) 知识点 第一章 数据库管理系统(DBMS) 主要功能 数据定义功能 (DDL, 数据定义语言, …

第二证券股市知识:小白炒股是做长线好还是短线好?

关于小白来说&#xff0c;挑选炒长线比炒短线要好一些&#xff0c;其间原因如下&#xff1a; ​ 1、对出资者技能要求相对较低 短线出资&#xff0c;需要出资者对个股的走势掌握比较精确&#xff0c;才干通过高抛低吸赚取必定的差价&#xff0c;否则很容易让散户卖飞个股&am…

快速记忆成百上千个账号密码

在日常生活中&#xff0c;我们不仅需要记忆6位数字的银行卡密码&#xff0c;还需要记忆各式各样网站和应用的账号密码&#xff0c;可能我们自己也不记得曾经注册过多少个账号。账号和密码如此繁多&#xff0c;管理这些账号和密码&#xff0c;也是一个让人头疼的问题。 最原始的…

前两天上线了一个小功能,差点把我们项目搞崩溃

项目场景&#xff1a; 最近一直在迭代公司的系统&#xff0c;业务提出需要增加一个消息通知的功能&#xff0c;对接完需求之后&#xff0c;我们就 开始热火朝天的编码、测试、上线&#xff0c; 就是右上角这个小图标&#xff0c;为了提升用户体验&#xff0c;我们采用每隔20S…

有效利用MRP能为中小企业带来什么?

在离散制造企业&#xff0c;主流的生产模式主要为面向订单生产和面向库存生产&#xff08;又称为预测生产&#xff09;&#xff0c;在中小企业中&#xff0c;一般为面向订单生产&#xff0c;也有部分面向库存和面向订单混合的生产方式&#xff08;以面向订单为主&#xff0c;面…

windows系统docker镜像导出

docker镜像导入导出(windows)_windowdocker下载镜像导出-CSDN博客https://blog.csdn.net/qq_22211217/article/details/93936363

网页设计软件Bootstrap Studio6.7.1

Bootstrap Studio是一个适用于Windows的程序,允许您使用流行的fre***orca Bootstrap创建和原型网站。您可以将现成的组件拖动到工作区并直观地自定义它们。该程序生成干净和语义的PDF、CSS和JS代码,所有Web浏览器都支持这些代码。 Bootstrap Studio有一个漂亮而强大的界面,它…

[个人感悟] MySQL应该考察哪些问题?

前言 数据存储一直是软件开发中必不可少的一环, 从早期的文件存储txt, Excel, Doc, Access, 以及关系数据库时代的MySQL,SQL Server, Oracle, DB2, 乃至最近的大数据时代f非关系型数据库:Hadoop, HBase, MongoDB. 此外还有顺序型数据库InfluxDB, 图数据库Neo4J, 分布式数据库T…

linux系统指令查漏补缺

目录 一.磁盘操作 二.lvm 三.top 4.nohup 一.磁盘操作 1. lsblk -f 显示磁盘和它的相关内容 2.tuen2fs -c -1 /dev/sdx 关闭某个磁盘的自检 3.修改配置&#xff0c;使文件系统不要开机自检 cat /etc/fstab 全0表示开机不自检 全1表示开机自检 同时在这个文件中可添加…

计算机网络面试HTTP篇二

HTTP/1.1 如何优化&#xff1f; 问你一句&#xff1a;「你知道 HTTP/1.1 该如何优化吗&#xff1f;」 我们可以从下面这三种优化思路来优化 HTTP/1.1 协议&#xff1a; 尽量避免发送 HTTP 请求&#xff1b;在需要发送 HTTP 请求时&#xff0c;考虑如何减少请求次数&#xff…

Nature推荐的三种ChatGPT论文写作指令

1. 润色学术论文 ChatGPT学术润色指令&#xff1a; “I’m writing a paper on [topic]for a leading [discipline] academic journal. WhatItried to say in the following section is [specific point]. Please rephrase itfor clarity, coherence and conciseness, ensuri…

C语言---数据结构(1)--时间复杂和空间复杂度计算

1.什么是时间复杂度和空间复杂度 1.1算法效率 算法效率分为时间效率和空间效率 时间效率被称为时间复杂度&#xff0c;而空间效率被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度&#xff0c;而空间复杂度主要衡量一个算法所需要的额外空间&#xff0c;在计算…

麦肯锡:量子传感究竟在何处可以发光发热

量子传感技术已经提供价值&#xff0c;潜在的应用案例可以塑造多个行业。有四种核心技术具有应用前景&#xff1a;固态自旋、中性原子、超导电路和离子阱&#xff0c;它们具有在广泛的物理属性上的传感能力&#xff0c;包括磁场、电场、旋转、温度、重力、时间和压力。选择哪种…

spring cloud Alibaba 整合 seata AT模式

准备工作&#xff1a; 1、MySQL正常安装并启动 2、nacos正常部署并启动 3、下载 Seata-1.4.2 源码包和 seata-server-1.4.2 服务端源码包&#xff08;版本根据自己的需要选择&#xff0c;我这里选择1.4.2&#xff09; 下载地址&#xff1a; Seata&#xff1a;https://gite…

区间问题——905. 区间选点

目录 区间问题 定义 运用情况 注意事项 解题思路 AcWing 905. 区间选点 题目描述 运行代码 代码思路 改进思路 其它代码 代码思路 区间问题 定义 区间通常是指一个连续的范围&#xff0c;可以用数轴上的一段来表示。 运用情况 在数学中广泛用于表示变量的取值范…