模板及泛型编程的概念

今天这篇文章我们来简单地介绍一下c++中相较于c有着巨大提升的一部分——模板。

泛型编程

在了解模板之前,我们首先来了解一下什么是泛型编程。

其实早在学习c语言的时候,我们就已经使用过泛型编程的思想了。泛型的泛就是广泛的意思,对于编程概念来讲:就是一个代码段的思想能够满足复用。

在学习c语言的时候我们就已经很熟悉了,比如对于交换操作,我们有时候发现这个功能很多地方需要用到,但是又不想在每个功能里面都写一次交换操作。所以我们就可以专门实现一个叫int Swap(int* px, int* py)的函数,专门针对于某些需要交换整形的地方。这样子不仅能更好的维护代码可读性,写代码的效率也提高不少。

但是c语言中有一个很大的缺陷就是,一次只能写一个类型的该操作,如果写不同类型还得换不同的名字来写。所以c++提出了函数重载的概念,可以根据输入的数据类型来自动分辨是哪个交换的函数:

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++提出了模板的概念,就可以达到如此效果。

所以我们就得到了泛型编程的定义:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

而模板分为函数模板和类模板,我们下面来依次讲解一下。

template关键字

首先我们得知道,声名模板的格式:
template<class T1, class T2…>
当然class可以替换成typename,二者可以混合使用。使用typename其实是更加好理解的,但是由于这是比较新的标准,大部分还是用class的多。

切记:不能把class替换成struct。

其实和声名函数很像,特别是后面的参数列表,只不过()变成了<>,参数的具体类型变成了对应的模板而已。

然后就可以在这个模板的声名后续写模板函数和模板类了。

模板函数

如何使用

我们先对刚刚的交换函数进行模板构造:

template<class T1>
void Swap(T1& left, T1& right){
   T1 tmp = left;
   left = right;
   right = tmp;
}

我们可以先看看使用效果:
在这里插入图片描述
我们发现是可以达到我们想要的效果的。这就大大的降低了代码的编写长度了。

模板函数的原理

其实模板函数的原理就是一个蓝图:它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。

我们在调试的过程中,会发现无论什么类型的数据好像都是走模板那个流程。但其实并不是这样的,实际上c++的编译器会自动地根据我们传入的数据进行分析,分析出应该使用哪个模板。其实就是做类型推演,实现了半自动化,这些重复的事情就不需要用户来做了。

在这里插入图片描述

就像图中这样,编译器自动做了类型推演的事情罢了。

模板的实例化

模板毕竟是个模板,真正要使用的时候是需要实例化的。也就是需要指定类型。

隐式实例化

但是很多人会疑惑,刚刚交换操作那个部分好像也没什么实例化的样子啊,没有具体类型的声名,就好像直接使用模板了一样。

其实并不然,实际上如果我们不说明的话,就是编译器自行干这个事情,编译器会自己推演类型然后实例化出一个函数使用。

但是有些情况下,隐式实例化是达不到想要的效果的:
在这里插入图片描述
当我们这样声名的时候,编译器就报错了。

这是因为模板中只有一个模板类型T,但是我们输入的参数类型既有int也有double,那么T在接收类型的时候就会不清楚到底是用哪个,所以会报错。

对此有三种解决方案:
1.强制转换数据类型
在这里插入图片描述
这种方法比较简单。但是需要注意的是因为数据类型在转换的时候可能导致数据丢失,所以答案会不一样

2.多声名一个模板类型

template<class T1, class T2>
T1 Add(const T1& left, const T2& right) {  
	return left + right;
}

但是返回值可能得需要自行控制,同时要注意函数重载的一些注意事项。

3.使用显示实例化
显示实例化就是在使用模板函数的时候在后面加一个数据类型的声名,这个我们放在下个部分讲。

显示实例化

在这里插入图片描述
就像如此,当然这中间其实是发生了隐式类型转换的。我们当前就先记住整形转为其他的数据类型是可以隐式类型转换的。还有就是前段时间学的类的隐式类型转换。

如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。

模板参数的匹配原则

这个时候就得想,如果专门写的函数和模板都存在,该怎么调用呢?

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); // 与非模板函数匹配,编译器不需要特化
}

此时这种情况,发现专门为int写的函数其实是可以通过模板类型推演出的,那么调用Add的时候优先使用的就是专门写的那个,因为这样效率会更高。

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

当然如果写成Test当前第二行的样子,非模板函数是没那么好的,所以编译器会自己进行类型推演后调用一个更加合适的那个。但是上述的两个其实调用的都是非模板。这是因为第二个如果使用模板函数是会报错的。

这是因为:非模板函数是可以自动地发生隐式类型转换。但是对于模板来讲,需要自行强转输入的数据类型,或者进行显示实例化,直接限定参数的数据类型发生转换。

类模板

模板既可以声名定义函数,声名写一个类也是可以的。

类模板的定义格式

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

我们可以对以往写的栈进行类模板定义:

#include<stdlib.h>
#include<assert.h>
//对类Stack进行模板
template<class T>
class Stack {
public:
	Stack(int n = 4)
		:_a(new T[n]),
		_top(0),
		_capacity(0)
	{}
	~Stack() {
		delete[] _a;
		_a = nullptr;
		_top = _capacity = 0;
	}

	void Push(const T& x) {//不需要修改入栈的值,所以加const修饰
		//判断扩容
		if (_top == _capacity) {
			T* tmp = new T[_capacity * 2];
			//c++是没有realloc这样函数的功能,所以得自己完成
			memcpy(tmp, _a, sizeof(T) * _top);     
			_a = tmp;
			_capacity *= 2;   
		}
		_a[_top++] = x;
	}

	void Pop() {
		assert(_top > 0);  
		_top--;
	}
private:
	T* _a;
	size_t _top;
	size_t _capacity;
};

c++中没有realloc这样方式的函数,所以扩容后数据转移得自行操作。

还需要注意的是,类模板中的函数虽然可以声名与定义分离,但是不能分开在两个文件。会导致链接错误,这点以后再说。

声名与定义分离的写法:

#include<iostream>
using namespace std;
// 类模版
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;
};
// 模版不建议声明和定义分离到两个文件.h 和.cpp会出现链接错误,具体原因后面会讲
template<class T>//T写成任何字母都没事
void Stack<T>::Push(const T& data)
{
	// 扩容
	_array[_size] = data;
	++_size;
}

当然分离后的那个模板T其实写成任何字母都可以,不影响。因为这只是个代号,做类型推演的时候会被替代的。但是一般来讲还是会写成一样的,方便阅读。

类模板实例化

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

也就是说,对于刚刚的Stack类模板,需要使用的话:

int main() {
	Stack<int> st1;
	Stack<char> st2;
	st1.Push(1); 
	st1.Push(2); 
	st1.Push(3);
	st1.Push(4);
	st1.Push(5);
	
	st2.Push('a');
	st2.Push('b');
	st2.Push('c');
	st2.Push('d');
	return 0;
}

必须要进行显示实例化。只有这样才能用。

很多人会觉得,使用typedef也不是可以达到这样效果吗,只需要改一下数据类型即可。但是这样还是有缺陷,如果我想要同时定义一个int类型数据的栈和char类型数据的栈,使用typedef就不能达到这个效果了。

所以类模板也是有极大的好处的。

本篇文章只是对模板的概念进行一个初步的了解,具体的内容会在后续章节再来细讲。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐