目录
前言
一,前置知识:非类型模板参数
示例:模拟固定大小数组
相较于宏,模版的优势
二,模板的特化
模板的特化的概念
函数模板特化
类模板特化
全特化
偏特化(半特化)
类模板特化应用示例
三,模板分离编译
什么是分离编译
模板的分离编译
解决方法
四,模板总结
结语:
前言
C++ 模板最大的优势就是编写一套通用代码,适配多种不同的数据类型。 但是通用逻辑并不能处理所有场景。举一个典型例子:实现一个比较大小的模板函数,普通对象可以正常比较;如果传入指针类型,通用模板只会比较指针地址,而不是指针指向的内容,业务逻辑就会出错。
C++ 引入模板特化机制:保留通用模板,针对某些特殊类型单独实现一套定制逻辑。
模板特化分为:
- 函数模板特化(只有全特化,不支持偏特化)
- 类模板特化:分为全特化、偏特化(半特化)
重要面试考点:偏特化只存在类模板,C++ 标准没有函数模板偏特化。
一,前置知识:非类型模板参数
模板参数分为两大类:
类型模板参数:
template<class T>,T 代表一种数据类型。非类型模板参数:传入编译期常量,在模板内部可以将该参数作为常量使用。
示例:模拟固定大小数组
#include <iostream> using namespace std; namespace wang { // N为非类型模板参数,默认值为10 template<class T, size_t N = 10> class array { public: T& operator[](size_t index){ return _array[index]; } const T& operator[](size_t index)const{ return _array[index]; } size_t size()const{ return N; } bool empty()const{ return 0 == N; } private: T _array[N]; }; }非类型模板参数限制
1 浮点数、类对象、字符串字面量不允许作为非类型模板参数。
2 非类型模板参数的值必须在编译期就可以确定,不能是运行时变量。
相较于宏,模版的优势
宏的数据是定死的,但是这不是。
宏的定义:预处理阶段文本替换,没有类型检查,不属于 C++ 语法,由预处理器处理。
宏的代码:
#define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; #define N 5//宏 class stack { private: int a[N]; int _top; }; int main() { stack s1; stack s2; return 0; }如上代码,我定义了一个宏,N为5,那么N的大小就无法发生变化了,实例化出来的对象中的a数组是定死了5个大小。
模板的代码:
#define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; template<class T, size_t N = 10> class Stack { private: int a[N]; int _top; }; int main() { Stack<int, 5> st1; Stack<int, 15> st2; Stack<int> st3; return 0; }通过上面的代码与下面的调试图可以看出非类型模板可以通过实例化进行扩容,不会将空间大小固定死。
适用的场景:
1.什么时候用宏
头文件保护:
#ifndef HEADER #define HEADER条件编译:
#ifdef DEBUG调试开关。获取文件名、行号:
__FILE____LINE__业务逻辑尽量不要用宏实现函数。
2.什么时候用模板
写一套通用代码,兼容多种数据类型(泛型)。
通用容器、通用算法(如 std::vector、std::sort)。
需要类型安全、可以调试、支持特化的泛型逻辑。
一句话总结
- 宏 = 文本粘贴,预处理,无类型,不安全。
- 模板 = C++ 泛型工具,编译期生成代码,带类型检查,安全强大。
二,模板的特化
模板的特化的概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理。
template<class T> bool Less(T left, T right) { return left < right; }我们使用上面这个模板进行比较Date日期类的大小,比较如下:
int main() { cout << Less(1, 2) << endl; // 可以比较,结果正确 Date d1(2022, 7, 7); Date d2(2022, 7, 8); cout << Less(d1, d2) << endl; // 可以比较,结果正确 Date* p1 = &d1; Date* p2 = &d2; cout << Less(p1, p2) << endl; // 可以比较,结果错误 return 0; }为什么直接比数值与调用重载< 比较都没有问题,但是用用指针比较日期就出问题了,这是为什么,因为默认规则 用指针比较日期就是直接比指针地址,并不是比较日期,所以这个比较是不准确的。
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
函数模板特化
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板。
2. 关键字template后面接一对空的尖括号<>。
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型。
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
这时我们可以使用函数模板的特化解决上面的问题了,代码如下:
template<> bool Less<Date*>(Date* left, Date* right) { return *left < *right; }使用函数模板的特化问题就解决了。
为什么不会调用普通的模板而是调用模板的特化呢?
因为会优先调用最匹配的模板,如果有对应的函数,将不会调用模板,而是直接调用函数。
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。
函数模板,模板特化和函数重载是可以的一起存在的,优先顺序是函数重载,模板特化,函数模板(因为函数重载和模板特化同时存在的时候,模板特化永远不会被调用,这也是C++不推荐函数模板特化的原因,因为很容易就被重载覆盖了)
template<class T> bool Less(const T& left,const T& right) { return left < right; } template<> bool Less<int*>(const Date*& left, const Date*& right) { return *left < *right; }对于const类型的模板的特化,可以如上代码一样写吗?
是不行的。
如上图,这不是函数模板的特化,这是为什么呢?
因为模板的const是修饰的left与right,而模板的特化的const位与*的左边,所以修饰的是left与right所指向的对象,而不是left与right本身,所以报错了。
修改如下:
template<class T> bool Less(const T& left,const T& right) { return left < right; } template<> bool Less<int*>( Date* const & left, Date* const & right) { return *left < *right; }类模板特化
当我们有一个通用的类模板,对于某一种 / 某一部分类型,不想使用通用版本的逻辑,想要单独写一份特殊实现,这个过程就叫做模板特化。
核心目的:为特定类型定制不一样的代码逻辑。
类模板特化分为两大类:
- 全特化(完全特化):将模板所有的模板参数全部指定为具体类型。
- 偏特化(半特化 / 部分特化):只指定一部分模板参数,剩下参数依旧保留可变。
全特化
全特化即是将模板参数列表中所有的参数都确定化。
#include <iostream> using namespace std; // 通用类模板 template<typename T> class Test { public: void show() { cout << "通用模板版本" << endl; } }; // 全特化:专门针对 string 类型 template<> class Test<string> { public: void show() { cout << "string 全特化版本" << endl; } }; int main() { Test<int> t1; t1.show(); // 通用模板版本 Test<string> t2; t2.show(); // string 全特化版本 return 0; }偏特化(半特化)
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。
偏特化有两种常见形式:
- 部分参数指定:多个模板参数,固定其中一部分
- 范围限定:不指定具体类型,但是给模板参数增加限制(例如特化所有指针类型)
全特化如下:
template<> struct Less<Date*> { bool operator()(Date* x, Date* y) const { return *x < *y; } };半特化如下:
template<class T> struct Less<T*> { bool operator()(T* x, T* y) const { return *x < *y; } };类模板特化应用示例
#include <iostream> using namespace std; template<typename T> class PrintUtil { public: void print(const T& val) { cout << "值为:" << val << endl; } }; // bool 全特化 template<> class PrintUtil<bool> { public: void print(const bool& val) { if(val) cout << "布尔值:真" << endl; else cout << "布尔值:假" << endl; } }; int main() { PrintUtil<int> p1; p1.print(66); PrintUtil<bool> p2; p2.print(true); return 0; }三,模板分离编译
什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
模板的分离编译
问题现象
如果你像普通函数一样,模板声明放.h,模板实现放.cpp,编译能通过,链接报错(undefined reference)找不到函数定义。
底层原理
C++ 模板是延迟实例化: 模板本身不是真正的函数 / 类。只有当代码使用了模板(指定了类型),编译器才会生成一份对应类型的实体代码。
编译
.cpp模板实现文件时,编译器看不到外面哪里会实例化这个模板,不会生成任何函数实体。编译
main.cpp(调用模板)时,只能看到.h里面的声明,看不到实现代码,也无法生成实例。两边都没生成实例,最后链接就找不到实现。
解决方法
解决上述模板分离编译的问题有两种方法 :
- 将声明和定义放到一个文件中(推荐用这种)
- 模板定义的位置显式实例化。(不推荐,不实用)
四,模板总结
【优点】
1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
2. 增强了代码的灵活性。
【缺陷】
1. 模板会导致代码膨胀问题,也会导致编译时间变长。
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。
结语:
感谢你们的观看,希望可以帮助到你们!