C++基础——模板初阶

ʕ • ᴥ • ʔ

づ♡ど

🎉 欢迎点赞支持🎉

个人主页: 励志不掉头发的内向程序员

专栏主页: C++语言


文章目录

前言

一、泛型编程

二、函数模板

2.1、函数模板概念

2.2、函数模板格式

2.3、函数模板原理

2.4、函数模板实例化

2.5、模板参数的匹配原则

三、类模板

3.1、类模板的定义格式

3.2、类模板的实例化

总结


前言

本章节所讲的模板是我们C++的一个重大的发明,它使得我们C++突飞猛进,也是从这里开始和C语言拉开了很大的差距。让我们一起来学习一下吧。


一、泛型编程

在我们学了我们的模板之前,如果我们要对不同的类型对象进行交换就得写不同的函数

cpp 复制代码
void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}

void Swap(double& left, double& right)
{
	double temp = left;
	left = right;
	right = temp;
}

void Swap(char& left, char& right)
{
	char temp = left;
	left = right;
	right = temp;
}

像这样,利用函数重载重载我们需要的对象,如果有很多个对象我们就得写很多个这样的函数,但是不知道大家发现没有,这些函数除了类型不同,其他的地方明明就是一模一样,如果每个都要写,那代码量无疑就会变得很大,有没有什么办法可以简化这一行为呢,所以我们的C++为了避免这一情况发生,便发明了泛型编程这一思想。我们的C++提供了一个类似模板的语法,让我们只要提供不同的类型的材料,就能够生成不同类型的产品了,这就是泛型编程。

我们有了模板这个东西,就没有必要去在意这个代码需要什么类型,因为模板代码是通用代码,它适用于任何的类型,它属于我们函数的一种复用。我们的模板就是泛型编程的基础。

二、函数模板

2.1、函数模板概念

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

2.2、函数模板格式

template<class T1, class T2, class T3.......,class Tn>;

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

或者

template<typename T1, typename T2, typename T3.......,typename Tn>;

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

我们刚才的Swap就可以写成

cpp 复制代码
template <class T>
void Swap(T& left, T& right)
{
	T temp = left;
	left = right;
	right = temp;
	cout << left << " " << right << endl;
}

这就是一个模板函数了,这个函数中的T就是我们的模板,在我们的调用时,编译器会去看我们传的实参是什么类型的,就会把我们的T定义成什么类型的,这样就可以同时满足我们刚才3个函数乃至更多函数才能达到的效果了。

cpp 复制代码
int main()
{
	int a1 = 10, b1 = 20;
	cout << a1 << " " << b1 << endl;
	Swap(a1, b1);

	double a2 = 10.1, b2 = 12.2;
	cout << a2 << " " << b2 << endl;
	Swap(a2, b2);

	char a3 = 'A', b3 = 'Z';
	cout << a3 << " " << b3 << endl;
	Swap(a3, b3);

	return 0;
}

这就是我们的函数模板格式。

2.3、函数模板原理

我们的模板本质上就是一个蓝图,它不是一个函数,它是我们编译器用它的方式产生特定具体类型函数的一个模具,本质上就是把这些重复的工作交给编译器去做,方便人的使用。

我们的模板函数在调用阶段,我们的编译器会根据我们的传入的实参类型去推演我们对应的函数类型,就比如我们double类型的实参去使用模板函数时,我们编译器就会去通过我们的实参类型去推演,最终将我们的T确定为double类型,然后生产出一份专门处理double函数的代码,对于我们的其他类型也是如此。

当然我们一个类型T只能推演一个类型,我们不能说传两个类型给T,这样T就不知道该推演成什么类型了。

cpp 复制代码
//此时left是int,而right是double,但它们都是类型T
//不能这样做
template <class T>
void Swap(T& left, T& right)
{
	T temp = left;
	left = right;
	right = temp;
	cout << left << " " << right << endl;
}

int main()
{
	int a1 = 10;
    double b1 = 20.0;
	cout << a1 << " " << b1 << endl;
 
	Swap(a1, b1);


	return 0;
}

如果想要达到传不同类型的效果,我们可以再创建出一个变量出来。

cpp 复制代码
template <class T1, class T2>
void Swap(T1& left, T2& right)
{
	T temp = left;
	left = right;
	right = temp;
	cout << left << " " << right << endl;
}

int main()
{
	int a1 = 10;
    double b1 = 20.0;
	cout << a1 << " " << b1 << endl;
	
    Swap(a1, b1);

	return 0;
}

这样就没有问题了,我们T1推演成int,T2推演成double。

2.4、函数模板实例化

当我们用不同的参数去使用函数模板的行为称为函数模板实例化,函数模板实例化有两种:隐式实例化和显示实例化。

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

就像我们刚才的Swap模板函数,我们传任意参数给模板后,我们的编译器会自动的推演其类型,这就是隐式实例化。

cpp 复制代码
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10;
	double d1 = 10.0;

	Add(a1, d1);

	return 0;
}

该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T, 编译器无法确定此处到底该将T确定为int或者double类型而报错。

注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅

此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化

cpp 复制代码
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10;
	double d1 = 10.0;

	Add(a1, (int)d1);
	return 0;
}

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

cpp 复制代码
int main(void)
{
	int a = 10;
	double b = 20.0;
	// 显式实例化
	Add<int>(a, b);
	return 0;
}

这样我们的编译器发现如果类型不匹配,就会尝试进行隐式类型转换,如果无法转换成功编译器就会报错。

2.5、模板参数的匹配原则

我们的编译器是一个非常懒惰且怕事情的机器人,它不想担责的同时它也不想做事情,就比如说我们的模板参数匹配,我们的非模板函数可以和一个同名的函数模板同时存在,这个模板可以实例化成我们这个非函数模板

cpp 复制代码
// 专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
	return left + right;
}

我们可以看出,我们的函数模板是可以推演成我们自己写的函数的,那我们的编译器会怎么调用呢,是调用我们的函数模板还是我们的函数呢?

对于我们的非模板函数和同名的函数模板,如果其他条件都相同,在调用时会优先调用我们的非函数模板而非从该模板产生出一个实例。但是如果可以产生一个具有更好匹配的函数,那么将会选择模板。

我们假如是传参是int,那我们的编译器看到已经有一个非模板函数就是int类型的参数了,那它就会懒着去在用模板去生成一个了,但是如果有一点点不匹配,那编译器就会去用模板去生成一个新的函数,因为它怕出现问题得担责。

三、类模板

3.1、类模板的定义格式

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

class 类模板名

{

// 类内成员定义

};

或者

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

class 类模板名

{

// 类内成员定义

};

我们可以看到,类模板的定义和函数模板是十分类似的。

cpp 复制代码
template<typename T>
class Stack
{
public:
	Stack(size_t capacity = 4)
	{
		_array = new T[capacity];
		_capacity = capacity;
		_size = 0;
	}
	void Push(const T& data);
private:
	T* _array;
	size_t _capacity;
	size_t _size;
};

同时我们不建议类模板的声明和定义放在不同文件中,这样会导致链接错误。

3.2、类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的

类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。

cpp 复制代码
// Stack是类名,Stack<int>才是类型
Stack<int> st1; // int
Stack<double> st2; // double

总结

以上便是我们的类模板初阶的主要内容,了解了我们的类模板的主要内容,接下来我们就可以去尝试用C++的方法去实现我们的数据结构了,也就是接下来要讲的STL库的内容,我们下一章节再见

🎇坚持到这里已经很厉害啦,辛苦啦🎇

ʕ • ᴥ • ʔ

づ♡ど