【带你吃透C++】模板详解

本文收录于专栏C++

关注作者,持续阅读作者的文章,学习更多知识!
https://blog.csdn.net/weixin_53306029?spm=1001.2014.3001.5343

🎃 引入:泛型编程

泛型编程最初诞生于 C++ 中,由 Alexander Stepanov[2] 和 David Musser[3] 创立。目的是为了实现 C++ 的 STL (标准模板库)。其语言支持机制就是模板( Templates )。模板的精神其实很简单:参数化类型。换句话说,把一个原本特定于某个类型的算法或类当中的类型信息抽掉,抽出来做成模板参数 T 。

我们来简单的理解一下:
假设我们现在需要对两组数中的内容分别进行交换,一组是两个整型,一组是两个浮点型,那我们需要模拟实现两个Swap函数:

//C++的函数重载使得用于交换不同类型变量的函数可以拥有相同的函数名
// 交换两个整型
void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
// 交换两个双精度浮点型
void Swap(double& x, double& y)
{
	double tmp = x;
	x = y;
	y = tmp;
}

很明显,这样有一些缺点:

  • 重载的多个函数仅仅只是类型不同,代码的复用率比较低,只要出现新的类型需要交换,就需要新增对应的重载函数。

  • 代码的可维护性比较低。

针对这些缺点,C++提出了泛型编程,也就是写一种与类型无关的代码,提高代码的复用性;或者说是提供一个模板,让不同类型的代码可以根据模板代码生成与其对应的代码。

我们今天要讲的模板,就是泛型编程的基础。
在这里插入图片描述

🎃函数模板

函数模板的概念

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

函数模板的格式
//T代表的是一种类型
//也可以用class来代替typename
template<typename T1,typename T2,,typename Tn>
返回类型 函数名(参数列表)
{
  //函数体
}

于是,交换两个数的函数可以写成:

template<typename T>//或者template<class T>
void Swap(T& x, T& y)
{
	T tmp = x;
	x = y;
	y = tmp;
}
函数模板的原理

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
在这里插入图片描述
在编译器编译阶段,对于函数模板的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用,彼此的地址也是不同的。比如,当用int类型使用函数模板时,编译器通过对实参类型的推演,将T确定为int类型,然后产生一份专门处理int类型的代码,对于double类型也是如此。

小贴士:

C语言为什么不支持模板,因为它不支持泛型编程(例如C++中的模板),C语言中的宏,void 指针 ,_Generic关键字有类似的作用,但相比模板,就是小巫见大巫了。

函数模板的实例化

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

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

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 20.2;
	
	cout<<Add(a1, a2)<<endl;//编译器根据实参a1和a2推演出模板参数为int类型
	cout << Add(d1, d2)<< endl;//编译器根据实参d1和d2推演出模板参数为double类型
	cout << Add(a1,d2) << endl;//错误
	//注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅

在编译期间,编译器根据实参推演模板参数的实际类型时,根据实参a1将T推演为int,根据实参d2将T推演为double,但是模板参数列表中只有一个T,编译器无法确定此处应该将T确定为int还是double。

针对需要实例化的参数类型不同的问题,我们有两种处理方式:

  • 1.用户自己来强制转化
cout << Add(a1,(int)d2) << endl;
cout << Add((double)a1, d2) << endl;
  • 2.使用显式实例化

二、显示实例化:在函数名后的<>中指定模板参数的实际类型

cout <<Add <int>(a1,d2) << endl;
cout << Add<double>(a1, d2) << endl;
函数模板的匹配原则

一个非模板函数可以和一个同名的函数模板同时存在:

// 专门处理int的加法函数(非函数模板类型)
int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数(函数模板类型)
template<class T>
T Add(T left, T right)
{
	return left + right;
}
void Test()
{
	Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
	Add<int>(1, 2); // 因为使用了显式实例化,所以只能使用函数模板实例化

	
	
	Add(1.1, 2.3);//选择函数模板类型
//两者都可以使用,因为非函数模板类型属于普通函数,普通函数可以隐式地进行自动类型转换
//但是如果使用非函数模板类型,需要类型转换匹配,所以它会优先选择实例化模板生成
}

🎃类模板

类模板的定义格式
template<class T1,class T2,,class Tn>
class 类模板名
{
  //类内成员声明
};

举例:

template<class T >
class Stack
{
//普通类:类名就是类型;
//类模板: 类名不是类型,类型是Stack<T>
public:
	Stack(int capacity=4)
		:_a(new T[acpacity])
		,_top(0)
		,_capacity(capacity)
	{}

	~Stack()
	{
		dalete[] _a;
		_a = nullptr;
		_top = _capacity = 0;
	}

	// 类里面声明,类外面定义
	void Push(const T& x);

private:
	T* _a;
	int _top;
	int _capacity;
};
//类外面定义
template<class T>//类模板中的成员函数若是放在类外定义时,需要加模板参数列表
void Stack<T>::Push(const T& x)
{
	//....
}

注意模板不支持把声明写到.h ,定义写到.cpp这种声明和定义分开的方式,会报链接错误 ,原因我们在[模板分离编译部分]讲。

类模板的实例化

类模板实例化需要在类模板名字后面根<>,然后将实例化的类型放在<>中

int main()
{
	//类模板的使用都是显示实例化
	Stack<TreeNode*> st1;//TreeNode*
	Stack<int> st2;//int

	return 0;
}

模板基础了解到这里就讲完了,下面我们来说说模板进阶的知识----》


🎃非类型模板参数

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

  • 类型形参: 出现在模板参数列表中,跟在class或typename关键字之后的参数类型名称。
  • 非类型形参: 用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

我们来举个例子:
定义一个静态的栈

#define N 10
template<class T>
class Stack
{
private:
	T _a[N];
	size_t top;

};

int main()
{
	Stack<int> sk1;//10
	Stack<int> sk2;//假设我们同时还想要一个大小为1000的栈(sk2),因为N的限制,我们只能定义多个栈
	return 0;
}

我们可以使用非类型模板参数来解决这个问题:

//T是类型模板参数--一般使用整型
//N是非类型模板参数,N是一个常量
template<class T,size_t N>
class Stack
{
private:
	T _a[N];
	size_t top;

};

int main()
{
	Stack<int,10> sk1;//10
	Stack<int,1000> sk2;//1000
	//这样通过控制Stack<int,m> 中的m(m必须是常量)值可以实现多个大小不同的栈
	return 0;
}

//模板参数都可以设置缺省值

template<class T, size_t N=10>
class Stack
{
private:
	T _a[N];
	size_t top;

};

int main()
{
	Stack<int> sk1;//10(使用缺省值:默认给10)
	Stack<int, 1000> sk2;//1000(如果这里给了,不使用缺省值)
	
	return 0;
}

注意

1.非类型模板参数只允许使用整型家族;浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2.非类型的模板参数在编译期就需要确认结果,因为编译器在编译阶段就需要根据传入的非类型模板参数生成对应的类或函数。

🎃模板的特化

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

函数模板特化
//泛型版本
template <class T>
int compare(const T &v1, const T &v2)
{
  if(v1 < v2) return -1;
  if(v2 > v1) return 1;
  return 0;
}
 

对于该函数模板,当实参为两个char指针时,比较的是指针的大小,而不是指针指向内容的大小,此时就需要为该函数模板定义一个特化版本,即特殊处理的版本。

函数模板的特化步骤:

  • 首先必须要有一个基础的函数模板。
  • 关键字template后面接一对空的尖括号<>。
  • 函数名后跟一对尖括号,尖括号中指定需要特化的类型。
  • 函数形参表必须要和模板函数的基础参数类型完全相同,否则不同的编译器可能会报一些奇怪的错误。
//为实参类型 const char * 提供特化版本
template <> 
int compare<const char *>(const char * const &v1, const char * const &v2)
{
  return strcmp(v1, v2);
}
 

解读:

  1. template <> //空模板形参表

  2. compare<const char *> //模板名字后指定特化时的模板形参即const char * 类型,就是说在以实参类型const char * 调用函数时,将产生该模板的特化版本,而不是泛型版本。

  3. (const char * const &v1, const char * const &v2)//可以理解为: const char * const &v1, 去掉const修饰符,实际类型是:char *&v1,也就是v1是一个引用,一个指向char型指针的引用,即指针的引用,加上const修饰符,v1就是一个指向const char 型指针的 const引用,对v1的操作就是对指针本身的操作。

类模板的特化

类模板的特化分为全特化和偏特化。

全特化: 对类模板参数列表的类型全部都确定(明确指定)

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

// 全特化
template<>
class Date<int, double>
{
public:
	Date()
	{
		cout << "Date<int, double>" << endl;
	}
private:
	int _d1;
	double _d2;
};

偏特化: 对类模板的参数列表中部分参数进行确定化分为部分特化和参数进一步限制

部分特化:

// 部分
// 偏特化/半特化
template<class T1>
class Data <T1, char>//只要第二个类型是char就匹配这个
{
public:
	Data() { cout << "Data<T1, char>" << endl; }
private:
};

参数进一步限制 :

// 对模板参数更进一步的条件限制
// 偏特化/半特化:不一定指的是特化部分参数,而是对模板参数类型的进一步限制
template<class T1, class T2>
class Data <T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
private:
};

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

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

我们试着实例化几个对象,看他们用的是哪个模板:

在这里插入图片描述

🎃模板的分离编译

什么是分离编译

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

  • 通俗来说:我们一般创建三个文件,一个头文件(Func.h)用于进行函数声明,一个源文件(Func.cpp)用于对头文件中声明的函数进行定义,最后一个源文件(Test.cpp)用于调用头文件当中的函数。
模板的分离编译

模板的分离编译
上面我们提到了模板不支持把声明写到.h ,定义写到.cpp这种声明和定义分开的方式,会报链接错误 ,这种方式叫做模板的分离编译。那么为什么模板不支持分离编译呢?

我们现在对模板函数(F)和普通函数(Print)进行分离编译:
在这里插入图片描述

报错提示:
在这里插入图片描述

我们看到模板函数(F)显示无法解析找到,所以得出结论:函数模板不能分离编译,普通函数可以。

下面我们对其进行分析:

我们都知道,程序要运行起来一般要经历以下四个步骤:

  1. 预处理: 头文件展开、去注释、宏替换、条件编译等。
  2. 编译: 检查代码的规范性、是否有语法错误等,确定代码实际要做的工作,在检查无误后,将代码翻译成汇编语言。
  3. 汇编: 把编译阶段生成的文件转成目标文件。
  4. 链接: 将生成的各个目标文件进行链接,生成可执行文件。 在这里插入图片描述
  • 预处理阶段需要进行头文件的展开,然后生成.i文件
    在这里插入图片描述
  • 在编译阶段,Test.i中,虽然调用了两个函数,但是因为有头文件的展开,因此也有两个函数的声明,不会报错,然后生成汇编文件。
  • 之后就到达了汇编阶段,此阶段利用 Test.s 和 Func.s 这两个文件分别生成了两个目标文件,对应 Test.o 和 Func.o 两个目标文件。注意,在此之前两个文件各走一条路。
  • 最后到了链接阶段:因为之前的过程没有对模板进行实例化(不知道T的类型),因此没有函数参数,也就没有函数地址,所以在链接时,Test.o文件中调用F函数时,没有函数地址,call调用不到F函数,所以报错。

总的来说就是在链接阶段之前,模板没有进行实例化,因此也没有地址,在链接的时候,调用模板的地方无法通过名字在别的文件中找到模板的地址,也就无法实现模板的分离编译。

解决方法:

1.在模板定义位置显示实例化

不推荐,这样就失去了泛型的特点->因为我们需要用到一个函数模板实例化的函数,就需要对模板手动显示实例化一次,非常麻烦。

2.暴力:不分离编译,将模板的声明和定义都放到一个文件当中。

模板总结

优点:

  1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
  2. 增强了代码的灵活性。

缺陷:

  1. 模板会导致代码膨胀问题,也会导致编译时间变长。
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。

----》好了,C++模板的介绍就到这里,欢迎点赞、关注+收藏~~《----
在这里插入图片描述

  • 29
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 33
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 33
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

…狂奔的蜗牛~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值