1. 泛型编程
如何实现一个通用的交换函数呢?
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;
}
......
使用函数重载虽然可以实现,但是有一下几个不好的地方:
1)重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数
2)代码的可维护性比较低,一个出错可能所有的重载均出错
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
2. 函数模板
2.1 函数模板的概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.2 函数模板格式
template<typename T1, typename T2,......,typename Tn> 其中:template为关键字 typename为类型的占位
返回值类型 函数名(参数列表){}
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
#include <iostream>
using namespace std;
template <typename T>
void Swap(T& a, T& b) {
T tmp = a;
a = b;
b = tmp;
}
int main() {
int a = 10;
int b = 20;
char c = 'c', d = 'd';
double e = 1.0, f = 2.0;
Swap(a, b);
Swap(c, d);
Swap(e, f);
// Swap(a, f);
// Swap((char)a, f);
system("pause");
return 0;
}
在上面写的为隐式实例化,在上面写的并没有显示提示编译器为什么类型,编译器根据传入参数的类型自己进行推演。但是有些情况下,隐式实例化编译会不通过,例如交换a,f的值,现在Swap只给了一个抽象类型,而现在这两个实参的类型一个为int一个为char,编译器现在不知道该怎么处理,好比两个人讨论问题产生分歧。
现在就需要进行显式实例化,现有两种方式:
1)强制类型转换。强转Swap((char)a, f);这个在编译器上也是不能进行的操作,不是因为它的类型不行,而是作为强转之后,Swap函数参数为引用,将int类型转为char类型引用是无法成功转化的。类似于 char& r =a;引用指向变量本身,而a不为char则不能指向。现在就需要用到所学 常引用,const char& r = a;const char& 这个变量指向的不是a,指向的为a做隐式类型转换指向的临时变量,临时变量具有常性,所以能够成功进行转换。所以需要在void Swap(const T& a, const T& b) 加上const,但是在此场景下不能加const,因为两变量本身就需要进行改变。
2)显示实例化。即可在函数名后的<>中指定模板参数的实际类型,这是一种更为优雅的写法。
2.3 函数模板的原理
模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。模板:代码复用,提高工作效率,但是底层的代码没有减少。
2.4 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
/*
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
编译器无法确定此处到底该将T确定为int 或者 double类型而报错
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
Add(a1, d1);
*/
// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
Add(a, (int)d);
return 0;
}
显式实例化:在函数名后的<>中指定模板参数的实际类型。类型一致时也能进行显示实例化,但实际用处不大。
int main(void)
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
2.5 函数模板参数匹配规则
1)一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
// 专门处理int的加法函数
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); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
}
非模板函数和模板函数:参数类型如果完全匹配,直接调用非模板函数。如果不是完全匹配,此时如果模板可以生成更匹配的代码,则使用模板生成。如果指明要显示实例化,则使用模板生成代码。并且模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
3. 类模板
3.1 类模板的定义格式
// 定义方式1
template <typename T, typename T1, typename T1...>
class Date
{};
// 定义方式2
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
下以日期类举例:
template<class T>
class Date1 {
private:
T _year;
T _month;
T _day;
};
int main() {
// Date1 d;
Date1<char> d1;
Date1<int> d2;
Date1<double> d3;
system("pause");
return 0;
}
现在对Date1 d; 的定义是非法的,因为现在是一个模板类,模板是可以隐式实例化的,但是模板隐式实例化的前提是需要知道参数的类型,现在这样的定义参数类型是未知的,定义什么样的成员也是未知的。
在此模板类的重要应用在于稍后学习的STL,如STL里面的vector、list数据结构(容器)当中可以存放任意类型的数据,不能赋予指定的数据类型,这样就将代码写死了。
template<class T>
class Date1 {
friend ostream& operator<<(ostream& _cout, Date1<T> d);
public:
Date1(T year = 1900, T month = 1, T day = 1)
:_year(year)
, _month(month)
, _day(day)
{
}
private:
T _year;
T _month;
T _day;
};
// cout<<Date1<int>
// Date1<int> << cout
ostream& operator<<(ostream& _cout, Date1<int> d) {
_cout << d._year << "-" << d._month << "-" << d._day << endl;
return _cout;
}
// 顺序表
template <class T>
class Vector {
public:
Vector(int capacity = 20) // 给成T 变为 Date1<int> capacity = 20; 出错了
:_data(new T[20])
,_size(0)
,_capacity(capacity)
{
}
void PushBack(const T& x) {
// check capacity
_data[_size++] = x;
}
~Vector(){
if (_data) {
delete[] _data;
_data = nullptr;
}
}
// 对operator[]来讲,需要两种接口
const T& operator[](size_t pos) const { // 只读
assert(pos < _size);
return _data[pos];
}
// []是一个可读可写的接口,若只传值的话并没有实现[]的所有操作
T& operator[](size_t pos) { // operator是一个可读可写的接口,,即便加上const也不能防止数据改写
assert(pos < _size);
return _data[pos];
}
// 只是一个值传递,在外部修改Size,不影响内部的_size
size_t Size() const {
return _size;
}
void Print() {
for (int i = 0; i < _size; i++) {
cout << _data[i];
}
cout << endl;
}
private:
T* _data; // Date1<int>* _data;
size_t _size; // 具体情况 具体考虑,不要一味写成T泛型,写成泛型还可能出错
size_t _capacity;
};
// 类模板中函数放在类外进行定义时,需要加模板参数列表
template<class T>
void Vector<T>::Print() {
}
template<class T>
void VectorPrint(const Vector<T>& v) {
for (int i = 0; i < v.Size(); i++) {
cout << v[i] << endl; // v.operator[](&v, i)
// v[i] = 3 // 即便是const的operator[]仍然能够对其进行数据修改
// 所以一般operator[]需要两种方式,一个是可读可写的,一个是只读的
cout << v.operator[](i) << endl;
}
}
int main() {
Vector<char> v1;
Vector<double> v2;
Vector<int> v3;
Vector<Date1<int>> v4;
v3.PushBack(1);
v3.PushBack(2);
v3.PushBack(3);
v3.PushBack(4);
v3.PushBack(5);
v1.PushBack('y');
v1.PushBack('l');
v4.PushBack(Date1<int>(2019, 3, 23));
v4.PushBack(Date1<int>(2019, 3, 24));
v4.PushBack(Date1<int>(2019, 3, 25));
v4.PushBack(Date1<int>(2019, 3, 26));
v4.PushBack(Date1<int>(2019, 3, 27));
v4.PushBack(Date1<int>(2019, 3, 28));
v3.Print();
v1.Print();
v4.Print();
VectorPrint(v1);
VectorPrint(v3);
VectorPrint(v4); // VectorPrint<Date1<int>>(Vector<Date1<int>>)
// Vector<Vector<int>> T1-->int T2-->Vector<int>
system("pause");
return 0;
}
在针对类对象的打印时,需要对输出运算符进行重载,而输出运算符又牵扯到友元,因为this占第一个位置,而一般输出cout占第一个位置。若在类内部进行输出运算符重载时,其为Date1类的成员函数,即this占第一个参数。<< 其有两个操作数,一个是 Date1<int>对象,另一个是ostream输出流对象,所以在重载时参数应该少一个。ostream& operator<<(ostream& _cout)并返回_cout进行连续输出,但是不能够按照常理进行使用,定义为成员函数时,this始终为第一个(最左边)的操作数。在类内进行定义是可以使用,但是使用形式很奇怪,很丑。Date1<int> << cout;,若想和原有的无差别,可以将其定义在类外,现在其不能访问该类的私有成员,故将其定义为该类的友元。这就完成了对输出运算符重载,正常打印即可!
模板的打印函数的效率极其高,一个函数覆盖了所有的类型,这就是模板的好处!