C++模板(函数模板/类模板)

目录

Ⅰ、编程

Ⅱ、函数模板

1、函数模板概念

2、函数模板格式

3、函数模板的原理

4、函数模板的实例化

1.隐式实例化

2.显示实例化

5、模板参数的匹配原则

 ​编辑补充:函数不一定都能推演,但是类模板一定要指定

Ⅲ、类模板

1、类模板的定义格式

2、类模板的实例化

3、类外定义类模板参数

4、模板的分离编译


Ⅰ、编程

泛型编程:不再是针对某种类型,能适应广泛的类型

如下的交换函数:

//交换int类型
void Swap(int& left, int& right)
{
    int temp = left;
    left = right;
    right = temp;
}
//利用C++支持的函数重载交换double类型
void Swap(double& left, double& right)
{
    double temp = left;
    left = right;
    right = temp;
}

使用函数重载虽然可以实现不同类型的交换函数,但是有以下几个不好的地方:

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

那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?

如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材
料的铸件(即生成具体类型的代码),那将会节省许多头发。巧的是前人早已将树栽好,我们只需
在此乘凉。


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

模板分为如下两类:

  • 函数模板
  • 类模板

Ⅱ、函数模板

1、函数模板概念

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

2、函数模板格式

template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表)
{
    //……
}
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
  1.  template 是定义模板的关键字,后面跟的是尖括号 < >
  2.  typename 是用来定义模板参数的关键字
  3.  T1, T2, ..., Tn 表示的是函数名,可以理解为模板的名字,名字你可以自己取。

因此,交换函数就可以这样套用模板:

template<typename T>//或者template<class T>
void Swap(T& left, T& right)
{
    T tmp = left;
    left = right;
    right = tmp;
}

//我们在用 template< > 定义模板的时候,尖括号里的 typename 其实还可以写成 class:
template<class T>     // 使用class充当typename (具体后面会说)
void Swap(T& rx, T& ry) {
	T tmp = rx;
	rx = ry;
	ry = tmp;
}

虽然参数的名字我们可以自己取,但是我们一般喜欢给它取名为 T,因为 T 代表 Type(类型),有些地方也会叫 TPTY,或者 KV结构(key-value-store)

注意事项:函数模板不是一个函数,因为它不是具体要调用的某一个函数,而是一个模板。

"函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。"  —— 《百度百科》

3、函数模板的原理

template<typename T>
void Swap(T& rx, T& ry) {
	T tmp = rx;
	rx = ry;
	ry = tmp;
}
 
int main(void)
{
	int a = 0, b = 1;
	double c = 1.1, d = 2.2;
	char e = 'e', f = 'f';
 
	Swap(a, b);
	Swap(c, d);
	Swap(e, f);
 
	return 0;
}

问题:我上述交换函数调用过程中的Swap都是调用的同一个函数吗?

当然不是,这里我三次Swap不是调用同一个函数,其实我Swap的时候根据不同的类型通过模板定制出专属你的类型的函数,然后再调用, 如下图:

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

补充:

        通俗来理解,可以把模板理解成印章,我们不会把印的模具传递出去,而是印出来的纸;所以这里调用的当然不是模板,而是这个模板造出来的东西。

而函数模板造出 "实际要调用的" 的过程,叫做模板实例化。

编译器在调用之前会干一件事情 —— 模板实例化。

我们下面就来探讨一下模板实例化。

4、函数模板的实例化

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

  • 隐式实例化:让编译器根据实参推演模板参数的实际类型
  • 显示实例化:在函数名后的<>中指定模板参数的实际类型

1.隐式实例化

让编译器根据实参推演模板参数的实际类型

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
	Add(a1, a2); //编译器推出T是int
	Add(d1, d2); //编译器推出T是double
}

但是我调用的时候如若这样就会出错: 

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

如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。这里引发冲突编译器无法确定这里的T到底是int还是double。

此时有两种处理方式:

法一:用户自己来强制转化

int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
    Add(a1, (int)d1); //强制类型转换。或者Add((double)a1, d1);
}

法二:使用显式实例化:(如下)

2.显示实例化

定义:在函数名后的 < > 里指定模板参数的实际类型。

简单来说,显式实例化就是在中间加一个尖括号 < >  去指定你要实例化的类型。(在函数名和参数列表中间加尖括号)

函数名 <类型> (参数列表);

 解决刚才的问题

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
    //显示实例化
	Add<int>(a1, d1); //double隐式类型转换成int 
	Add<double>(a1, d2); 
}

总结:

函数模板你可以让它自己去推,但是推的时候不能自相矛盾。

你也可以选择去显式实例化,去指定具体的类型。

补充:模板支持多个模板参数

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

5、模板参数的匹配原则

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

如下代码会调用哪个Add函数?

#include<iostream>
using namespace std;
//专门处理int的加法函数
int Add(int left, int right)
{
    printf("当然是直接用现成的啦+");
	return left + right;
}
//通用加法函数
template<class T>
T Add(T left, T right)
{
    printf("T");
	return left + right;
}
int main()
{
	Add(1, 2); //会调用哪个Add函数?
}

 

得出结论:编译器在调用时,有现成的就调用现成的,没有就套用模板。当然,我们也有办法强制让编译器走模板函数,如下:

void Test()
{
    Add(1, 2); // 与非模板函数匹配,编译器不需要特化
    Add<int>(1, 2); // 调用编译器特化的Add版本
}

 即用显示实例化会强制用模板

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

 补充:函数不一定都能推演,但是类模板一定要指定

假设有如下的函数模板:

#include<iostream>
using namespace std;
template<class T>
T* func(int n)
{ 
	return new T[n];
}
int main()
{
	//函数模板显示实例化
	int* p=func(10); 
	//int* p1 = func<int>(10);
	//double* p2 = func<double>(10);
}

报错:

 这里的模板就推不出T的类型。因此我们就要对其显示实例化

#include<iostream>
using namespace std;
template<class T>
T* func(int n)
{ 
	return new T[n];
}
int main()
{
	//函数模板显示实例化
	//int* p=func(10); 
	int* p1 = func<int>(10);
	double* p2 = func<double>(10);
}

因此如果函数模板不能自动推演,就要显示实例化,指定模板参数。

Ⅲ、类模板

1、类模板的定义格式

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

如下的栈示例:

//typedef int STDataType; //C语言的做法
template<class T>
class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = new T[capacity];
		_capacity = capacity;
		_top = 0;
	}
	~Stack()
	{
		delete[]_a;
		_capacity = _top = 0;
	}
private:
	T* _a;
	int _top;
	int _capacity;
};

2、类模板的实例化

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

Vector类名,Vector<int>才是类型
int main()
{
	Stack<int>st1; //int类型
	Stack<double>st2;//double类型
}

上述可以看出类模板是要显示实例化的,而我函数模板是不需要自己显示实例化的,编译器会自动帮我推演

#include<iostream>
using namespace std;

template<class T>
class Stack {
public:
	Stack(T capacity = 4)
		: _top(0)
		, _capacity(capacity) {
		_arr = new T[capacity];
	}
	~Stack() {
		delete[] _arr;
		_arr = nullptr;
		_capacity = _top = 0;
	}
private:
	T* _arr;
	int _top;
	int _capacity;
};

int main(void)
{
	Stack<int> st1;      // 指定存储int
	Stack<double> st2;   // 指定存储double

	return 0;
}

① Stack 不是具体的类,是编译器根据被实例化的类型生成具体类的模具。

template<class T>
class Stack {...};

类模板名字不是真正的类,而实例化的结果才是真正的类。

② Stack 是类名,Stack<int> 才是类型:

Stack<int> s1;
Stack<double> s2;

3、类外定义类模板参数

思考问题:下面的代码为什么会报错?(类定义正确)

#include<iostream>
using namespace std;
template<class T>
class Stack {
public:
	Stack(T capacity = 4)
		: _top(0)
		, _capacity(capacity) {
		_arr = new T[capacity];
	}
	void Push(const T& x);
	// 这里我们让析构函数放在类外定义
	~Stack();
private:
	T* _arr;
	int _top;
	int _capacity;
};

// 类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Stack::~Stack() {   // Stack是类名,不是类型! Stack<T> 才是类型,
	delete[] _arr;
	_arr = nullptr;
	_capacity = _top = 0;
}

void Stack<T>::Push(const T& x) {
	;
}
int main() {
	return 0;
}

解答:

① Stack 是类名,Stack<int> 才是类型。这里要拿 Stack<T> 去指定类域才对。

②每一次声明都要有template <class T>

#include<iostream>
using namespace std;
template<class T>
class Stack {
public:
	Stack(T capacity = 4)
		: _top(0)
		, _capacity(capacity) {
		_arr = new T[capacity];
	}
	void Push(const T& x);
	// 这里我们让析构函数放在类外定义
	~Stack();
private:
	T* _arr;
	int _top;
	int _capacity;
};

// 类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Stack<T>::~Stack() {   // Stack是类名,不是类型! Stack<T> 才是类型,
	delete[] _arr;
	_arr = nullptr;
	_capacity = _top = 0;
}
template<class T>
void Stack<T>::Push(const T& x) {
	;
}
int main() {
	return 0;
}

4、模板的分离编译

模板的声明和定义是可以分离的(前提是声明和定义在一个文件)。像下面这样就可以:

//函数模板的声明
template<typename T>
void Swap(T& left, T& right);
//类模板的声明
template<class T>
class Vector
{
public:
	Vector(size_t capacity = 10);
private:
	T* _pData;
	size_t _size;
	size_t _capacity;
};
 
//函数模板的定义
template<typename T> //定义的时候也要给模板参数
void Swap(T& left, T& right)
{
	T tmp = left;
	left = right;
	right = tmp;
}
//类模板的定义
template<class T> //定义的时候也要给模板参数
Vector<T>::Vector(size_t capacity)
	: _pData(new T[capacity])
	, _size(0)
	, _capacity(capacity)
{}

模板不支持声明和定义放到两个文件中的(xxx.h和xxx.cpp),会出现链接错误。 为什么不支持声明和定义分别放到两个文件呢?

解答:源文件在生成可执行程序的过程会经历预处理、编译、汇编、链接这四大模块

        我template.i在编译后生成对应的.s文件以及后续的.o文件其实都是空的,编译器下不了手,因为无法确定T类型,这也就导致符号表是空的,没有地址。而调用的地方就没问题,因为main函数里头已经实例化显示出了T的类型。随后就去符号表里找到对应函数的地址,但是找不到。所以链接就会出错

解决办法1:针对要使用的模板类型显示实例化指定,你调用函数的地方有哪些类型,就要指定显示实例化哪些类型。加上了显示实例化,此时就能链接上了

(但是这要模板有何用,还不如直接给出确定类型,所以非常不推荐)

解决办法2:将声明和定义放到一个新文件 "xxx.hpp" 里面(其实就是只能在一个文件的意思,没有解决办法~~)

  • 103
    点赞
  • 282
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 42
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

NO.-LL

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

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

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

打赏作者

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

抵扣说明:

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

余额充值