目录
1. 模板
模板就类似于模具。我们可以给模具,根据不同的材料铸成不同的铸件。
那么我们给模板参数不同的类型,就会生成具体类型的代码。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
2. 函数模板
2.1 概念
函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.2 格式
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){ }
注意:typename是用来定义模板参数关键字,也可以使用class(常用,毕竟短),但是不能用struct
template<class T4>
T4 Sum(const T4& left,const T4& right) {
return left + right;
}
2.3 原理
其实模板就是将本来应该我们做的重复的事情交给了编译器,编译器产生特定具体类型函数的模具,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用,每调用一次会产生一个相应类型的函数。
2.4 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
(1)隐式实例化:让编译器根据实参推演模板参数的实际类型
(2)显式实例化:在函数名后的<>中指定模板参数的实际类型
2.5 模板参数的匹配规则
(1)一个非模板函数可以和一个同名的函数模板同时存在,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。
(2)一个非模板函数可以和一个同名的函数模板同时存在,如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
(3)模板函数不允许自动类型转换,只会去生成匹配更符合参数类型的函数,但普通函数可以进行自动类型转换。
(1)的例子:Sum(1,2) 这个时候非函数模板类型完全匹配,会优先调用非模板函数,不需要函数模板实例化
(2)的例子:Sum(1,2.0) 这个时候会模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
3. 类模板
3.1 概念
类模板针对的是类中的成员类型
3.2 类模板格式
template<typename T1, typename T2,......,typename Tn>
class 类模板名 {
// 类内成员定义
};
template<class T2>
class MyVector {
public:
MyVector(size_t capacity)
:arr_(new T2[capacity]),
size_(0),
capacity_(capacity) {}
void PushBack(T2 data) {
arr_[size_] = data;
++size_;
}
void PopBack() {
--size_;
}
size_t getSize() {
return size_;
}
T2& operator[] (size_t pos) {
assert(pos < size_);
return arr_[pos];
}
private:
T2* _arr;
size_t _size;
size_t _capacity;
};
3.3 类模板的实例化
类模板是没有隐式实例化的,所以类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中。类模板名字不是真正的类,而实例化的结果才是真正的类。
int main() {
MyVector<int> t(10); //int类型
MyVector<double> t2(10);//double类型
return 0;
}
4. 非类型模板参数
4.1 概念
类型形参:出现在模板参数列表中,跟在class或者typename后面的参数类型名称。
非类型形参:用一个常量作为类/函数模板的一个参数,在类/函数模板中可将该参数当成常量来使用
template <class T,size_t N = 11>
void Func(T arr[],size_t size = N) {
……
}
// T就是类型参数
// N就是非类型参数,是个常数
注意:
(1)浮点数、类对象以及字符串不允许作为非类型模板参数
(2)非类型的模板参数必须在编译期就能确认结果
5. 模板的特化
5.1 概念
在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化分为函数模板特化与类模板特化。为什么要实现特化。下面举一个例子
template <class T>
T& Max( T& left, T& right) {
return left > right ? left : right;
}
int main() {
int a = 10;
int b = 20;
cout << Max(a, b) << endl;
system("pause");
return 0;
}
对于上面的代码当然是可行的,返回的结果是两个数中较大的数。但是对于下面的代码输出的结果却不是我们预期的。
template <class T>
T& Max( T& left, T& right) {
return left > right ? left : right;
}
int main() {
const char* p1 = "string2";
const char* p2 = "string1";
cout << Max(p1, p2) << endl;
system("pause");
return 0;
}
对于两个字符串的比较,我们一般是把每个字符的ASCII码进行比较。那么下面的代码应该输出string2。但实际输出的却是string1.如果将p2移到p1的上面,输出的就是string2了。没错这里比较的是位置,这俩指针位置就是地址。栈是向下增长的。
对于特殊情况我们需要进行模板的特化。模板的特化分为函数特化和类特化。
5.2 函数特化
(1)必须要先有一个基础的函数模板
(2)特化时,关键字template后面接一对空的尖括号<>
(3)特化时,函数名后跟一对尖括号,尖括号中指定需要特化的类型
(4)函数形参表: 必须要和模板函数的基础参数类型完全相同。
同上面的例子,我们可以利用函数的特化。下面就得到正确的结果。
template <class T>
T& Max( T& left, T& right) {
return left > right ? left : right;
}
template<>
const char*& Max<const char*>( const char*& p1, const char*& p2) {
if (strcmp(p1, p2) <= 0) {
return p2;
} else {
return p1;
}
}
tips:函数的特化不仅复杂,而且容易出现莫名其妙的错误,所以一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。也就是直接定义一个普通函数,利用匹配规则来调用我们定义的普通函数。
5.3 类模板的特化
(1)类模板的全特化:将模板参数类表中所有的参数都确定化
template<class T1,class T2 >
class Temp {
public:
Temp() {
cout << "模板" << endl;
}
private:
T1 _data;
T2 _val;
};
template<>
class Temp<int, char> { // 两个参数都确定
public:
Temp() {
cout << "全特化" << endl;
}
private:
int _data;
char _val;
};
(2)类模板的偏特化:部分特化
template<class T1,class T2 >
class Temp {
public:
Temp() {
cout << "模板" << endl;
}
private:
T1 _data;
T2 _val;
};
template<class T>
class Temp<T, char> {
public:
Temp() {
cout << "部分特化" << endl;
}
private:
T _data;
char _val;
};
(3)类模板的偏特化:对参数做进一步限制
template<class T1,class T2 >
class Temp {
public:
Temp() {
cout << "模板" << endl;
}
private:
T1 _data;
T2 _val;
};
template<class T1,class T2>
class Temp<T1*, T2*> {
public:
Temp()
{
cout << "对参数做进一步限制" << endl;
}
private:
T1* _data;
T2* _data;
};
6. 类模板特化应用之类型萃取
6.1 由来
因为模板是根据传入的参数生成相应的代码。这里的类型萃取,就是筛选出自定义类型和基础类型。下面以拷贝函数做一个例子。因为拷贝又分为浅拷贝和深拷贝。浅拷贝效率高,但是容易引发资源错误,因为它不拷贝资源,深拷贝会拷贝资源,也就是全部拷贝,但是效率低。随意对于模板,我们不知道传入的是什么类型,就无法确定浅拷贝还是深拷贝。这就引出了类型萃取。
6.2 实现步骤
- 第一步、我们先定义两个类,来保存一个是否是自定义类型
struct TruePodType { // Plain old data structure,缩写为POD
static bool Get() { // 如果是基础类型我们就返回真
return true;
}
};
struct FalsePodType {
static bool Get() { // 自定义类型就返回假
return false;
}
};
- 第二部、定义一个基础的类模板,我们把自定义类型取成别名字
template<class T>
struct TypeTraits {
typedef FalsePodType IsPODType;
};
- 第三步、进行特化,我们把内置类型取成相同的名字。看第四步
template<>
struct TypeTraits<int> {
typedef TruePodType IsPODType;
};
template<>
struct TypeTraits<char> {
typedef TruePodType IsPODType;
};
template<>
struct TypeTraits<double> {
typedef TruePodType IsPODType;
};
template<>
struct TypeTraits<short> {
typedef TruePodType IsPODType;
};
- 第四步、创建函数
void Copy(T* dest, 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];
}
}
}
- 第五步,测试
char str1[] = "string";
char str2[7];
Copy(str2, str1, strlen(str1) + 1); // ---->memcpy
for (auto&e : str2) {
cout << e << endl;
}
string str3[] = { "string","string","string" };
string str4[3];
Copy(str4, str3, 3); // ------>赋值拷贝
for (auto&e : str4) {
cout << e << endl;
}
7. 模板的分离编译
- 编译过程
对于程序的编译过程,在c语言阶段已经详细的介绍过(点击进入-->程序的编译)。该过程分为预处理----》编译-----》汇编------》链接。
- 分离编译
一个程序由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链 接起来形成单一的可执行文件的过程称为
- 模板的编译
对于模板来说,模板只有被调用的时候才会被实例化。所以如果直接进行编译,模板会出现链接错误,为了解决这个错误我们需要把模板的声明和定义防止一个.h或者.cpp文件里。
8. 模板总结
- 优点
(1)模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
(2)增强了代码的灵活性
- 缺陷
(1)模板会导致代码膨胀问题,也会导致编译时间变长
(2)出现模板编译错误时,错误信息非常凌乱,不易定位错误