1.泛型编程
理解:就是编写与类型无关的通用代码,是代码复用的一种手段。
为什么要实现泛型编程?
我们已经知道在C++中函数可重载,在函数重载有些地方不好使用:代码的复用率比较低;代码的可维护性比较低。因为C++中有模板,所以采用模板可以实现泛型编程,从而生成代码,这样就不需要我们去一个一个重载一些通用的函数。(比如:交换函数)
从这里我们引出模板,模板分为函数模板和类模板。
2.函数模板
理解:简单来说函数模板代表了一个函数家族,此函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
格式:template<typename T1, typename T2,....,typename Tn>
返回值类型 函数名(参数列表){}
举例:
说明:T就是模板函数参数,交换两个相同类型的的值
注意:交换不同类型的值,若模板参数只要一个编译器就会报错,因为编译器也不知道将T推演成什么类型;若模板有两个参数,可以通过交换地址来实现交换。
注意:typename是用来定义模板参数关键字,也可以使用class(不能使用struct代替class)
原理:在编译阶段,编译器根据传递的实参进行类型的推演,产生一份专门处理实参类型的函数。
实例化:分为隐式实例化和显示实例化
理解:用不同类型的参数使用函数模板,即实例化
隐式实例化:让编译器根据实参推演模板参数的实际类型
显示实例化:在函数名后的<>中指定模板参数的实际类型(比如:Swap<int>(a,b));注意:如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
模板参数的匹配原则:
1.一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
2.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生一实例。如果模板可以产生一个具有更好匹配的函数,那么就选择模板函数
3.模板函数不允许自动类型转换,但是普通函数可以进行自动类型转换(隐式类型转换)
3.类模板
格式:template<class T1, class T2,......,class Tn>
class 类模板名 {}
实例化:它与函数模板的实例化不同,类模板实例化需要在类模板名字后根<>,然后实例化的类型放在<>中即可,类模板名字不是真的类,而实例化的结果才是真正的类
类模板实现顺序表:
//模板实现顺序表
template <class T>
class Vector{
public:
Vector(size_t capacity = 20)
:_data(new T[20])
, _size(0)
, _capacity(capacity)
{}
~Vector() {
if (_data){
delete[] _data;
_data = nullptr;
}
}
//插入
void PushBack(const T& x)
{
//先检查 check capacity
assert(_size >= _capacity);
_data[_size++] = x;
}
//删除
void PopBack() {
assert(_size <= 0);
--_size;
}
//有效元素的个数
size_t Size() {
return _size;
}
//[]的运算符重载
T& operator[](size_t pos) {
assert(pos < _size);
return _data[pos];
}
void Print() {
for (int i = 0; i < _size; i++){
cout << _data[i] << "";
}
cout << endl;
}
private:
T* _data;
size_t _size;
size_t _capacity;
};
int main() {
//<int>指定类型,不指定类型,编译器不知道是什么类型
//类模板只能显示实例化
//Vector类名,Vector<int>才是类型
Vector<int> a1;
Vector<char> a2;
Vector<double> a3;
//插入
a1.PushBack(3);
a1.Print();
a2.PushBack('a');
a2.Print();
system("pause");
return 0;
}
注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
注意:类模板中函数放在类外进行定义时,需要加模板参数列表
4.非类型模板参数
模板参数分为:类型形参和非类型形参
类型形参:出现在模板参数列表中,跟下class或者typename之后的参数类型名称
非类型形参:就是一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将改参数当做 常量来使用
注意:1.浮点数、类对象和字符串是不允许作为非类型模板参数的
2.非类型的模板参数必须在编译期就能确认结果
5.模板的特化
理解:由于使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,因此,就需要对模板进行特化。也就是在原模板的基础上,针对特殊类型所进行特化的实现方式。
特化分为:函数模板特化和类模板特化
函数模板特化:
1.必须要先有一个基础的函数模板
2.关键字template后面接一对空的尖括号<>
3.函数名后跟一个对尖括号,尖括号中指定需要特化的类型
4.函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器就会报一些奇怪的错误
注意:一般情况下如果函数模板不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。
类模板的特化:
全特化:将模板参数类表中所有的参数都确定化。
#include <iostream>
using namespace std;
//全特化
//全特化是将模板参数类表中所有的参数都确定化
template <class T, class T>
class Data{
public:
Data(){
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
template <>
class Data<int, char>{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
int _d1;
char _d2;
};
偏特化:任何针对模板参数进一步进行条件限制设计的特化版本(两种表现形式:部分特化和参数更进一步的限制)
部分特化:将模板参数类表中的一部分参数特化
参数更进一步的限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本
//偏特化:任何针对模板参数进一步进行条件限制设计的
//特化版本。
template <class T, class T>
class Data{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
//偏特化有两种表现方式:
//部分特化:将模板参数表中的一部分参数特化
template <class T>
class Data<T, int>{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T _d1;
int _d2;
};
//参数更进一步限制
//两个参数偏特化为指针类型
template <class T1, class T2>
class Data<T1*, T2*>{
public:
Data()
{
cout << "Data<T1*, T2*>" << endl;
}
private:
T1* _d1;
T2* _d2;
};
//两个参数偏特化为引用类型
template <class T1, class T2>
class Data<T1&, T2&>{
public:
Data(const T1& d1, const T2& d2)
:_d1(d1)
, _d2(d2)
{
cout << "Data<T1&, T2&>" << endl;
}
private:
const T1& _d1;
const T2& _d2;
};
6.类模板特化应用之类型萃取
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
using namespace std;
//为了将内置类型与自定义类型区分开,给出以下两个类
//分别代表内置类型与自定义类型
//代表内置类型
struct TrueType{
static bool Get() {
return true;
}
};
//代表自定义类型
struct FalseType{
static bool Get() {
return false;
}
};
//类模板定义自定类型调用结构体
template <class T>
struct TypeTraits{
typedef FalseType IsPODType;
};
//类模板特化
//将所有的内置类型进行特化
template <>
struct TypeTraits<char>{
typedef TrueType IsPODType;
};
//......
//通过对TypeTraits类模板重写改写方式四中Copy函数模板,来确认所拷贝对象的实际类型
/*
T为int:TypeTraits<int>已经特化过,程序运行就会使用已经特化过的TypeTraits<int>,
该类中的IsPODType刚好为类TrueType,而TrueType中的Get函数返回true,内置类型
使用memecpy方式拷贝
T为string:TypeTraits<string>么有特化过,程序运行时使用TypeTraits类模板,
该类中的IsPODType刚好为类FasleType,而FasleType中的Get函数返回false,
自定义类型使用赋值方式拷贝
*/
template <class T>
void Copy(T* dest, const T *src, size_t size) {
if (TypeTraits<T>::IsPODType::Get()){
memcpy(dest, src, sizeof(T)*size);
}else{
for (size_t i = 0 i < size; ++i){
dest[i] = src[i];
}
}
}
//STL实现的类型萃取
//代表内置类型
struct _true_type{};
//代表自定义类型
struct _fasle_type{};
//类模板
template <class T>
struct _type_traits{
typedef _false_type is_POD_type;
};
//类模板特化
//特化所有的内置类型
template <>
struct _type_traits<int>{
typedef _true_type is_POD_type;
};
//....
//注意:在重载内置类型时,所有的内置类型都必须重载出来,
//包括有符号和无符号
//比如:对于int类型,必须特化三个int/signed int/unsigned int
//在需要区分内置类型的自定义类型的位置,标准库通常都是通过_true_type与_false_type给出一对重载的
//函数:然后用一个通用函数对其进行封装
//注意:第三个参数可以不提名字,改参数最重要的作用是让两个_copy函数形成重载
template <class T>
void _copy(T *dest, T *src, size_t n, _true_type){
memcpy(dest, src, n*sizeof(T);
}
template <class T>
void _copy(T *dest, T *src, size_t n, _false_type) {
for (size_t i = 0; i < n; ++i){
dest[i] = src[i];
}
}
template <class T>
void Copy(T *dest, T *src, size_t n){
_copy(dest, src, _type_traits<T>::is_POD_type());
}
7.模板分离编译
理解:一个程序(项目)由若干源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成但一个可执行文件的过程就是分离编译模式
模板的分离编译:如果把模板的定义和声明还有main函数分别放在不同的文件中,链接时会报错
解决办法:
1.将声明和定义放到一个文件"xxx.hpp"里面或者"xx.h"其实也是可以的。
2.模板定义的位置显示实例化。
8.模板总结
优点:1.模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此产生
2.增加了代码的灵活性
缺点:1.模板会导致代码膨胀问题,也会导致编译时间变长
2.出现模板编译错误时,错误信息非常凌乱,不易定位错误