目录
1. 非类型模板参数
1.1 array
1.2 非类型模板参数的使用场景
1.3 注意事项
2. 模板的特化
2.1 函数模板的特化
2.2 类模板的特化
2.3 全特化和偏特化(半特化)
3. 模板关于分离编译
4. 模板优缺点
5. 模板相关笔试题
本章完。
1. 非类型模板参数
对于函数模板和类模板,模板参数并不局限于类型,普通值也可以作为模板参数。
STL 的 array 就有一个非类型模板参数。
T 是类型,而 N 这里并不是类型,而是一个常量。
类型模板参数定义的是虚拟类型,注重的是你要传什么,而非类型模板参数定义的是常量。
1.1 array
array是一个固定大小的顺序容器(静态数组),是 C++11 新增的,它有什么独特的地方吗?
很可惜,基本没有,并且 vector 可以完全碾压 array,这就是为什么只在这里简单地讲讲 array。
看段代码:
#include <iostream>
#include <array>
#include <vector>
using namespace std;
int main()
{
vector<int> v(100, 0);
array<int, 100> arr;
cout << "v : " << sizeof(v) << endl;
//这里sizeof算的是成员变量的大小,VS2022下vector应该有四个成员变量,32位平台每个指针是4个字节,因此16字节
cout << "arr : " << sizeof(arr) << endl;
return 0;
}
vector 是开在空间大的堆上的而 array 是开在栈上的,堆可比栈的空间大太多太多了。
array 能做的操作几乎 vector 都能做,因为 vector 的存在 array 显得有些一无是处。
所以我们拿 array 去对标 vector 是不对的,拿去和原生数组比还是可以对比的。
但是 array 也只是封装过的原生数组罢了,就是有了接口函数,
比起原生数组,array 的最大优势也只是有一个越界的检查,读和写都可以检查到是否越界。
原生数组的读检查不到,写只能检查到后面几个数,
#include <iostream>
#include <array>
#include <vector>
using namespace std;
int main()
{
int a[10];
array<int, 10> arr; // array也不会初始化
int x = a[15]; // 没报错
a[10] = 2; // 报错
a[11] = 2; // 没报错
int y = arr[15]; // 报错
arr[10] = 2; // 报错
arr[11] = 2; // 报错
return 0;
}
在 C++11 增加完 array 后备受吐槽,从简化的角度来说完全可以不增加 array。
并且现在大多数人都习惯了用原生数组,基本没人用array。
1.2 非类型模板参数的使用场景
假设我们要定义一个静态栈:
#define N 100
template<class T>
class Stack
{
private:
int _arr[N];
int _top;
};
如果定义两个容量不一样的栈,一个容量是100 另一个是 500,能做到吗?
这就像 typedef 做不到一个存 int 一个存 double,而使用模板可以做到 st1 存 int,st2 存 double。
这里你的 #define 无论是改 100 还是改 500 都没办法解决这里的问题,
对应的,这里使用非类型模板参数就可以做到 s1 存 100,s2 存 500。
#include <iostream>
using namespace std;
template<class T, size_t N>
class Stack
{
private:
int _arr[N];
int _top;
};
int main()
{
Stack<int, 100> st1; // 大小是100
Stack<int, 500> st2; // 大小是500
return 0;
}
在模板这定义一个常量 N,派遣它去做数组的大小。
于是我们就可以在实例化 Stack 的时候指定其实例化对象的大小了,分别传 100 和 500。
1.3 注意事项
注意事项 ①:非类型模板参数是是常量,是不能修改的。
#include <iostream>
using namespace std;
template<class T, size_t N>
class Stack
{
public:
void modify()
{
N = 10; // 错误 C2106 “ = ”: 左操作数必须为左值
}
private:
int _arr[N];
int _top;
};
int main()
{
Stack<int, 100> st1;
st1.modify();
return 0;
}
注意事项 ②:有些类型是不能作为非类型模板参数的,比如浮点数、类对象以及字符串。
非类型模板参数基本上都是整型家族,char也是整形家族,也只有整型家族是有意义和价值的。
#include <iostream>
using namespace std;
template<class T, double N> // 错误 C2058 常量表达式不是整型
class Stack
{
private:
int _arr[N];
int _top;
};
int main()
{
Stack<int, 100> st1;
return 0;
}
注意事项 ③:非类型的模板参数必须在编译期就能确认结果。
即非类型模板参数的实参只能是常量。
#include <iostream>
using namespace std;
template<class T, size_t N> // 错误 C2058 常量表达式不是整型
class Stack
{
private:
int _arr[N];
int _top;
};
int main()
{
size_t N;
cin >> N;
Stack<int, N> st1; // 错误 C2971 “Stack” : 模板参数“N”:“N”: 包含非静态存储持续时间的变量不能用作非类型参数
return 0;
}
2. 模板的特化
通常情况下,使用模板可以实现一些与类型无关的代码
但是,对于一些特殊类型,可能我们就要对其进行一些 "特殊化的处理" 。
举例:如果不对特殊类型进行特殊处理就可能会出现一些问题,比如:
#include <iostream>
using namespace std;
class Date // 简化的日期类
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d) const
{
if ((_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day))
{
return true;
}
else
{
return false;
}
}
private:
int _year;
int _month;
int _day;
};
template<class T> // 函数模板 -- 参数匹配
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl; // 可以比较,结果正确
Date d1(2023, 1, 1);
Date d2(2023, 1, 2);
cout << Less(d1, d2) << endl; // 可以比较,结果正确
Date* p2 = &d2;
Date* p1 = &d1;
cout << Less(p1, p2) << endl; // 可以比较,结果错误
return 0;
}
这里我们想比较的是指针指向的内容,而不是指针本身,怎么解决?
2.1 函数模板的特化
首先,必须要先有一个基础的函数模板。
其次,关键字 template 后面接上一对空的 <> 尖括号。
然后,函数名后跟一对尖括号,尖括号中指定需要特化的内容。
最后,函数形参表必须要和模板函数的基础参数类型完全相同。
template<class T> // 函数模板 -- 参数匹配
bool Less(T left, T right)
{
return left < right;
}
template<> // 针对某些类型要特殊化处理 ———— 使用模板的特化解决
bool Less<Date*>(Date* left, Date* right) {
return *left < *right;
}
代码演示:
#include <iostream>
using namespace std;
class Date // 简化的日期类
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d) const
{
if ((_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day))
{
return true;
}
else
{
return false;
}
}
private:
int _year;
int _month;
int _day;
};
template<class T> // 函数模板 -- 参数匹配
bool Less(T left, T right)
{
return left < right;
}
template<> // 针对某些类型要特殊化处理 ———— 使用模板的特化解决
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
int main()
{
cout << Less(1, 2) << endl; // 可以比较,结果正确
Date d1(2023, 1, 1);
Date d2(2023, 1, 2);
cout << Less(d1, d2) << endl; // 可以比较,结果正确
Date* p2 = &d2;
Date* p1 = &d1;
cout << Less(p1, p2) << endl; // 可以比较,结果正确
return 0;
}
解读:对于普通类型,它还是会调正常的模板。对于 Date* 编译器就会发现这里有个
专门为 Date* 而准备的特化版本,编译器会优先选择该特化版本。这就是函数模板的特化。
思考:现在我们加一个普通Less函数的函数重载,Date* 会走哪个版本?
bool Less(Date* left, Date* right)
{
return *left < *right;
}
答案:函数重载,会走直接匹配的普通函数版本,因为是现成的,不用实例化。
你可以这么理解:原模板是生肉,模板特化是半生不熟的肉,直接匹配的普通函数是熟肉。
所以:函数模板不一定非要特化,因为在参数里面就可以处理,
写一个匹配参数的普通函数也更容易理解。
2.2 类模板的特化
刚才函数模板不一定非要特化,因为可以写一个具体实现的函数。
但是类模板我们没法实现一个具体的实际类型,就必须要特化了。
我们前面实现的仿函数(类模板)也有这样的问题:
#include <iostream>
using namespace std;
class Date // 简化的日期类
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d) const
{
if ((_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day))
{
return true;
}
else
{
return false;
}
}
private:
int _year;
int _month;
int _day;
};
template<class T> // 函数模板 -- 参数匹配
bool Less(T left, T right)
{
return left < right;
}
template<> // 针对某些类型要特殊化处理 ———— 使用模板的特化解决
bool Less<Date*>(Date* left, Date* right) {
return *left < *right;
}
类模板
template<class T>
struct Less2
{
bool operator()(const T& x1, const T& x2) const
{
return x1 < x2;
}
};
int main()
{
Less2<Date> LessFunc1;
Date d1(2023, 1, 1);
Date d2(2023, 1, 2);
cout << LessFunc1(d1, d2) << endl; // 可以比较,结果正确
Less2<Date*> LessFunc2;
Date* p2 = &d2;
Date* p1 = &d1;
cout << LessFunc2(p1, p2) << endl; // 可以比较,结果错误
return 0;
}
加上类模板的特化:
#include <iostream>
using namespace std;
class Date // 简化的日期类
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d) const
{
if ((_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day))
{
return true;
}
else
{
return false;
}
}
private:
int _year;
int _month;
int _day;
};
template<class T> // 函数模板 -- 参数匹配
bool Less(T left, T right)
{
return left < right;
}
template<> // 针对某些类型要特殊化处理 ———— 使用模板的特化解决
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
类模板
template<class T>
struct Less2
{
bool operator()(const T& x1, const T& x2) const
{
return x1 < x2;
}
};
template<>// 类模板特化
struct Less2<Date*>
{
bool operator()(const Date* x1, const Date* x2) const
{
return *x1 < *x2;
}
};
int main()
{
Less2<Date> LessFunc1;
Date d1(2023, 1, 1);
Date d2(2023, 1, 2);
cout << LessFunc1(d1, d2) << endl; // 可以比较,结果正确
Less2<Date*> LessFunc2;
Date* p2 = &d2;
Date* p1 = &d1;
cout << LessFunc2(p1, p2) << endl; // 可以比较,结果错误,加上类模板特化后结果正确
return 0;
}
2.3 全特化和偏特化(半特化)
全特化和偏特化的概念和缺省值很像,前面我们写的都叫作模板的全特化。
全特化:全特化即是将模板参数列表中所有的参数都确定化。
偏特化(又称半特化):将部分参数类表中的一部分参数特化。
(半特化并不是特化一半,就像半缺省并不是缺省一半一样)
偏特化有以下两种表现方式:
① 部分特化:将模板参数类表中的一部分参数特化。
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
T1 _d1;
int _d2;
};
② 参数更进一步的限制:偏特化并不仅仅是指特化部分参数,
而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
//两个参数偏特化为指针类型
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;
};
void test2()
{
Data<double, int> d1; // 调用特化的int版本
Data<int, double> d2; // 调用基础的模板
Data<int*, int*> d3; // 调用特化的指针版本
Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}
放一段代码体会偏特化的花哨玩法:
#include <iostream>
using namespace std;
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;*/
};
// 偏特化
template <class T1>
class Data<T1, int>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
/*T1 _d1;
int _d2;*/
};
template<class T1, class T2>
class Data<T1*,T2*>
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
};
template<class T1, class T2>
class Data<T1&, T2&>
{
public:
Data() { cout << "Data<T1&, T2&>" << endl; }
};
template<class T1, class T2>
class Data<T1&, T2*>
{
public:
Data() { cout << "Data<T1&, T2*>" << endl; }
};
int main()
{
Data<int, int> d0;
Data<double, int> d1;
Data<int, char> d2;
Data<double, double> d3;
Data<double*, double*> d4;
Data<int*, char*> d5;
Data<int*, char> d6;
Data<int&, char&> d7;
Data<int&, double&> d8;
Data<int&, double*> d9;
return 0;
}
这就对应说过的类型匹配原则,有更匹配的就去调用它,没有就逐层递减去匹配。
3. 模板关于分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链 接起来形成单一的可执行文件的过程称为分离编译模式。
如上图所示,在template.cpp源文件中定义了Sub函数,并在template.h头文件中进行了声明。
但是在编译过程中,编译器是对各个源文件进行单独编译的,template.cpp源文件进行编译的过程中,没有检测到Sub函数模板的实例化,所以不会生成对应的代码,在main.cpp源文件中进行调用,链接阶段便会出错。如图:
理解两个概念:
- 导出符号表:编译完成后该源文件中地址(函数定义的位置)已经确定的函数
- 未解决符号表:源文件中地址还没有确定的函数
这里main.cpp源文件编译完成后,没有找到Sub函数的定义,但是由于头文件中进行了声明,在预处理阶段头文件中的声明会拷贝到源文件中,所以并不会立即报错,而是将Sub函数放在未解决符号表中,链接阶段,在template.cpp文件的导出符号表中找Sub函数的入口地址,而如果Sub函数没有生成则会报错。
4. 模板优缺点
【优点】
① 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
② 增强了代码的灵活性。
【缺点】
① 模板会导致代码膨胀问题,也会导致编译时间变长。
② 出现模板编译错误时,错误信息非常凌乱,不易定位错误。
5. 模板相关笔试题
1. 下列的模板声明中,其中几个是正确的( )
1)template
2)template<T1,T2>
3)template<class T1,T2>
4)template<class T1,class T2>
5)template<typename T1,T2>
6)template<typename T1,typename T2>
7)template<class T1,typename T2>
8)<typename T1,class T2>
9)template<typeaname T1, typename T2, size_t N>
10)template<typeaname T, size_t N=100, class _A=alloc<T>>
11)template<size_t N>
A.3
B.4
C.5
D.6
2. 以下程序运行结果正确的是( )
template<typename Type>
Type Max(const Type &a, const Type &b)
{
cout<<"This is Max<Type>"<<endl;
return a > b ? a : b;
}
template<>
int Max<int>(const int &a, const int &b)
{
cout<<"This is Max<int>"<<endl;
return a > b ? a : b;
}
template<>
char Max<char>(const char &a, const char &b)
{
cout<<"This is Max<char>"<<endl;
return a > b ? a : b;
}
int Max(const int &a, const int &b)
{
cout<<"This is Max"<<endl;
return a > b ? a : b;
}
int main()
{
Max(10,20);
Max(12.34,23.45);
Max('A','B');
Max<int>(20,30);
return 0;
}
A.This is Max This is Max<Type> This is Max<char> This is Max<int>
B.This is Max<int> This is Max<Type> This is Max<char> This is Max<int>
C.This is Max This is Max<int> This is Max<char> This is Max<int>
D.This is Max This is Max<Type> This is Max<char> This is Max
3. 关于模板的编译说法错误的是( )
A.模板在.h文件中声明,在.cpp里面实现
B.模板程序一般直接在一个文件里面进行定义与实现
C.不久的将来,编译器有望支持export关键字,实现模板分离编译
D.模板不能分离编译,是因为模板程序在编译过程中需要经过两次编译
4. 以下程序运行结果正确的是( )
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, int>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
T1 _d1;
int _d2;
};
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;
};
int main()
{
Data<double, int> d1;
Data<int, double> d2;
Data<int *, int*> d3;
Data<int&, int&> d4(1, 2);
return 0;
}
A.Data<T1, T2> Data<T1, int> Data<T1*, T2*> Data<T1&, T2&>
B.Data<T1, int> Data<T1, T2> Data<T1&, T2&> Data<T1*, T2*>
C.Data<T1, int> Data<T1, T2> Data<T1*, T2*> Data<T1&, T2&>
D.Data<T1, T2> Data<T1, T2> Data<T1*, T2*> Data<T1&, T2&>
答案:
1. D
分析:正确的定义为:4 6 7 9 10 11,一共6个
2. A
分析:Max(10,20); //能够直接匹配int参数,调动非模板函数
Max(12.34,23.45); //double类型参数没有最佳匹配函数,此时只能调动模板函数
Max('A','B'); //能够直接匹配char参数,调动非模板函数
Max<int>(20,30); //由于直接实例化了函数,因此要调动模板函数,但是,由于进行函数的int特化,所以会调动特化版本的模板函数
3. A
A.模板不支持分离编译,所以不能在.h声明,在.cpp实现
B.由于不支持分离编译,模板程序一般只能放在一个文件里实现
C.不支持分离编译并不是语法错误,而是暂时的编译器不支持,不久将来,或许会被支持
D.模板程序被编译两次,这是不能分离编译的原因所在
4. C
分析:Data<double, int> d1; // 调用特化的int版本
Data<int, double> d2; // 调用基础的模板
Data<int *, int*> d3; // 调用特化的指针版本
Data<int&, int&> d4(1, 2); //调用特化的引用版本
本章完。
下一部分:C++中的继承,讲完继承讲多态。