C++之模板初阶
片头
哈喽,小伙伴们,好久不见~ ,
古时候,人们对于文化知识的需求不断增长,手抄书籍的方式已经无法满足这种需求。因此,人们开始探索更高效的复制和传播知识的方法-----印刷术。
在写C++程序的时候,我们也会遇到大量且重复的问题,例如:Swap交换函数。如果我们稍微改变一下2个数的类型,那么交换函数又要重新写。
为了解决问题更高效,模板就出现了。
一、泛型编程
Q1:如何实现一个通用的交换函数呢?
//2个数同时为int类型
void Swap(int& left, int& right) {int temp = left;left = right;right = temp;
}//2个数同时为double类型
void Swap(double& left, double& right) {double temp = left;left = right;right = temp;
}//2个数同时为char类型
void Swap(char& left, char& right) {char temp = left;left = right;right = temp;
}
//....
使用函数重载虽然可以实现通用的交换函数,但是有以下几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
- 代码的可维护性比较低,一个出错可能所有的重载均出错
那能否告诉编译器一个模板,让编译器根据不同的类型利用该模板来生成代码呢?
如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的成品(即生成具体类型的代码),那将会节省许多时间。老祖宗们已经帮我们写好啦~
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
二、函数模板
2.1 函数模板的概念
函数模板代表一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.2 函数模板格式
template<typename T1,typename T2,......,typename Tn>
返回值类型 函数名(参数列表)
{
函数体
}
例如,我们写一个函数交换的模板
template<typename T>//typename也可以替换为class
void Swap(T& left, T& right) {T temp = left;left = right;right = temp;
}
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
2.3 函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该做的重复的事情交给了编译器
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
2.4 函数模板的实例化
我们打开反汇编:
可以看到,调用的不是同一个函数
我们刚刚说到,函数模板就像一个蓝图,本身并不是函数,而是编译器根据参数产生具体类型函数的模具。所以使用函数模板就相当于把我们要做的重复的动作交给了编译器完成。
使用函数模板创建不同类型的函数,称为函数模板的实例化。其中,又分为隐式实例化和显式实例化。
(1)隐式实例化
在编译阶段,编译器根据传入的实参类型来推演实例化函数的对应类型,称为隐式实例化。
第3个Add语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参e将T推演为int,通过实参f将T推演为double,但是模板参数列表中只有1个T,编译器无法确定此处到底该将T确定为int还是double类型而报错(简而言之,2个实参类型不同,编译器无法推演T的类型)
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转换出问题,编译器就要背黑锅。
此时有两种处理方式:
(1)用户自行进行强制类型转换
比如:
(2)使用显式实例化
在函数名后面加上<类型>,即可指定模板参数的实际类型,例如:
显式实例化,就不需要编译器来推演类型了,直接使用我们指定的类型,和指定类型不同类的参数就可以进行隐式类型转换。
2.5 模板参数的匹配原则
(1)一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
例如,我们有一个专门处理int类型的Add函数,和一个通用的Add函数
//专门处理int的加法函数
int Add(int left,int right) {return left + right;
}//通用加法函数
template<class T>
T add(T left,T right) {return left + right;
}int main(){Add(1, 2);//与非模板函数匹配,不需要使用函数模板Add<int>(1, 2);//显式实例化指定类型,需要调用函数模板return 0;
}
(2)如果用模板函数实例化的条件和调用非模板函数的条件相同,则优先调用非模板函数而不会进行实例化
第一个Add中的参数既可以调用非模板,也可以用模板函数实例化,所以直接用非模板函数。如果一定要用函数模板,那就显式实例化。
我们一起来看看~
函数模板实例化出的函数与int类型的非模板函数构成重载
(3)模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
三、类模板
3.1 类模板的定义格式
template<class T1,class T2,.......,class Tn>
class 类模板名
{
//类内成员定义
}
对于类模板,为啥我们不能像C语言一样用typedef呢?
因为,typedef只能定义1种类型,不能同时定义2种类型,比如下面这种情况:
如果想同时实现Stack类里面既能存放int类型,又能存放double类型,只有写2个Stack类,一个StackInt类,一个StackDouble类。
但是这2个类除了数据类型不同,类里面的构造函数、析构函数都是一样的。所以代码显得冗余。
现在,我们可以自己定义一个栈的类模板
//类模板
template<class T>
class Stack {
public:Stack(int capacity = 4) {_array = (T*)malloc(sizeof(T) * capacity);if (_array == NULL) {perror("malloc fail!\n");exit(1);}_capacity = capacity;_top = 0;}~Stack() {if (_array) {free(_array);_array = NULL;}_capacity = 0;_top = 0;}
private:T* _array;int _capacity;int _top;
};
此时对于栈的类模板,因为构造函数是全缺省,我们不一定要传参,编译器也就无法推演类型。
所以需要我们进行显式实例化:(函数模板:实参传递给形参,推演模板参数的类型)
emmm,如果,我们想实现声明和定义相分离呢?函数的声明在Stack类里面,函数的定义在Stack类外面,怎么做呢?
在类外定义类模板中的函数时,作用域限定符前不能只用类名,而是要用类型,并且加上模板参数和声明。
最后,类模板只能在同一文件上声明和定义。例如,分别在.h文件和.cpp文件上声明和定义,会出现链接错误。
学到这里,一些以前看不懂的代码也能看懂了,例如:
int main()
{vector<int>v1;for (int i = 0; i < v1.size(); i++) {cout << v1[i] << " ";}cout << endl;return 0;
}
就是实例化出了一个int类型的vector,访问其成员函数size获取数据个数,通过循环迭代和[]运算符重载打印出它的内容
需要提醒的是,vector是类名,vector<int>才是类型
拓展:写一个C++粗略的动态顺序表
template<class T>
class Vector {
public:Vector(int capacity = 10) :_array(new T[capacity]),_capacity(capacity),_top(0){}~Vector();//我们以析构函数为例,在类内声明类外定义void PushBack(const T& data);void PopBack();//...int Size() {return _size;}private:T* _array;int _capacity;int _size;
};template<class T>
Vector<T>::~Vector() {if (_array)delete[] _array;_size = _capacity = 0;
}
3.2 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
int main() {//Vector类名,Vector<int>才是类型Vector<int> s1;Vector<double> s2;
}
片尾
今天我们学习了C++之模板初阶,希望看完这篇文章能对友友们有所帮助!!!
求点赞收藏加关注!!!
谢谢大家!!!