目录
1. 什么是模板?
如果我们想实现对不同类型的数据进行交换,首先我们想到的是进行重载,但是重载有几个不好的地方:
- 重载的函数只是类型不同,没有使用代码的复用性,并且只要有新的类型出现,就需要添加新的函数
- 代码的可维护性差,如果一处出了错误,则都会发生错误
那么有没有比重载更好的方法呢?,我们可不可以告诉编译器一个模板,让它根据不同类型生成不同函数呢?
这就是模板的特点:编写与类型无关的代码,充分利用了代码的复用性
2. 函数模板
2.1 函数模板的概念
函数模板代表了一个函数集体,该模板与类型无关,根据传入的参数类型生成不同的代码
2.2 函数模板的格式
template<typename T1,typename T2,typename T3.... typename Tn>
返回值类型 函数名(参数列表)
{
//函数体
}
例如:
这样就实现了简单的函数模板
2.3 函数模板的原理
什么是函数模板?
实质上函数模板只是一个蓝图,它本身不是函数,是编译器帮助我们将该蓝图生成了一个又一个的代码,将我们要做的事情,交给了编译器去做。
2.4 函数模板的实例化
用不同类型的参数使用模板,称为函数模板的实例化,模板参数的实例化又分为:显示实例化和隐式实例化。
1. 隐式实例化
如果我们传入了不同类型的参数,编译器会根据传入的类型推出T的类型,这就是隐式实例化。
但隐式实例化也有些问题,如果传入了两个不同的类型,编译器无法推出T的类型是哪一个,又怎么办呢?
这时有两个办法:
- 将其中传入的一个参数强转成另一个相同的类型
2.显示实例化
2.显示实例化
在函数后面加上<>,里面写上传入的数据类型,编译器会生成相应类型的代码。
2.5 模板参数的匹配原理
- 一个非模板函数和一个同名的模板函数可以共存,同时该模板函数还可以实例化称为该非模板参数
2. 对于非模板函数和模板函数,如果其他条件相同,则会优先调用非模板函数,而不会调用 模板函数
为什么会优先调用非模板函数呢?,这是因为会优先调用适合的,并且是已存在的,模板函数还需要进行实例化才能调用
3. 类模板
那既然函数存在通用的问题,那么类模板也存在着上述问题,如果仅仅只是成员的类型不同就需要重新写一份类,那也太麻烦了,更加违背了代码的复用性,因此C++也提供了对类进行模板的设计。
3.1 类模板的定义格式
template<class T1,class T2,...class Tn>
class 模板名
{
//类成员定义
};
举例:
// 动态顺序表
// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Vector
{
public :
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
// 使用析构函数演示:在类中声明,在类外定义。
~Vector();
void PushBack(const T& data);
void PopBack();
// ...
size_t Size() {return _size;}
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
//类模板中成员函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
if(_pData)
delete[] _pData;
_size = _capacity = 0;
}
在类内声明,类外定义时,必须要添加模板参数列表,让编译器识别此处是一个模板,并且在实现函数时,必须要添加上作用域
3.2 类模板的实例化
类模板的实例化必须在类名后加上<>,在<>里面需要添加实例化的类型,类模板名不是真正的类,加上了传入的类型组合起来才是真正的类。
类模板的实例化和函数模板的实例化不同,函数模板的实例化可以隐式实例化,而类模板只能显示实例化,否则编译器将无法对该模板进行操作。
例如上面的类模板:
vector只是类名
只有类名后面加上了<T>才代表类型
比如:
vector<int>、vector<double>
4. 非类型参数模板
参数模板分为:类型参数模板和非类型参数模板
类型参数:跟在class 和 typenam后面的d名称
非类型参数:用一个变量(常量)来作为模板的参数
非类型参数模板比较典型的就是array:
我们可以发现array的参数列表 中不仅仅是class,还存在size_t类型
非类型参数模板特点:
- 浮点数、类对象和字符串等不能作为非类型参数
- 非类型的参数模板在编译期间就已经确定
5. 模板的特化
5.1 特化的概念
什么是特化?
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型可能会造成一些问题,对于这种类型需要特殊处理
比如:
我们发现当传入地址时,得到的结果并不是我们想要的结果。
我们可以发现当传入的是类型时,结果是正确的,可是当我们传入指针时,结果却和我们所想的不同, 这就属于特殊情况,因此我们需要在这种特殊情况下,对于类模板作出特殊的处理。
这种处理就称为:特化,特化就意味着特殊的处理化,在原模板的基础来,对于传入的特殊类型进行特殊化的实现方式,又因为模板分为函数模板和类模板,因此特化也分为函数特化和类特化。
5.2 函数特化
函数特化的基本步骤:
- 必须要有一个基础的模板,供大部分的非特殊类型使用
- 关键字template后面需要加上<>
- 函数名后跟上一对<>,<>里面写入要特化的类型
- 函数的形参必须和原模板的形参相同
根据上述步骤,我们就可以写出针对特定类型的函数了:
这样就完成了对于特殊类型的特殊化处理。
5.3 类模板的特化
5.3.1 全特化
什么是全特化?
全特化就是模板参数列表中的参数全部确定化
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
//全特化
template<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<int, char>" << endl;
}
private:
int _d1;
char _d2;
};
void TestVector()
{
Data<int, int> d1;
Data<int, char> d2;
}
int main()
{
TestVector();
return 0;
}
我们发现当传入的类型是特定类型时,就会采用特殊的模板生成的类。
5.3.2 偏特化
什么是偏特化?
偏特化指的就是模板参数列表中的一些参数确定化
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
//偏特化
template<class T1>
class Data<T1, char>
{
public:
Data()
{
cout << "Data<T1, char>" << endl;
}
private:
int _d1;
char _d2;
};
void TestVector()
{
Data<int, int> d1;
Data<double, char> d2;
}
int main()
{
TestVector();
return 0;
}
当传入的参数的第二个位置只要是char就会调用偏特化的类模板生成的类。
偏特化有两大应用:
1. 将模板函数列表中的一些参数进行特化
2. 可以对传入参数的进一步限制
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data()
{
cout << "Data<T1*, T2*>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
//两个参数偏特化为引用类型
template <typename T1, typename 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. 模板的优缺点
优点:
1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2. 增强了代码的灵活性
缺点:
1. 模板会导致代码膨胀问题,也会导致编译时间变长
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误