前言
大家好吖,欢迎来到 YY 滴C++系列 ,热烈欢迎! 本章主要内容面向接触过C++的老铁
主要内容含:
欢迎订阅 YY滴C++专栏!更多干货持续更新!以下是传送门!
目录
- 一. 模板参数与模板参数列表
- 1)模板参数&模板参数列表
- 2)非类型模板参数
- 二.函数模板
- 1)函数模板概念
- 2)函数模板的格式
- 3)函数模板的实例化
- 1.【隐式实例化】
- 2.【显式实例化 】
- 3.【模板参数的匹配原则 】
- 4)函数模板的特化
- 1.【特化的使用场景】
- 2.【特化的步骤】
- 3.【结论:函数模板不建议特化】
- 三.类模板
- 1)类模板的格式
- 2)类模板的实例化
- 3)区分访问类模板时用【类型】而非【类名】
- 4)使用类模板内相关时的注意事项【假设场景:取类模板内的迭代器】
- 5) 类模板的特化【全特化&偏特化】
- [1]简单介绍
- [2]全特化
- [3]偏特化
- [4]偏特化运用场景
- 四. 模板的分离编译报错【声明定义要放在一个源(头)文件中】
- 1)分离编译模式
- 2)程序运行步骤简述
- 3)报错内容:“无法解析的外部符号”
- 4)类模板在C++11支持声明定义分离
一. 模板参数与模板参数列表
模板参数分类类型形参与非类型形参:
- 类型形参:出现在模板参数列表中,跟在class(typename)后面的参数类型名称
- 非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
1)模板参数&模板参数列表
2)非类型模板参数
- 非类型模板参数主要用于定义一个【静态栈】例如array
- 要注意非类型模板参数只能用于整型 【浮点数、类对象以及字符串是不允许作为非类型模板参数的】
- 非类型的模板参数必须在编译期就能确认结果
// 静态栈
// 非类型模板参数
// 1、常量
// 2、必须是整形
template<class T, size_t N>
class Stack
{
public:
void func()
{
// 常量,不能修改(调用func会报错)
N = 0;
}
private:
T _a[N];
int _top;
};
二.函数模板
1)函数模板概念
- 函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化, 根据实参类型产生函数的特定类型版本 ;
2)函数模板的格式
template<typename T>
void Swap( T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
//可识别不同的同种类型交换(例:char与char,int与int,double与double)
PS:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class) ;
3)函数模板的实例化
引入:
- 用不同类型的参数使用函数模板时,称为 函数模板的实例化 。模板参数实例化分为: 隐式实例化 和 显式实例化
- PS:实例化实现的任务是交给编译器的
1.【隐式实例化】
引入:
- 隐式实例化的机制是让编译器 根据实参推演模板参数的实际类型 ,而这往往会出现一些问题
- 适用情况:其交换的两者是同一类
- 不适用情况:其交换的两者 不是同一类
template<class T> T Add(const T& left, const T& right) { return left + right; } int main() { int a1 = 10; double d1 = 10.0; Add(a1, d1); //解决方式:Add(a1, (int)d1);强制类型转换 } }
分析:
- 该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
- 通过实参a1将T推演为int类型 ,通过实参d1将T推演为double类型 ,但模板参数列表中只有一个T,
- 编译器无法确定此处到底该将T确定为int 或者 double类型而报错
解决方式:
- 用户自己强制类型转换
Add(a1, (int)d1);
- 显式实例化
2.【显式实例化 】
显式实例化:在函数名后的<>中 指定 模板参数的实际类型
int main(void) { int a = 10; double b = 20.0; // 显式实例化 Add<int>(a, b); return 0; }
3.【模板参数的匹配原则 】
- 一个非模板函数可以和一个 同名 的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以 产生一个具有更好匹配的函数, 那么将选择模板
- 模板函数不允许自动类型转换 ,但普通函数可以进行自动类型转换
4)函数模板的特化
1.【特化的使用场景】
- 特化有其使用需求与场景,但对于一些特殊类型的可能会得到一些错误的结果,比如:实现了一个专门用来进行小于比较的函数模板
- 用于比较【整型】【日期类Date】时,可以正常比较,但要用于比较【日期类指针】指向的【日期类】的大小时,需求无法完成
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl; // success
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl; // success
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 比较的是指针的大小,需求无法完成
return 0;
}
2.【特化的步骤】
步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号< >
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型 【当传入参数类型是特化类型时,则不走模板生成】
- 函数形参列表: 必须要和模板函数的基础参数类型完全相同 (如果不同,编译器可能会报一些奇怪的错误)
//基础的函数模板
template<class T>
bool Less(T left, T right)
{
return left < right; //步骤1:先有一个基础的函数模板
}
// 对Less函数模板进行特化
template<> //步骤2:关键字template后面接一对空的尖括号
bool Less<Date*> //步骤3:函数名后跟一对尖括号,尖括号中指定需要特化的类型
(Date* left, Date* right) //步骤4:函数形参列表要和模板函数的基础参数类型完全相同
{
return *left < *right;
}
int main()
{
cout << Less(1, 2) << endl; // success
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl; // success
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 比较的是指针的大小,需求无法完成
return 0;
}
3.【结论:函数模板不建议特化】
- 注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出
bool Less(Date* left, Date* right)
{
return *left < *right;
}
三.类模板
1)类模板的格式
template<class T1, class T2, ..., class Tn>
class xxx//(类模板名)
{
// 类内成员定义
};
2)类模板的实例化
- 类模板实例化与函数模板实例化不同,
类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可
,类模板名字不是真正的类,而实例化的结果才是真正的类 ;Vector<int> s1; Vector<double> s2;
3)区分访问类模板时用【类型】而非【类名】
注意区分:
- 在类中:类名等同于类型
- 在类模板中:类型是类型,类名是类名
例如:
- 在下面代码中,类模板中函数放在类外进行定义时,需要加模板参数列表;在访问类模板时,用的是Vector(类型),而不是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; }
4)使用类模板内相关时的注意事项【假设场景:取类模板内的迭代器】
- 当我们想要实现一个打印任意容器元素的print函数,我们需要遍历容器,于是设置了模板参数
Container
- 如果我们直接写成
Container::const_iterator it = v.begin();
形式,其中的const_iterator
可能是静态变量,内部类名等等- 所以我们要在前面加上
typename
,确保编译器能够识别到其是类型,等模板实例化再去找
//template<typename Container>
template<class Container>
void Print(const Container& v)
{
// 编译不确定Container::const_iterator是类型还是对象
// typename就是明确告诉编译器这里是类型,等模板实例化再去找
// Container::const_iterator it = v.begin();
typename Container::const_iterator it = v.begin();
auto it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
5) 类模板的特化【全特化&偏特化】
[1]简单介绍
- 全特化即是将模板参数列表中的所有参数都确定化
- 偏特化即是【任何针对模版参数进一步进行条件限制设计的特化版本】:部分参数确定化,增加限定条件(指针/引用)
- 特化后的类是新的类,不用带上原类所有的成员变量或者函数,编译器会处理这块问题
- 特化后的类不能独立于原类存在
[2]全特化
template<>
class Data<int, char>
[3]偏特化
template <typename T1>
class Data<T1, char>//部分特化——————————>特化的char模板
template <typename T1, typename T2>
class Data <T1*, T2*>//两个参数偏特化为指针类型
class Data <T1&, T2&>//两个参数偏特化为引用类型
Data<double , int> d1; // 调用特化的char模板
Data<int , double> d2; // 调用基础的模板
Data<int *, int*> d3; //调用特化的指针模板
[4]偏特化运用场景
#include<vector>
#include <algorithm>
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{
bool operator()(Date* x, Date* y) const
{
return *x < *y;
}
};
int main()
{
Date d1(2022, 7, 7);
Date d2(2022, 7, 6);
Date d3(2022, 7, 8);
vector<Date> v1;
//场景1
v1.push_back(d1);
v1.push_back(d2);
v1.push_back(d3);
// 可以直接排序,结果是日期升序
sort(v1.begin(), v1.end(), Less<Date>());
//场景2
vector<Date*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);
// 可以直接排序,结果错误日期还不是升序,而v2中放的地址是升序
// 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象
// 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期
sort(v2.begin(), v2.end(), Less<Date*>());
return 0;
}
四. 模板的分离编译报错【声明定义要放在一个源(头)文件中】
1)分离编译模式
- 定义:一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链
接起来形成单一的可执行文件的过程称为分离编译模式。
2)程序运行步骤简述
- 要经过:预处理–>编译–>汇编–>链接
- 编译:对程序进行一些分析&错误检查后生成汇编代码;
- 头文件不参与编译,编译器对工程中的多个源文件是单独分开编译【把.c文件编译成.obj文件】
- 链接:将多个obj文件合并成一个,处理没有解决的地址问题
3)报错内容:“无法解析的外部符号”
- 如下所示:
报错原因分析:
- 由于模板声明和定义是分离的,模板定义部分是在.c文件中,经过编译阶段变成.obj文件
- 在.c文件中,编译器没有看到到对模板函数的实例化,因此不会生成对应函数
- 最后编译器在链接阶段会去找函数的地址,但是在上一步中函数没有实例化没有生成具体的代码,因此报错
4)类模板在C++11支持声明定义分离
在 C++中,类模板的声明和定义必须放在一起,因为编译器在编译时需要检查类模板的具体实现。如果将声明和定义分离,编译器就无法检查类模板的具体实现,这将导致编译错误。
在 C++11 中引入了模板具体化 (template specialization 的概念,允许程序员在另一个文件中声明和定义模板的一个特殊版本,但这只适用于模板具体化,对于普通的类模板而言,声明和定义仍然必须放在一起。