【C++】模板以及模板的特化

C语言对于代码的复用率较低,以交换两数为例:

1.如果我们要交换两个 int 型数据,要写形参为 int 型的函数

void Swapi(int& p1, int& p2)
{
	int tmp = p1;
	p1 = p2;
	p2 = tmp;
}

2.如果我们要交换两个 double 型数据,要写形参为double型的函数,并且还要注意函数名不能重复

void Swapd(double& p1, double& p2)
{
	double tmp = p1;
	p1 = p2;
	p2 = tmp;
}

C++中的函数重载,虽然允许了函数的名字相同,

但实际操作时,我们仍然需要改变形参类型,以应对各种不同类型的实参。

只要有新的类型出现,就仍需写新的函数。

因此,函数重载对于代码的复用率较低这一问题,仍然没有给出较好的改善。


点击查看图片来源

为了解决这一问题,C++提出了模板的概念。

模板,顾名思义,它不是具体的某个函数,就好比冶炼金属时用的模具,工人们往其中浇注液态金属,填充材料,填充的材料不同,最后形成的颜色与大小就不同,但他们大体看上去外观是一致的


模板

模板分为:

  • 函数模板
  • 类模板

函数模板

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

类型版本。

同样还是以交换两数为例

新的关键字:template (模板)

template <class T>
void Swap(T &p1, T &p2)
{
	T tmp = p1;
	p1 = p2;
	p2 = tmp;
}

int main()
{
	int a = 1;
	int b = 2;
	Swap(a, b);
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

}

当然,我们也可以给模板参数一个缺省值

template <class T = int>

class是用来定义模板参数的关键字T是模板参数

class也可以用typename替换,效果是一样的

template <typename T>    //typename
void Swap(T &p1, T &p2)
{
	T tmp = p1;
	p1 = p2;
	p2 = tmp;
}

上述过程利用模板完成了函数的调用,并且是模板的隐式实例化。


模板的实例化

1.类模板与模板类的区别:

类模板是一个模板,模板类是通过模板实例化生成的一个具体的类

如果用印刷术作比喻,那么类模板就是印章模板类就是印章印出来的一本本具体的书


2.未被实例化的类模板,编译器只会检查基本语法是否正确,不会去检查该语法是否可以被执行。

因为编译器本身就只是通过类模板去生成模板类,类模板不占内存空间,只有被实例化出来的模板类才会占空间。

在没有实例化出具体的类时,你可以理解为编译器就当类模板不存在。

如果你是印书的人,你只有在需要印书的时候,才会去检查、发现印章有错误。

因为你在乎你印出来的书是否正确,如果你不需要印这本书,你根本就不用关心印章是否有误


所以对于类模板的成员变量和函数,在不同的情况下,编译器对待它们的方式不同。

  • 如果压根就没有对模板实例化,编译器只会去检查模板内部的语法问题,但不会在乎该语法是否被允许执行。

​ 也就是说,只要你不是乱写一通,都不会报错。只要模板的框架是对的,就不会报错。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kvOY6AQI-1681821416087)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220406202953107.png)]

可以看到,编译通过了,但是正常来说,常量必须赋初值

这里因为我们没有将模板实例化,编译器只检查了语法无误,根本不关心常量是不是要赋初值。

但是如果我们以该类模板创建对象:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hXbAcftt-1681821416088)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220406204112376.png)]

这时候编译器才会检查——没有给常量赋初值

这是编译器对于成员变量的做法。


对于函数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nnJ1DYPF-1681821416089)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220406204516890.png)]

即使我们实例化了类模板,但是编译器依然不检查函数。

函数只有在被调用的时候,才会被实例化,这时候编译器才会去检查函数是否可行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OwYlgV7j-1681821416089)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220406204704959.png)]


模板的实例化分为:

  • 隐式实例化

我们不需要明确地告诉编译器,我们传的实参是什么类型的,编译器会自动根据我们传的实参,

推演出模板参数T应该是什么。

  • 显式实例化

我们需要主动告诉编译器,我们要怎样设置模板参数T。


隐式实例化参考上述样例即可。


显式实例化

上述样例中,我们传的实参都是相同类型的。

template <class T>
void Swap(T &p1, T &p2)  //这里为什么要加const参考“引用”中的临时变量讲解
{
	T tmp = p1;
	p1 = p2;
	p2 = tmp;
}


int main()
{
	int a = 1;
	int b = 2;
	double c = 3.3;
    
	Swap(a, c);  //注意与下面的代码对比该行
    
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4uqGxAGr-1681821416089)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220301171218614.png)]

如果我们还是像刚才那样调用Swap函数,那么编译器就懵了,

上面说过,编译器是根据我们传过来的参数,推导出模板参数应该得是什么类型的

这样一来,当编译器看到传过来的第一个参数是int型的时候,心想:“哦!我的模板参数得是int型。”

然后编译器继续往下读,看到传过来第二个实参是doule型时,编译器傻了。

img

编译器推不出来模板参数的类型了,因为他不知道你到底要它用哪一种类型去执行函数。


因此,**这时候就不能隐式实例化了,**不能再“藏着掖着”了,

要跟编译器摊牌,讲清楚你要用哪个类型的模板参数来实例化出模板类。

这就是为什么有两种实例化方式。


显式实例化:

错误示例:

template <class T>
//void Swap(const T& p1, const T& p2)  //这里为什么要加const,结合隐式类型转换,参考“引用”中的临时变量讲解
//{
//	T tmp = p1;
//	p1 = p2;
//	p2 = tmp;
//}

    
int main()
{
	int a = 1;
	int b = 2;
	double c = 3.3;
    
	Swap<int>(a, c);  //显式实例化,这里实际上传参时要发生隐式类型转换
    //Swap<double>(a, c);  
    
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

}

正确用法:

template<class T>
T Add(const T& a, const T& b)//这里为什么要加const,结合隐式类型转换,参考“引用”中的临时变量讲解
{
	return a + b;
}


int main()
{
	int a = 1;
	double c = 3.3;


	cout << "a + c = " << Add<int>(a, c);
    //显式实例化,这里实际上传参时要发生隐式类型转换

}

需要注意的是,显示实例化通常会涉及强制类型转换。

这里不能再以Swap做示范,因为Swap需要改变两个数的值。

此处显示实例化为int型,本质上是将double型数据强转成int型,临时变量具有常性

为此我们的形参需要以const修饰,才不会发生权限的放大从而导致报错。

而以const修饰值不允许改变。


类模板

相信学习过数据结构的老铁们,对这段代码都不陌生

typedef int STDataType;

为了方便日后要存不同类型的数据时,不用再一个一个修改对应函数里的数据类型,所以我们用了typedef。

但是typedef也有不足的地方:在程序执行的过程中,无法修改,除非将程序停下,修改代码。且得手动修改

因此,我们需要学习类模板来解决这一问题。


以顺序表为例:

template<class T>
class vector
{
	vector() :_sz(0),_capacity(0)
	{
		if (_sz == _capacity)
		{
			_capacity == 0 ? _capacity = 4, _capacity *= 2;
			int newcapacity = _capacity;
			T *newarr=new []()
		}
	}
    
    
    void PushBack(const T& x)  //为了省时间,就不写全了,理解就好
	{
		;
	}

    
private:
	T _a;
	int _sz;
	int _capacity;
};

注意:类模板中函数放在类外进行定义时,需要加模板参数列表

类模板只能显式实现,如 vector


非类型模板参数

样例:

#define N 10

template<class T>
class Sample
{
public:

	T a[N];
};

引入模板后,我们可以只用一个模板类,就能在让类中的数组可以有不一样的数据类型存储不同的数据

同时,这里我们借助了 #define 去控制N,这样我们可以在面对不同的需求时,修改数组的大小。


但是!!! 如果现在我想要两个这样的类对象——简称对象A、B

A的数组大小我只需要10就够了,但是B我想要100个,怎么办?

#define N 100

也容易,把N改成100就都满足了。但是这样做有缺点:

  • 对于B来说,刚刚好,没有浪费空间,但是对于A来说,却浪费了90个空间,

    如果我今天要是 A 只要10个,B 要1000个空间呢?那就更浪费了。

  • 程序在运行的时候,我们无法修改代码,也就无法修改N的值了。


为此,我们将模板参数分为两种:

  • 类型模板参数

模板参数所表示的是某种类型。

  • 非类型模板参数

模板参数只能是整型,而不是某种类型,该模板参数会被视为整型常量使用

注意,再次强调!!!非类型模板参数只能是整型(signed int 、 unsigned int),

其他一切类型都不能作为非类型模板参数。

我们对代码进行如下改进:

template<class T = int ,size_t N = 10>  //N为非类型模板参数(int)
class Sample
{
public:

	T a[N];
};

void test2()
{
	Sample<int,10>A;
	Sample<int,100>B;
	cout << "对象A的数组大小: "<< sizeof(A.a)/4 << endl;
	cout << "对象B的数组大小: "<< sizeof(B.a)/4 << endl;
}

这里我给了模板参数缺省值。

结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uOLQPoV2-1681821416091)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220406193945314.png)]

刚才说过了,非类型模板参数只能是整型

为了证明这点,我们不妨试一下使用其他类型:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ykmf1aQJ-1681821416091)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220406194342559.png)]

再有,刚才还提到了,非类型模板参数会被视为整型常量使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1WJZRE3n-1681821416092)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220406205149512.png)]

我们借助函数修改一下N,再调一下函数,报错了。非类型模板参数确实是被视为常量

至于为什么要调函数,讲模板的实例化的时候已经说过了,如果不调函数,不会生成具体的类,编译器检查语法无误,不会报错。

模板的特化

类模板可以很大程度上实现与类型无关的代码,但是在一些特殊的使用场景下,偶尔也会出现问题。

依然是分为函数模板类模板 来讲。

函数模板的特化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-stVEHtwi-1681821416092)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220406210855542.png)]

我们希望的是对比它们的内容是否有所差异,但是很明显,对于两个数组而言,它们的数组名**分别指向栈上的不同空间。**比较它们的指针后,程序给我们的答案是它们不相同。

这里就是由于编译器根据函数模板推演出来的函数并不能很好地反映我们所要表达的

因此对于个别样例,我们需要专门为他们提供函数,这就是函数模板的特化

样例:

template<class T>
bool Compare(const T &l, const T &r)
{
	return l == r;
}


template<>
bool Compare<const char*>(const char* const &l,const char* const &r)
{
	return strcmp(l, r);
}


//bool Compare(const char* l, const char* r)
//{
//	return strcmp(l, r) == 0;
//}



void test()
{
	int a = 1;
	int b = 1;
	cout << Compare(a, b) << endl;

	const char* p1 = "hello";
	const char* p2 = "hello";
	cout << Compare(p1, p2) << endl;

	const char p3[] = "hello";
	const char p4[] = "hello";
	cout << Compare(p3,p4) << endl;
}

函数模板的特化:(两种方式)

template<>
bool Compare<const char*>(const char* const &l,const char* const &r)
{
	return strcmp(l, r);
}
bool Compare(const char* l, const char* r)
{
	return strcmp(l, r) == 0;
}

也就是说,对于编译器推演出来的函数,不符合我们的要求,我们就自己写一个,编译器在调用函数时,

如果有已经存在且参数匹配的函数会优先调用已经存在的函数,不会再去自己推导模板生成。

万事万物都遵循能量最低原则,编译器也是,能躺平就躺平,有现成的干嘛还自己搞一个。

值得注意的是:

  • 以上两种都称为函数模板的特化,虽然第一种更规范,但是更推荐使用第二种

因为第一种虽然正确,但是它的书写条件太过苛刻

它要求你的函数特化在形参的修饰上 必须和原生的函数模板一模一样,

比如原生的函数模板有const,则函数特化也要有const

不能有丝毫的偏差,哪怕只是一个引用&缺少也不行

事实上,如果有小伙伴自己去把上面的代码调试一下,也会发现,如果我们用第一种方式特化,

让他对于指针不再是检查地址,而是检查内容,但是对于数组,它依然没有得到很好的解决。

如果要引用一个数组,则引用的类型必须与数组的类型一致

请注意:对数组的引用 ≠ 对指针的引用,这是两个不同的概念。

本例中,数组的类型应当是 char [6] ,在编译器看来,我们并没有特化出这样的一个形参类型的函数

它依然会去调用原生函数模板,结果依然不正确。

此处,正是因为形参多了一个引用符号,使得编译器要去找一个 形参类型为 char [6] 的函数,

这里可以视为特例,存在引用时,数组名表示整个数组。

但是编译器找不到。如果我们把引用去掉,则我们传过来的不过是数组首元素地址,

其类型为const char* ,刚好能够与我们的特化函数相匹配,结果正确。

> [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RBVD6fiR-1681821416093)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220409100958703.png)]

但是这又与函数特化在形参的修饰上 必须和原生的函数模板一致 的规则相矛盾,除非我们把原生模板的引用去掉。这也就是为什么说这样的书写方式,对于函数特化的条件太过苛刻

原本还想自己尝试写一个数组类型的特化函数出来,但是奈何试来试去,始终不行:

> [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZnGc9FPN-1681821416093)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220409101429546.png)]


至于第二种函数特化的方式,其实就是我们给编译器一个现成的,特殊情况特殊处理,

这种书写方式,几乎可以看做没有限制,不会被原生模板束缚。

只要你的函数名对,编译器就会找你是否匹配。

总结:函数模板的特化,是为了解决编译器推演出的函数逻辑,与我们的预期不符,因此我们对特殊情况做特殊处理,自己写一个给编译器。更推荐以一个独立函数的方式去特化。


类模板的特化

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

//类模板特化
template<class T1, class T2>	  //无论何种类型都收纳
class Luffy
{
public:
	Luffy()
	{
		cout << "Luffy(T1 , T2)" << endl;
	}
};


//全特化,所有类型都已固定
template<>
class Luffy<int, int>			 //只接受你的两个参数都是int型
{
public:
	Luffy()
	{
		//有需求,做不同于原生类模板的处理
		cout << "Luffy(int , int)"<< endl;
	}
};


//偏特化,部分特化
template<class T1>				//无论何种类型都收纳
class Luffy<T1, int>			//只要你的第二个参数是int型
{
public:
	Luffy()
	{
		//有需求,做不同于原生类模板的处理
		cout << "Luffy(T1 , int)" << endl;
	}
};


template<class T2>				 //无论何种类型都收纳
class Luffy<double,T2>			 //只要你的第一个参数是double型
{
public:
	Luffy()
	{
		//有需求,做不同于原生类模板的处理
		cout << "Luffy(double, T2)" << endl;
	}
};



//这也是一种偏特化
template<class T1, class T2>	//无论何种类型都收纳
class Luffy<T1*, T2*>			//只要你的两个参数都是指针
{
public:
	Luffy()
	{
		cout << "Luffy(T1* , T2*)" << endl;
	}

};



template<class T1,class T2>		//无论何种类型都收纳
class Luffy<T1&, T2&>			//只要你的两个参数都是引用
{
public:
	Luffy()
	{
		cout << "Luffy(T1& , T2&)" << endl;
	}
};





void test()
{
	Luffy<char, char> p1;
	Luffy<int, int> p2;
	Luffy<double, char> p3;
	Luffy<int*, int*> p4;
	Luffy<double&, double&> p5;
}

注:函数模板也有全特化和半特化

如上述代码,类模板特化其实是很自由的,可以有很多种组合,按需特化即可


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

阿波呲der

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

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

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

打赏作者

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

抵扣说明:

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

余额充值