目录
2.1.5.1一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这 个非模板函数
2.1.5.2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而 不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模 板
1.泛型编程
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
这是一段交换得到代码,那么咱们看到,这个代码只能够实现int类型的数据交换,如果换成double类型,就不能够实现交换了。那么这种情况下,按照以前的做法,是不是只能够再写一个swap的重载函数,那么这种方法固然可行,但是万一又让你写很多类型的交换函数呢?你难道将他们的交换函数全部写出来?没必要,因为这些东西除了参数类型不同之外,其他的全都相同。所以说,为了节省冗余的代码,咱们引入了泛型编程这个概念。 即创建一个swap的函数模板,从而将实例化这个任务交给编译器去完成,咱们只需要传实参即可。这样是不是就容易多了?
2.模板
C++ 模板(Templates)是泛型编程(Generic Programming)的核心,允许开发者编写与类型无关的代码,从而提高代码复用性、减少冗余。模板广泛应用于标准库(如 std::vector
、std::sort
)以及高性能计算、游戏引擎等领域。
2.1 函数模板
2.1.1 函数模板的概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生 函数的特定类型版本。
2.1.2 函数模板的格式
template<typename T1,typename T2,..........>
函数返回值 函数名 (参数列表){}
template<typenameT>//不光是T,X,Y,Z均可
void Swap( T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
这个T什么类型都可以,包括这个T可以作为参数类型,返回值类型均可。
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替 class)
2.1.3 函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。 所以其实模板就是将本来应该我们做的重复的事情交给了编译器 。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应 类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演, 将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
2.1.4 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
2.1.4.1 隐式实例化
让编译器根据实参自己推断出模板参数的实际类型
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
//以下两句是隐式类型转换
cout << Add(a1, a2) << endl;;
cout<<Add(d1, d2)<<endl;
/*该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
编译器无法确定此处到底该将T确定为int 或者 double类型而报错
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要
背黑锅*/
Add(a1, d1);//a1与d1的类型不通,不符合函数模板的类型
//第一种解决办法:直接强转
Add(a1, (int)d1);
//第二种办法,就是通过显式实例化
//尖括号里是你明确要实例化的类型,即你只能调用这个类型
Add<int>(a1, d1);
//如果说编译器在这种情况下,遇到类型不符合的,编译器会尝试进行隐式类型转换
//如果转换不成功,编译器就会报错
return 0;
}
2.1.4.2 显式实例化
显式实例化也写在上面的代码中了,以及一些需要注意的东西,大家自行阅读上面的代码。
2.1.5 模板参数的匹配原则
2.1.5.1一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这 个非模板函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
return left + right;
}
void Test()
{
Add(1, 2);
// 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
}
这里,第一个调用的Add函数,参数类型与已经存在的函数一模一样,那么函数模板就不需要再自己造一个了,直接调用存在的Add函数即可。第二个,已经明确了,我就要调用函数模板的int类型,那这没有办法,人家限定了。
2.1.5.2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而 不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模 板
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
Add(1, 2);
// 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的
}
这儿第一个调用的Add函数咱们上面已经说过原因了,下面来说一下第二个Add函数,第二个很明显,前后两个实参的类型不同,那么这时候,你要是再去调用已经存在的函数,精度就不行了,所以这时候就必须要调用函数模板了,让编译器创建一个与其参数类型匹配的函数。
即”有现成吃现成,没有现成自己做“。模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
2.2 类模板
2.2.1 类模板的定义格式
template<class T1,class T2,class T3.........>
class 类模板名
{
//类内成员定义
};
我之前的有关STL的文章中的模拟实现都是以模板来展开实现的,大家若还是不明白类模板的定义格式,以及怎么写的,可以参考我之前写的文章。
2.2.2 类模板的实例化
// Stack是类名,Stack<int>才是类型
Stack<int> st1; // int
Stack<double> st2; // double
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的 类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
看到这个类,是不是很熟悉,咱们之前所写过的vector<int>,vector<string>等都是的。
2.3 类型模板参数与非类型模板参数
模板参数分类类型形参与非类型形参。
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常 量来使用。
// 非类型模板参数
template<class T, size_t N = 10>
class Stack
{
private:
T _a[N];
size_t _top;
};
注意:
1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2. 非类型的模板参数必须在编译期就能确认结果。
2.4 模板特化
2.4.1 概念
看到特化大家会想到什么?没错,特化特化,就是特殊处理。
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些 错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板。
template<class T>
bool Less(T left, T right)
{
return left < right;
}
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;
}
可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示 例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内 容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方 式。模板特化中分为函数模板特化与类模板特化。
2.4.2 函数模板特化:
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇 怪的错误。
注意:函数模板只有全特化。
对于第三点:尖括号里的类型必须与基础函数模板的参数类型一致。
对于第四点:形参的类型,参数个数必须与基础函数模板形参类型一致,否则可能会引发错误。
注意:第四点的形参的类型,比如,T&,他的类型就是T&。而第三点中的参数类型,指的可不是T&,而是T。
来看第一张图片:红色的圈的是整个T,以及整个int&,怎么判断这个特化对不对呢?
先看第四点,即特化的形参的类型是否与基础函数的形参类型一致(基础形参类型是T,特化的可以是int,也可以是int&。但要注意,如果是int,那么尖括号里的也得是int,因为尖括号里的就是T的类型。而第二个int&,尖括号里的也必须得是int&,因为你将T一整个特化为了int&,那么尖括号里的为了与T保持一致,也得是int&)。
第二张图片,注意看T只特化了成了int,而&并没有动,你看我用红色方括号圈出来的,就知道了,但是特化的形参类型必须得是int&,为了与基础参数类型保持一致,但是尖括号里的只需要与T保持一致就可以了,而T被特化为了int,那么现在尖括号里的也是int。
通过以上两个例子,足够将函数模板特化搞清楚了。
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
所以,直接对Less函数进行模板特化即可。那么那俩指针的比较,直接走特化后的版本,不走模板生成了。
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该 函数直接给出。
bool Less(Date* left, Date* right)
{
return *left < *right;
}
该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化 时特别给出,因此函数模板不建议特化。
2.4.3 类模板特化
类模板特化就有全特化与偏特化了。
2.4.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;//走模板特化的版本
}
2.4.3.2 偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
偏特化有两种表现形式:
2.4.3.2.1 部分特化
将模板参数类表中的一部分参数特化。
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
T1 _d1;
int _d2;
};
2.4.3.2.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;
};
void test2()
{
Data<double, int> d1;// 调用特化的int版本
Data<int, double> d2;// 调用基础的模板
Data<int*, int*> d3;// 调用特化的指针版本
Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}
注意看清楚这个偏特化的形式 。是在typename下面直接加上你要特化的类型即可。不需要再开一个函数模板了!。
2.4.4 类模板特化使用示例
#include<vector>
#include<algorithm>
template<class T>
struct Less
{
bool operator()(const T& x, const T& 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;
v1.push_back(d1);
v1.push_back(d2);
v1.push_back(d3);
// 可以直接排序,结果是日期升序
sort(v1.begin(), v1.end(), Less<Date>());
vector<Date*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);
return 0;
}
// 可以直接排序,结果错误日期还不是升序,而v2中放的地址是升序
// 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象
// 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期
sort(v2.begin(), v2.end(), Less<Date*>());
return 0;
}
通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的。但是如果待排 序元素是指针,结果就不一定正确。因为:sort最终按照Less模板中方式比较,所以只会比较指 针,而不是比较指针指向空间中内容,此时可以使用类版本特化来处理上述问题 .
// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{
bool operator()(Date* x, Date* y) const
{
return *x < *y;
}
};
特化之后,在运行上述代码,就可以得到正确的结果.
3.模板分离编译(了解)
3.1 什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有 目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
就是咱们写文件或者项目的时候,定义的.c,.h,.cpp文件这些东西。
3.2 模板的分离编译
如果模板分离编译了会怎么样呢?
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
}
return 0;
假如有以上代码,是分离编译的,但是你会发现,报错了,什么错误呢?
要想弄懂这个,咱们还需要来进行知识回顾。
这是C/C++程序要运行,需要经历的几个步骤:
注意,第二步符号指令,cpu并不认识这些符号指令。只认识二进制之类的东西。并且在编译期间,头文件是不参与编译的,并且编译器对工程中的多个源文件是分离开单独编译的。
而这个链接,是将多个obj文件合并成一个,并且处理没有解决的地址问题。
链接:只有在只有声明,没有定义的时候才去链接(就是.c,.h,.cpp)这些文件才去链接。拿着这个声明去其他文件的符号表去找,找到定义的地址后,再将它那个地址拿回来。这样链接就可以了。所以说函数只有在定义时才有地址。且函数的地址是一堆call,move指令。所以说模板不可以分离到两个文件中,否则会出现链接错误。
你可以这么理解:
模板就是一个”虚“,即泛型,所以才有实例化这一说,即具体实例化出一个对应的东西。所以说模板可以分离定义到两个文件,但是必须显式实例化,否则定义的模板不知道具体实例化出什么东西。
那这时候就会有人问了,那不是链接可以找地址吗?那找到了地址,直接不就可以链接了吗?并不是,这个地址只有在比如模板实例化出例子的时候,才会有地址,但是实际上,模板根本不知道要实例化出什么东西,何谈有地址呢?那找不到地址,不就链接错误了吗?
所以,直接声明与定义在一个文件中,既可以避免链接错误。
上图为分析逻辑图,大家可以看一下。
OK,本篇完..................