❤️前言
大家好!今天和大家一起学习关于C++泛型编程和模板初阶的相关知识。
正文
我们之前已经学习了C++中非常重要的一个特性——函数重载,函数重载很好地提高了我们代码的可读性。但是对于适配多种参数的某种函数来说,我们如果使用函数重载就需要编写多个函数来完成不同的需要,这样的话不仅我们写代码会很累,而且写出的代码也会变得冗余。也就是说代码的复用性较差,而且有时难以维护。
那么我们就想:是否能做到我们交给编译器一个模板,它在运行时给我们根据需求创造出具体的事物并很好地完成相应的任务呢?
显然我们的前辈们早就想到了这一点,提前栽下了一颗树,我们只需要在其下乘凉即可,在C++中解决问题的方法就是——泛型编程。
泛型编程的含义是编写与类型无关的通用代码,它是代码复用的一种手段。模板是泛型编程的基础。其中,模板分为函数模板和类模板。
函数模板的概念与基本使用
函数模板的概念:函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
知道了函数模板的概念,我们现在来看看函数模板的基本使用方式:
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表)
{...}
其中,template是模板对应的关键字,typename是模板参数类型对应的关键字,只要我们在尖括号中写入了多个模板参数,我们就可以利用这些种类的模板参数对模板进行描述。具体的使用方式以交换函数Swap来展示:
// 交换任意的相同类型变量 a 、 b
template<typename T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
我们可以看到,T被我们用来当作一种类型来使用,如果我们进行函数调用和传参,那么这个类型就是固定的了,这时编译器就可以帮我们将这个模板转化成一个真实存在的函数,并且进行一系列的使用,这就是函数模板的基本使用方式了。
函数模板的实现原理
根据我们上面对于函数模板的基本了解,我们大概可以得出如下的结论:函数模板是一个蓝图,它本身并不能完成函数的职能,但是它为编译器提供了模板,也就是起一个模具的作用。这种方式将我们本该做的很多重复的事情交给了编译器,减少了代码冗余,提高了写代码的效率。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供 调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然 后产生一份专门处理double类型的代码,对于其他类型也是如此。
函数模型的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为隐式实例化和显式实例化。
隐式实例化:
隐式实例化的意思是编译器根据实参推演模板参数的类型并生成实际函数。现在我们以上面的Swap函数来做例子详细介绍隐式实例化的使用方式:
// 在main函数中执行如下代码:
int main()
{
int a = 0; int b = 1;
int c = 0; double d = 1.0;
// 正确的使用方式
Swap(a, b);
// 交换c、d的这条调用会失败并报错
Swap(c, d);
return 0;
}
当我们像上面那样去写,我们会发现第二条代码出现了问题:
也就是说,使用函数模板隐式实例化出的函数并不能进行对实参的隐式类型转换,不过这也很好理解,因为这里只有一个模板参数T,但是两个实参的类型并不相同,这时编译器也就无法判断它应该生成什么样的函数了。(编译器已经帮我们做了很多事了,所以我们在这里还是老老实实的按照规矩来写吧)
要解决这个问题只有一个方式,就是手动在模板参数列表里加上一个参数,并对Swap函数进行改造:
// 将Swap函数进行改造
template<typename T1,typename T2>
void Swap(T1& t1, T2& t2)
{
T1 tmp = t1;
t1 = t2;
t2 = tmp;
}
显式实例化:
显式实例化是指在调用函数时在函数名后加上<>并在其中指定模板参数的实际类型。以Swap函数为例:
int a = 0; int b = 1;
Swap<int>(a, b);
模板参数的匹配原则
我们可以对模板参数的匹配原则做一个小总结:
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。当然,如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
类模板的定义
类模板的基本定义方式:
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
知道了基本使用方式之后,我们来编写一个顺序表的类模板:
// 动态顺序表
// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Vector
{
public :
// 构造函数
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
// 使用析构函数演示:在类中声明,在类外定义。
~Vector();
void PushBack(const T& data);
void PopBack();
// ...
size_t Size() {return _size;}
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
if(_pData)
delete[] _pData;
_size = _capacity = 0;
}
定义类模板时需要注意的是,如果模板的成员在外定义时,应该要加上模板参数列表。而且当我们在外定义类成员时是需要在函数名前加上类名和作用域限定符的,这时对于类模板的区别就是要用显式实例化的形式才表示一个真的类名,例如上面Vector类的析构函数。
类模板的使用
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟 <>,然后将实例化的类型放在 <> 中即可,类模板的名字不是真正的类,而实例化的结果才是真正的类。
具体使用方式如下:
// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;
🍀结语
谢谢大家的阅读,同时希望大家阅读了这篇文章之后能有所收获。祝看到这的大家能够天天开心!