泛型编程
简单来说就是一个语言机制,我们可以通过一个模板容器,实现不同类型的对象都可以调用这个容器,泛型编程让你编写完全一般化并可重复使用的算法,其效率与针对某特定数据类型而设计的算法相同。
泛型即是指具有在多种数据类型上皆可操作的含义,与模板有些相似。泛型编程最著名的就是STL标准模板库。
函数模板
C++的很多特性都是为了解决C语言的不足而提出来的,模板同样也是。
C语言中如果要实现两个数交换,可以实现一个函数,但是如果是不同类型的数值,比如int和double却是不能使用同一个函数。如下代码:
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int a = 10, b = 20;
Swap(&a, &b);
double c = 10.0, d = 20.0;
return 0;
}
这段代码显然只能实现int类型的交换而c,d的交换只能再写一个函数。
c++有函数重载我们可以用同一个函数名调用不同的函数,但是还是要写两个类似逻辑的函数的。如此C++就引入了模板这个语法。
模板的原理
模板就类似于生活中的模子,通过这个模子可以复刻出来很多东西,比如手机壳,更换颜色或者材料又会生产出来不同的手机壳,但是他们本质都是为一个手机类型生产的,这就是模板,并且模板本身并不是一个手机壳。
所以函数模板本身并不是一个函数,他只是一个蓝图,编译器根据模板实例化出来的才是执行的函数。
这张图就可以展示出模板的原理和作用,编译器根据传的参数实例化出来三段函数。就是编译器干了本来该我们干的重复性的工作。
隐式实例化
template <class T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10,b = 20;
Swap(a, b);
double c = 1.6, d = 2.8;
cout << c << " " << d << endl;
Swap(c, d);
cout << c << " " << d << endl;
return 0;
}
模板关键字template面接上T就是Type的意思,代表了不同的类型,然后这里面的class可以换成typename都是一样的。
这里编译器可以根据我们传过去的参数类型去推导T的类型,然后将模板实例化,就是按照我们写的函数模板实例化出来一个具体类型的函数如int或者double
推演和实例化这个阶段是在预处理阶段完成的,假如实参推演出T的类型为int、char、double、就会实例化出来三段代码,这三段代码构成函数重载。同时这种实例化就叫做隐式实例化
思考一个问题:既然编译器根据传参实例化的函数,那如果传参的两个参数不同呢?
template <class T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10, b = 20;
double c = 1.6, d = 2.8;
Swap(a, c);
return 0;
}
如上,这段代码编译器会报错,因为推演的时候不能确定是根据第一个参数的类型还是根据第二个参数的类型。
但是如果要实现一个参数两种类型,有如下方法,比如实现的是Add函数(因为交换两个不同类型的数字不太合适的。)
template <class T1,class T2>
T2 Add(const T1& x, const T2& y)
{
return x + y;
}
int main()
{
int a = 10, b = 20;
double c = 1.6, d = 2.8;
cout << Add(a, c) << endl;
return 0;
}
可以通过定义两个模板参数来实现,将T1转换成int,T2转换成double,返回值可以是T1或者T2.
第二种方法,将参数强制类型转换:
template <class T1>
T1 Add(const T1& x, const T1& y)
{
return x + y;
}
int main()
{
int a = 10, b = 20;
double c = 1.6, d = 2.8;
cout << Add((double)a, c) << endl;
return 0;
}
比如这里可以将a强转成为double类型或者将c强转成int类型。都是可以实现的,这时候只需要一个模板参数即可。
注意:如果不加强制类型转换是编译不能通过的。但是我们回想以前写的函数是不是不加强转也是可以编译通过的呢?对的,因为这时候发生了隐式类型转换,**但是在使用模板的时候类型转换是不会发生的。**因为一旦转换错误那编译器就要背锅了。
int Add(const int& x, const int& y)
{
return x + y;
}
int main()
{
int a = 10, b = 20;
double c = 1.6, d = 2.8;
cout << Add(a, c) << endl;
return 0;
}
这段代码就是将c隐式类型转换成立int类型,先创建一个临时变量,然后将转换成int类型的c放到临时变量里面然后传参给y,因为临时变量带有常属性不可修改,所以要加const否则也是编译不过的。c的类型是不会改变的。
第三种方法就是显示实例化,这和后面有所联系
template <class T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int a = 10, b = 20;
double c = 1.6, d = 2.8;
cout << Add<double>(a, c) << endl;
return 0;
}
在函数名后加尖括号里面放上想要实例化的类型就可以指定T实例化成什么类型了,这种就是显式实例化。
这时候如果类型不匹配那编译器会进项类型转换,如果转换失败就会报错。
模板匹配规则
1,如果存在和参数匹配的现成的非模板函数,那编译器会直接调用非模板函数不会再去实例化一个新的函数
int Add(const int& x, const int& y)
{
return x + y;
}
template <class T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int a = 10, b = 20;
double c = 1.6, d = 2.8;
cout << Add(a, b) << endl;
cout << Add(a, c) << endl;
cout << Add<int>(a,b)<<endl;
return 0;
}
这里因为存在int,int类型的函数和a,b的类型匹配所以直接调用现成的,如果是第二个也会调用现成的,因为单个参数无法实例化出匹配的,但是现成的这个可以发生类型转换。
当然也可以指定调用模板函数,就是第三个调用。(显式实例化)
2,对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模
板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。(与第一条的第二个调用有关)
int Add(const int& x, const int& y)
{
return x + y;
}
template <class T1,class T2>
T1 Add(const T1& x, const T2& y)
{
return x + y;
}
int main()
{
int a = 10, b = 20;
double c = 1.6, d = 2.8;
cout << Add(a, c) << endl;
return 0;
}
下面的调试图可以看到调用的是模板函数,这时候模板可以生产出来一个第一个参数是int第二个参数是double,不需要发生类型转换,所以这时候就会优先调用模板函数。
3、模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
类模板
类模板的定义
//类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
下面来看一下顺序表的定义格式和使用。
namespace xzj
{
template <class T>
struct vector
{
vector()
:_a(nullptr)
, _size(0)
, _capacity(0)
{}
void push_back(const T& x)
{
if (_size == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
T* tmp = new T[newcapacity];
memcpy(tmp, _a, sizeof(T) * _size);
delete(_a);
_a = tmp;
_capacity = newcapacity;
}
_a[_size] = x;
_size++;
}
int size()
{
return _size;
}
T& operator[](int pos)
{
assert(pos < _size);
return _a[pos];
}
private:
T* _a;
int _size;
int _capacity;
};
}
int main()
{
xzj::vector<int> v1;
v1.push_back(1);
v1.push_back(1);
v1.push_back(1);
v1.push_back(1);
v1.push_back(1);
v1.push_back(1);
v1.push_back(1);
for (int i = 0; i < v1.size(); i++)
{
v1[i] *= 2;
}
for (int i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
return 0;
}
注意点,空间不够扩容的时候要注意释放以前的空间,因为c++没有realloc防止内泄露哟。
还要注意的是如果是声明在类里面,定义在类外面的话,因为模板的参数只能用在类里面,如果是在类外定义的话还需要在每个函数定义上面再次加一个模板的定义,参数都是T。
第二就是为什么要将这个类放在命名空间里面,因为C++本身的stl库就有vector如果引用的vector这个头文件就会报错,所以可以放在单独的一个命名空间保护起来。
下面就是声明在类里面,定义在类外面
namespace xzj
{
template <class T>
struct vector
{
vector()
:_a(nullptr)
, _size(0)
, _capacity(0)
{}
void push_back(const T& x);
T& operator[](int pos);
int size();
private:
T* _a;
int _size;
int _capacity;
};
template <class T>
void vector<T>::push_back(const T& x)
{
if (_size == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
T* tmp = new T[newcapacity];
memcpy(tmp, _a, sizeof(T) * _size);
delete(_a);
_a = tmp;
_capacity = newcapacity;
}
_a[_size] = x;
_size++;
}
template <class T>
T& vector<T>::operator[](int pos)
{
assert(pos < _size);
return _a[pos];
}
template <class T>
int vector<T>::size()
{
return _size;
}
}
这里要注意的是在定义函数的时候前面要指定函数属于那个类,这个类的名字是vector,因为单独的一个vector只是一个模板名,并不是类名,而且因为函数在类外面定义所以编译器不能识别T是模板参数,要在写一次template 让编译器知道这个T是模板参数。
总之就是:类模板中函数放在类外进行定义时,需要加模板参数列表;
模板是不支持声明与定义分离的,就是声明在头文件定义在cpp文件。
类模板的显式实例化
我们可以看到在上面代码的main函数里面是这样写的。
int main()
{
vector<int> v1;
vector<double> v2;
}
这里类模板实例化的时候必须指定实例化的类型也即是这个,并且vector并不是类名,是模板名,实例化出来的vector 这个才是类名。
为什么一定要指定类模板的类型呢?因为这里的模板参数没办法像函数模板哪里根据参数来推演类型,这里只能我们自己指定。