C++深入学习之模板

为什么需要模板

先来看下面一段程序:

cpp 复制代码
int add(int x, int y)
{
	return x + y;
}

double add(double x, double y)
{
	return x + y;
}

long add(long x, long y)
{
	return x + y;
}

string add(string x, string y)
{
	return x + y;
}

//T1 = T2 = T3
T3 add(T1 x, T2 y)
{
	return x + y;
}

可以看到重复率很高,代码很冗余。如果此时可以有一种通用的符号可以代替上述各种类型的话,那这么多的函数我们不就可以用一个函数来代替了吗?

这就是模板出现的原因,它的出现可以简化代码,让程序员少写代码。

还可以解决严格性与灵活性的冲突:

这是因为C++是强类型语言,其声明变量时需要有严格的类型声明,如 int a = 10;

这很严格,却丧失了一种灵活性(如上面说的多个不同类型变量是否可以通过一套模板来完成的例子)。

模板语法形式

cpp 复制代码
//例子:函数模板
template <模板参数列表>
函数的返回类型 函数名字(函数的参数列表)
{

}

//模板的第一种声明形式
template <typename T1, typename T2,...>
//模板的第二种声明形式
template <class T1, class T2,...>
//注意:模板参数列表中typename与class的含义是完全一样(除非用的编译器是2003年以前的,那么typename可能编译器会识别不了)

模板类型

模板就两种类型:函数模板与类模板;

在刚刚的 模板语法形式 一节中提到的例子就是函数模板的形式。

函数模板

cpp 复制代码
template<typename T>
T add(T x,T y){

}

add函数是前文所提到的例子,使用该模板技术之后,我们的 T 类型就可以用来代替上述一大串add函数中的各种类型啦。

函数模板示例 以及 实例化、特化概念

cpp 复制代码
#include <cstddef>
#include <iostream>
#include <string>
#include <string.h>
using namespace std;

//函数模板示例
//template<typename T>//模板参数列表
template<class T> //使用class的效果和typename是一样的
T add(T x, T y){
	cout << "T add(T , T) " << endl;
	return x+y;
}

//函数模板与函数模板之间也是可以进行重载的
template<class T> //使用class的效果和typename是一样的
T add(T x, T y, T z){
	cout << "T add(T , T, T) " << endl;
	return x+y+z;
}

//当我们发现有些时候我们的函数模板并不适用
//需要将函数特例化写出来的时候,就需要采用下面这种形式
//表示这个函数是我们函数模板的一个特化
//模板特化又分为模板的全特化与偏特化(部分特化)
//   全特化:将模板的参数列表中的参数全部以特殊版本的形式写出来(如下面的const char* add函数);
//   偏特化(部分特化):将模板参数列表中的参数类型,至少有一个没有特化出来
template<>
const char* add(const char* ps1,const char* ps2){
	cout << "const char* add(const char*,const char*)" << endl;
	size_t len1 = strlen(ps1);
	size_t len2 = strlen(ps2);
	size_t len = len1+len2+1;
	char* pstr = new char[len]();
	strcpy(pstr,ps1);
	strcat(pstr,ps2);
	return pstr;
}

//上面的函数模板在 经过模板参数列表的推导之后 成为下面的函数,被称为模板函数
//也可以说是从抽象到具象的一种 实例化
//实例化可被区分为 隐式实例化和显式实例化
int add(int x,int y){
	cout << "int ad(int , int) " << endl;
	return x+y;
}

/*不难发现上面两个函数(add(int x,int y) 与 add(T x, T y))之间发生了重载关系
 * 即普通函数与函数模板之间可以进行重载
 * 经测试可以发现 普通函数 是优先于 函数模板 被调用的
 * */

void test(){
	int ia = 3, ib = 4, ic = 5;
	double da = 3.3, db = 8.8;
	string s1 = "hello",s2 = "world";

	//add(ia,ib)这句代码我们并未显式声明ia和ib的类型,是靠编译器进行隐式推导出来的
	//因此这是一种隐式实例化
	cout << "add(ia,ib) = " << add(ia,ib) << endl;
	//而add<double>(da,db)这句代码我们显式声明了da和db的类型
	//因此这是一种显式实例化
	cout << "add(da,db) = " << add<double>(da,db) << endl;
	cout << "add(s1,s2) = " << add(s1,s2) << endl;
	cout << "add(ia,ib,ic) = " << add(ia,ib,ic) << endl;
	
	//模板的特化示例
	const char* str1 = "hebei";
	const char* str2 = "wuhan";
	//如果不进行模板的特化,下面这行代码将报错,因为两个const char*无法进行相加
	cout << "add(str1,str2) = "  <<add(str1,str2) << endl;
}

int main(){
	
	test();

	return 0;
}

模板参数列表参数类型的剖析

函数模板被分为 头文件 与 实现文件 的情况分析

问题研究的就是把函数模板的声明写到头文件中,把其实现写到另外一个文件里去,然后在测试文件里面进行测试一下来分析这种情况。

头文件中声明函数模板:

在实现文件中实现该函数模板:

测试文件中进行测试:

编译运行:

可以发现在函数模板声明与实现分文件存放时编译会出现问题,编译器找不到经过模板参数列表推导后的模板函数的定义。

所以得出一条重要结论:

对于模板而言,不能将头文件与实现文件分开(不能将声明与实现分开,否则会报错)。

但是如果非要将函数模板的头文件与实现文件进行分开编写的话,可以在头文件中去include实现文件:

注意要删去实现文件中的头文件引入嗷,否则会报重定义的问题:

编译运行,此时就没有问题了:

这与inline内敛函数是类似的。

成员函数的函数模板

上面聊的都是非类中成员函数的普通函数模板,现在我们来聊聊类中成员函数的函数模板。

直接看代码示例以及注释解析:

cpp 复制代码
#include <iostream>

using namespace std;

class Point{
	public:
		Point(double dx = 0.0,double dy = 0.0)
		:_dx(dx)
		 ,_dy(dy)
		{
			cout << "Point(double =0.0,double =0.0)" << endl;
		}
		
		//成员函数也是可以设置为模板形式的
		//给模板参数列表设置默认参数long
		template<typename T=long>
		T func(){
			return _dx;
		}

		~Point(){
			cout << "~Point()" << endl;
		}
	private:
		double _dx;
		double _dy;
};

void test(){
	Point pt(1,2);
	//调用时通过<>传递给func函数的模板参数列表告知其T类型为int
	//或者也开以给模板参数列表设置默认类型参数
	//这样才能正常调用
	cout << "pt.func() = " << pt.func<int>();
}

int main(){
	test();
	return 0;
}

可变模板参数 -- C++11新特性

可变模板参数是C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。

基本形式:

cpp 复制代码
template<typename ...Args>//这里的Args被称为模板参数包,表示这里面有N个typename参数

void func(Args...args){ //这里的args被称为函数参数包,功能同上

}

代码示例:

cpp 复制代码
#include <iostream>

using namespace std;

//可变模板参数,传0到任意个参数,参数类型与个数都不确定
//...后面不一定要写Args,只是习惯上那么写
template <typename ...T> //T是模板参数包
void print(T ...t){ //t是函数参数包
	//打印模板参数个数和函数参数个数
	cout << "sizeof...(T) = " << sizeof...(T) << endl;
	cout << "sizeof...(t) = " << sizeof...(t) << endl;
}

//上面的show方法使用到了递归却没有退出条件
//因此我们要手动给一个空的退出条件
void show(){
	cout << endl;
}

//打印参数包中的数据的打印方法
template <typename T,typename ...Args>
void show(T t,Args ...args){
	cout << t << " ";
	//相当于...在args前面时是一个打包参数的过程,即打包操作
	//...在args后面时则变成了一个拆解参数的过程,即解包操作
	show(args...);//递归遍历
}


void test(){
	//通过下面的调用可以发现
	//此时我们可以传递任意多的参数进去
	print();
	print(1,"hello");//int string
	print(1,true,3.3,"helloworld");//int bool double string
}

void test2(){
	show(1,3.3);//递归
	//调用过程:
	//1、第一次调用show时,1是第一个参数,所以被打印,然后后面一坨被打包进下一个show函数中当作参数
	//    cout << 1 << " ";
	//    show(3.3);
	//2、第二次调用时,3.3是第一个参数,所以被打印,后面已经没有参数了,所以调用了空参的show
	//    cout << 3.3 << " ";
	//    show();
	//3、第三次调用时,因为没有参数了,而函数模板的第一个参数是必须要有的,所以这里的show因为
	//   没有参数于是只能去调用无参的同名show函数
	//   cout << endl;
	//   最终打印换行结束递归
	
}

int main(){
	test2();
	return 0;
}

类模板

形式如下:

cpp 复制代码
template<typename T>
class Stack{
	private:
		T* data;
};

类模板示例

其实基本和函数模板没有什么太大分别,只要前面的函数模板研究清楚基本上就没有问题,一点点小的区别直接看代码示例即可:

cpp 复制代码
#include <iostream>

using namespace std;

//类模板
template<typename T,size_t kSize = 10>
class Example{

public:
	Example():_data(new T[kSize]()){
		cout << "Example()" << endl;
	}
	//析构函数我们选择在类外进行实现
	~Example();
private:
	T* _data;
};

//类模板和函数模板的用法基本没有区别,只要注意一下一个点即可
//就是在类外进行成员函数实现时,必须要声明模板嗷
//因为Example是一个类模板,属于一种抽象类型,T是不确定的
//所以我们在类外写其函数实现时也要带上模板参数列表
template<typename T,size_t kSize>
Example<T,kSize>::~Example(){
	if(_data){
		delete[] _data;
		_data = nullptr;
	}
}

void test(){
	Example<int,20> example;
}

int main(){
	test();
	return 0;
}

模板的嵌套

类模板与类模板之间可以嵌套,类模板与函数模板之间也可以嵌套:

cpp 复制代码
template<typename T> //类模板
class A{
	template<typename K> //类模板
	class B{
		template<typename ...Args> //函数模板
		T func(Args ...args){
		
		}
	}
}

模板也可以作参数

就是将模板作为参数放入模板参数列表中:

cpp 复制代码
template<template<class T1> class T2, class T3,int num>

模板注意事项

成员函数模板不能被设计为virtual

成员函数模板不能声明为virtual的原因主要是因为C++中的虚函数机制和模板机制在实现上有一些根本性的差异。

首先,理解一下虚函数的工作原理。在C++中,虚函数用于支持动态绑定,使得在基类中声明的虚函数在派生类中可以被重写。当通过基类指针或引用调用一个虚函数时,运行时系统会根据对象的实际类型确定要调用的函数版本。

然而,模板并不是在编译时实例化,而是在运行时根据实际参数类型进行实例化。这意味着模板实例化的代码通常在编译时就确定了,而不是在运行时。

由于虚函数和模板的这种根本性差异,将成员函数模板声明为virtual会导致一些问题。例如,当你在派生类中为成员函数模板提供一个新的实现时,由于虚函数的动态绑定特性,你可能会覆盖基类中的实现,而不是添加一个新的实现。这显然不是你想要的结果。

因此,为了避免这种混淆和潜在的错误,C++标准规定成员函数模板不能被声明为virtual。如果你需要为不同的类型提供不同的行为,你应该使用模板特化和条件编译来实现,而不是尝试将模板和虚函数结合使用。

小结

模板是C++引入的新特性,也是标准模板库STL的基础,模板有函数模板和类模板之分,两种应用有很多相似之处。

学习模板,最重要的是理解模板定义(函数模板定义、类模板定义)与具体定义(函数定义和类定义)的不同,模板不是定义,要通过实例化(通过模板)或者特化(避开模板)来生成具体的函数或者类定义,再调用函数或者创建类的对象。

模板支持嵌套,这就是说可以在一个模板里面定义另一个模板。以模板(类,或者函数)作为另一个模板(类,或者函数)的成员,也称为成员模板。同时,模板也可以作为另一个模板的参数,出现在类型参数表中。

模板的使用在日常工作当中使用较少,除非是专门做一些库组件开发的会用的比较多,掌握本文的内容应付日常的工作学习已经足够(若想把所有的模板内容全部学会的话那比C++语言本身都还要繁杂),因此能够在看一些开源代码时知道其代码用到模板的时候在干嘛即可,比如每个C++er都会学习的 STL 的源码就应用了大量的模板相关的知识。

相关推荐
若亦_Royi12 分钟前
C++ 的大括号的用法合集
开发语言·c++
eybk2 小时前
Pytorch+Mumu模拟器+萤石摄像头实现对小孩学习的监控
学习
6.942 小时前
Scala学习记录 递归调用 练习
开发语言·学习·scala
守护者1703 小时前
JAVA学习-练习试用Java实现“使用Arrays.toString方法将数组转换为字符串并打印出来”
java·学习
学会沉淀。3 小时前
Docker学习
java·开发语言·学习
Rinai_R4 小时前
计算机组成原理的学习笔记(7)-- 存储器·其二 容量扩展/多模块存储系统/外存/Cache/虚拟存储器
笔记·物联网·学习
吃着火锅x唱着歌4 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
ragnwang4 小时前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习
Web阿成5 小时前
3.学习webpack配置 尝试打包ts文件
前端·学习·webpack·typescript