目录
一、泛型编程
二、函数模板
1.函数模板的概念
2.函数模板的格式
3.函数模板的原理
4.函数模板的实例化
>>1.隐式实例化:让编译器根据实参推演模板参数的实际类型
>>2.显式实例化
5.模板参数的匹配原则
6.模板类型参数:类型参数和非类型参数
三、类模板
1.类模板的定义格式
2.类模板的实例化
3.顺序表的实现
四、特化
1.函数模板的特化
2.类模板的特化
>>1.全特化
>>2.偏特化
如何实现一个交换函数呢?
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;
}
.........
如上图,我们可以利用函数重载的方式实现多种类型的交换函数(int,double,char)但是这有几个缺点:
况且我们把类似的代码写那么多份不累吗?
在现实生活中,我们在流水线生产的时候,有时候只需要做一个模具,然后我们只需要将原材料投放进去就可以产生相应的各种各样材料不同但是样式一样的产品了。
比如:我们现在需要制造一批材料不同的勺子,我们分为以下几个步骤:
1.根据勺子的样子制作出模具;
2.将各种不同的材料分别导入模具中(金,银,铜);
3.得到了(金,银,铜)材料不同但是大小样式完全一样的勺子;
那么如果C++也可以像造勺子一样用模具,那是不是会很简单,这就是我们接下来要说的模板
我们先说一下泛型编程:
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被模板化,根据实参类型产生函数的特定类型版本。
template
返回值类型 函数名(参数列表{}
#include
using namespace std;
template
void Swap( T& left, T& right)
{
const T temp = left;
left = right;
right = temp;
}
int main()
{
int a = 1;
int b = 2;
Swap(a, b);
// Swap(1.0, 2.0);
// Swap('a', 'b');
return 0;
}
上面的代码之中我们只给定了模板,并没有任何类型的交换函数,但是却可以正常执行,通过反汇编查看:
我们发现,编译器根据我们给的a,b的类型推测出是整形的交换,编译器帮我们构造了整形的交换函数,并进行调用。(注意:这个过程是不对用户显现的)
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
函数模板是一个蓝图,它本身并不是函数,是编译器用来产生特定具体函数的模具。所以其实模板就是将本来我们做的重复的事情交给了编译器。
比如:我们实现多种类型的加法函数:
template
T Add(const T &left, const T &right)
{
return left + right;
}
int main()
{
Add(1, 2);
Add(1.0, 2.0);
Add('a', 'b');
return 0;
}
上面的代码中我们并没有实现任何类型的加法,仅仅是给出了模板,运行之后我们得到了自己想要的结果,通过反汇编查看
我们发现编译器为我们实现了相应的加法函数。
既然对于内置类型可以,那么对于自定义类型呢?我们再来试试
class Complex
{
public:
Complex(double real = 0.0, double image = 0.0)
:_real(real)
, _image(image)
{}
Complex operator + (const Complex &c)const
{
return Complex(_real + c._real, _image + c._image);
}
private:
double _real;
double _image;
};
template
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
//Add(1, 2);
//Add(1.0, 2.0);
//Add('a', 'b');
Complex c1(1.0, 2.0);
Complex c2(3.0, 4.0);
Complex c3;
c3 = Add(c1, c2);
return 0;
}
我们发现,它也是可以 根据模板帮我们构造类类型的函数的。
总结:在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
用不同类型的参数使用模板函数时,称为模板的实例化。模板参数实例化分为:隐式实例化和显示实例化
例如我们上面写的,都是隐式实例化,但是对于下面的这种情况就不能隐式实例化了
template
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a = 1;
double b = 2.0;
Add(a,b);
Add<>(1,2);//这也是隐式实例化
/*
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
编译器无法确定此处到底该将T确定为int 或者 double类型而报错
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
Add(a1, d1);
*/
return 0;
}
对于上面的这种情况只能我们用户进行手动的实例化了,这就是显示实例化
template
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a = 1;
double b = 2.0;
//Add(a,(int)b);
Add(a, b);
return 0;
}
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
>>1.一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板的函数
// 专门处理int的加法函数
int Add(int left, int right) {
return left + right;
}
// 通用加法函数
template T Add(T left, T right) {
return left + right;
}
void Test()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
Add(1, 2); // 调用编译器特化的Add版本
}
int main()
{
Test();
return 0;
}
通过打断点的方式我们可以发现当与非模板函数匹配的时候,编译器已经不需要进行特化了。
>>2.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生一个实例。如果模板可以产生一个更好匹配的函数,那么将选择模板。
template
class 类模板名
{
// 类内成员定义
};
// Vector类名,Vector才是类型
Vector s1;
Vector s2;
template
class Vector
{
public:
Vector(size_t initCapacity = 3)
:_array(new T[initCapacity])
,_capacity(initCapacity)
,_size(0)
{}
bool arrayFull()
{
return _capacity == _size;
}
void GetCapacity()
{
//1.空间扩容
int newcapacity = _capacity * 2;
//2.新空间的申请
T* newarray = new T[newcapacity];
_capacity = newcapacity;
//3.旧空间的拷贝
//strcpy(newarray, _array);
for (size_t i = 0; i < _size; i++) {
newarray[i] = _array[i];
}
//4.旧空间的释放
delete[] _array;
_array = newarray;
}
//尾插
void PushBack(const T& date)
{
//空间满了
if (arrayFull()) {
GetCapacity();
};
_array[_size] = date;
_size++;
}
size_t Capacity()
{
return _capacity;
}
//判空
bool Empty()const
{
return 0 == _size;
}
//获取元素个数
size_t Size()
{
return _size;
}
//尾删
void PopBack()
{
if (!Empty()) {
_size--;
}
}
//获取头元素
T& Front();
//获取尾元素
T& Back();
//销毁
~Vector()
{
if (!Empty())
{
delete[] _array;
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
T* _array;
size_t _capacity;
size_t _size;
};
template
T& Vector::Back()
{
return _array[_size - 1];
}
template
T& Vector:: Front()
{
return _array[0];
}
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
public:
Date(int year = 1999, int month = 11, int day = 11)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << '-' << d._month << '-'< array;
array.PushBack(1);
array.PushBack(2);
array.PushBack(3);
array.PushBack(4);
cout << array.Front() << endl;
cout << array.Back() << endl;
cout << array.Size() << endl;
cout << array.Capacity() << endl;
Vectors;
Date d1(2022, 3, 31);
Date d2(2022, 3, 30);
Date d3(2022, 3, 29);
s.PushBack(d1);
s.PushBack(d2);
s.PushBack(d3);
cout << s.Front() << endl;
cout << s.Back() << endl;
cout << s.Size() << endl;
cout << s.Capacity() << endl;
return 0;
}
对于下面这种情况我们依靠编译器的类型推演是不够的:
上面的结果我们发现了整形的比较和浮点型的比较都是正确的,但是字符串的比较却是错误的,这是为什么呢?这是因为他们底层直接按地址进行比较的,所以输出的是高地址的。
对于这种情况我们有两种方法:
1.直接把这种函数对应的普通函数写出来
2.对该函数模板进行特化
模板特化步骤:
注意:
1.函数模板一般不需要对其特化,如果模板那种类型的处理是错误,直接将该类型对应的普通函数写出来,不建议写特化函数,因为很复杂,有时会报各种奇奇怪怪的错误。
2.函数模板只有全特化,没有偏特化。
全特化即是将模板列表中所有参数都具体化
形式1:部分特化,将模板参数表中的一部分进行参数特化
如上图,无论第一个参数是什么,只要第二个参数是int类型就走偏特化的版本。
形式2:对参数更严格一点,偏特化并不仅仅是特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个版本。
如上图,当参数传指针的时候走的是指针的特化版本。
类模板特化用途:类型萃取-------类模板特化的应用