函数模板(进阶)
机甲为婚纱,银河为殿堂,爆炸为礼炮,见证了只属于他们的婚礼,樱花树下,再续前缘,鹤望兰无凋零之时,比翼鸟永世长存。
我们这一篇博客紧接我们前面的函数模板(初阶)这一篇博客,如果大家没有看过我的那一篇博客的话,建议大家去先去看一下,其实,不看对我们这篇的影响也不是很大,链接如下:
函数模板(初阶)_c 函数模板-CSDN博客文章浏览阅读1.4k次,点赞161次,收藏119次。Hello,大家好,我们大家都知道,C++这个编程语言是由C语言继承而来的,因为是继承,所以我们的C++就要做出一些区分,要不然的话,就和C语言没有本质上的区别了,我们现在在社会中使用比较多的是C++而非是C语言,是因为这里我们C++的祖师爷在C语言的基础之上又设计了一个模板相关的内容,这个模板就受到了很多人的欢迎。_c 函数模板https://blog.csdn.net/2301_81390458/article/details/142054952?spm=1001.2014.3001.5502
目录
1 非类型模板参数
1.1 非类型参数
2 非类型模板参数的应用:array类
3 模板的特化
3.1 特化的概念
3.2 函数模板的特化
3.2.1 函数模板特化的步骤
3.3 类模板的特化
3.3.1 全特化
3.3.2 偏特化(半特化)
4 模板分离编译
4.1 分离编译的介绍
5 模板总结
5.1 优点
5.2 缺点
1 非类型模板参数
模板参数分为类型参数与非类型参数,类型参数实际上就是在模板初阶中所讲的那个内容,出现在模板参数列表中,跟在class或者typename之后的参数类型的名称,由于前面讲过,所以这里我们就不多说了,我们下面直接开始将非类型参数。
1.1 非类型参数
非类型参数:顾名思义,我们这里的这个非类型参数它不是我们定义的一个类型,它就是一个整数类型的常量,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用,我们先来看一下这里的这个格式:
template<class T,size_t N=10>//OK,这就是我们这里的格式,class T是类型参数,size_t N=10是非类型参数,它就是一个常量(整数类型的)
接下来,我们通过代码进一步来讲解它的使用及其一些注意事项:
template<size_t N=10>
class stack
{
public:
stack()
{
v.resize(N);
}
vector<int> v;
};
int main()
{
stack<5> s1;//编译器这里定义了一个stack类类型的对象,这个对象的内部开辟了一个5个int大小的连续数组空间。
cout << s1.v.capacity() << endl;//5;
stack<20> s2;//编译器在这里定义了一个stack类类型的大小,这个对象的内部开辟了一个20个int大小的连续数组空间。
cout << s2.v.capacity() << endl;//20;
stack<> s3;//这种情况就是不传参的写法,他会默认用缺省值作为要开创的空间的个数,进而去开创相应大小的空间,不传参的情况不只这一种写法,还可以写成stack s3;但是对于这种不传参的情况,这里建议使用stack<> s3;这种写法。
cout << s3.v.capacity() << endl;//10;
//通过我们上述过程对非类型参数这个东西的讲解,我们会感觉它就和我们前面所学的#define(宏定义)的功能差不多,但是宏定义相对于它来说,还是逊色一点,宏定义它所定义的变量的大小是死的,是固定不变的,而我们这里的非类型形参却是活的,变量的大小由我们传过来的实参决定,如果不传,则以缺省值为主。
return 0;
}
OK,我们这里的非类型模板参数的主要目的其实就是为了能灵活地改变变量的大小,在非类型模板参数中,其实还有2个需要我们主义的地方:
1>.浮点数,类类型的对象以及字符串是不允许作为非类型模板参数的,也就是说,只有整数类型才可以作为类型模板参数的。我们来看一下,整数类型都包含哪些:int类型,char类型,size_t类型,long类型等。
2>.非类型的模板参数必须在编译器就能确认出结果。
2 非类型模板参数的应用:array类
在1中,我们简单学习并掌握了有关非类型模板参数的相关知识。接下来,我们这里来简单的看一下它的应用,它的应用主要体现在array之中。既然讲我们讲到这里了,我们这里就联合array一并讲解一下吧。
我们现在目前已知的应用了非类型模板参数这个知识点的就是array这个类了,我们这里就结合着代码来看一下array吧,
array,它是一个固定的静态数组,它在底层其实就是一个类,而它的成员变量是一个数组空间,在开创这个数组空间的大小时就使用了非类型模板参数这个知识点,我们可以通过传不同的值的大小,从而开创出我们想要的大小的数组空间,它在类的内部重载了几个较为重要的函数操作,例:
int main()
{
array<int, 10> a1;//a1这个array<int>类型的对象在内部开创了10个大小为int类型的类型空间(a1是一个空间大小为10的数组)。
array<int, 20> a2;//建立了一个拥有20个int类型大小空间数组的a2对象(类型为array<int>,是一个对象,20个int类型大小的数组是它的相应变量)。
//经过我们上述言语,使得我们对array有了一定的知识了解,感觉array它和我们直接定义一个"int arr[10];",这种定义没有什么区别,那C++在这里为什么要再弄一个array这个类模板来代替那种直接定义静态数组的方法呢?
//其实这样做的原因还是因为这个越界检查的问题,既然我们这里说到的这个越界的问题,那么我们就在这里再来补充一个知识,看下列代码:
int arr[10] = { 0 };//我们这里直接定义了一个10个类型的类型均为int的数组空间,并且我们全部将其初始化为0,接下来,我们来看一下它的越界访问。
cout << arr[15] << endl;//随机值;我们这里是选择读取arr[15]这个元素,输出的结果这里是显示的是一个随机值,按照我们的理解,当编译器读取到这里时,编译器应该会在这里报错的,但是,这里为什么会输出一个随机值呢?别急,我们先往后看。
arr[10] = 10;//编译器会报错;错误原因是因为对arr数组越界访问了。
arr[15] = 15;//编译器没有报错,如果我们仔细去看这一行代码的话,我们会发现我们修改了一个越界的元素,编译器在这里理应执行报错操作,但是实际上却没有在这里进行报错,这是为什么呢?通过这三行代码,我们可以知道对于直接定义的数组来说,越界访问进行读取的话,不检查,若是越界修改的话,会抽查,我们接下来来简单的说一下,为什么是抽查,编译器通常会将一个直接定义的数组空间结束后的紧挨着的两个位置设置为标志位,将其中的元素置为-1,若我们改变了这两个位置上的元素的话,那么编译器它就是检测这个位置的元素,不为-1了,就说明被改了,那么它就会报错。当然,如果我们这里修改其他越界位置的元素的话,就不会报错,因为他在检查的时候只会检查那两个标志位的值有没有被改变,越界读取不会检查,越界写会抽查。
cout << a1[10] << endl;//编译器会报错,原因是越界访问。
a1[15] = 10;//编译器会报错,原因是越界修改元素。
//array类类型的对象,在这里只要是越界就会报错,是因为a1[10]这一个步骤,它调用的是类中相应的operate[]这个成员函数,这个成员函数我们在其内部加了防止越界的代码步骤。
//通过上述的解释,在对于越界检查的问题上,array确实比直接定义做的更好,可是这样的话,大家再想想vector,它在函数内部也检查了,这个就要说到开创空间性能的问题了,我们array是直接在栈上开创空间的,而vector是在堆上开相应的空间,我们大家要知道在栈上开空间的效率要比在堆上开空间的效率更好一些。
return 0;
}
(我建议大家好好地把上面的代码中的解释仔细地看一看,我个人认为这部分的内容还是比较重要的)
3 模板的特化
3.1 特化的概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,就需要进行特的殊处理,例:
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
int a = 10, b = 20;
cout << Less(a, b);//1;我们这里调用Less这个类型为int的Less这个函数,在其中判断是否a<b,返回1,这个可以完成比较大小的操作。
int* p1 = new int(10);//在堆上开创一个int类型大小的空间,并将这块空间进行初始化操作,初始化为10。
int* p2 = new int(20);//在堆上开创一个int类型大小的空间,并将这块空间进行初始化操作,初始化为20。
//我们接下来来调用Less函数比较一下10和20这两个元素的大小。
cout << Less(p1, p2) << endl;//这里输出的值是不确定的,如果我们这里多运行几遍的话,有时候这里会输出1,有时候这里会输出0,是不稳定的,这里它调用的是Less<int*>这个实例化之后的函数,最后进行比较的是由p1和p2这两个指针进行比较的,也就是说那个left<right这一步骤,其实是两个地址的值在进行大小比较,而不是我们这里想的数据进行比较操作,既然如此,那么基于这种情况的话,我们就不能继续使用模板生成的那个Less函数了,需要另外想办法去实现了。
return 0;
}
我们通过上述代码的实现我们可以看到Less函数在大多数情况下是可以完成我们的操作的,但是也有一些特殊的情况,就比如说,我们上述的指针这样的,就会无法达到我们想要的效果,基于如此,才有了模板的特化这个知识来解决这个问题,此时,对于上述的指针的这个情况就需要进行模板的特化处理。即:在原模板类的基础上,针对特殊类型所进行的特殊化实现方式。模板特化中又分为函数模板特化与类模板特化。
3.2 函数模板的特化
3.2.1 函数模板特化的步骤
1>.必须要有一个基础的函数模板。
2>.关键字template后面要紧接一对空的尖括号<>。
3>.函数名后面要跟一对尖括号,尖括号中指定需要进行特化处理的类型。
4>.函数形参表:要求这个函数形参表要和模板函数的基础参数类型完全相同,如果不同的话,编译器可能会报出一些奇怪的错误。
template<class T>
bool Less(T left, T right)//我们先在这里定义一个基础的函数模板。
{
return left < right;
}//接下来,我们对指针类型的参数进行一下特化处理。
template<>//关键字template后面要紧跟一对<>。
bool Less<int*>(int* left, int* right)//Less是函数名,函数名后面要跟上一对<>,并且<>中要指定我们需要特化的类型,我们这里特化的类型是int*,并且函数参数列表中的基础参数类型均为int*类型。
{
return *left < *right;
}//上述代码是函数的主体部分,写上我们想要让其实现的思路。
//这里我们再来给大家介绍一下具体的意思:只要我们传到Less中的两个参数都是int*类型的话,就不会再让编译器根据Less函数模板再构造一个类型为int*的Less函数,而是会直接走我们这里的这个特化,走我们这里写的这个特化的代码步骤。
int main()
{
int* p1 = new int(10);
int* p2 = new int(20);
cout << Less(p1, p2) << endl;//1;这里它在调用Less时,就不会直接地去走模板了,而是直接走我们这里前面写的特化地那个版本的Less了。
int a = 10, b = 20;
cout << Less(a, b) << endl;//1;这个调用的就是编译器通过函数模板生成的那个参数为int类型的Less函数了。
return 0;
}
我们前面讲解了函数模板的特化这个过程,讲到这里,我们平时是不怎么推荐使用这个函数模板的特化的,我们这里完全可以使用一个函数来代替上面的这个函数模板的特化。
bool Less(int* left, int* right)
{
return *left < *right;
}//这个函数的效果和前面那个特化的Less函数的效果在这里基本一致,并且它的效果更好一些,当它和前面的那个特化的Less函数同时存在的时候,编译器会调用它,而不是特化。
我们平时在写代码处理像前面指针类似的这种情况的时候,我们是不太支持在这里写一个相关函数模板的特化的,而是建议像上面的代码一样另写一个与函数模板同名的函数专门处理指针,因为这个函数模板的特化有时处理起来真的很恶心。来简单的说明一下,在讲解之前我们这里先简单的来修改一下这里的这个函数模板的这个代码,我们这里使用的传值传参对于内置类型来说这里传值传参就没多大影响,但这里如果是自定义类型的话,就会在这里调用拷贝构造函数会降低性能,因此形参的类型应该是const &。
template<class T>
bool Less(const T& left, const T& right)
{
return left < right;
}//既然如此的话,那么我们这里就需要将特化也改一改了,按照我们所学的知识,改好后:
template<>
bool Less<int*>(const int*& left, const int*& right)
{
return *left < *right;
}//当我们执行这里的这个程序时,发现编译器在这里报错了,说这个特化不是他的专门显示化(特化),为什么呢?
这里的这个原因其实就出自这里的const,我们先来看函数模板,在模板中const修饰的是left和right本身,也就是引用本身,我们再看特化,const它在 " * " 前面,修饰的是指针所指向的内容,还有就是我们这个特化,这里的特化它所特化的是 int* 却写了一个const int*不搭配,因此会报错,也就是说这里应该将const放到 " * " 的后面,如下面代码所示:
template<class T>
bool Less(const T& left, const T& right)
{
return left < right;
}
template<>
bool Less<int*>(int* const& left, int* const& right)//这样写才是正确的。
{
return *left < *right;
}//如果T是指针类型的话,这个问题只最容易出现的,坑害了许多人。
这里我们既然讲到了这个const修饰的问题,我们这里就再来补充一个和const修饰相关的知识点。
int main()
{
const int i = 0;
int const j = 0;//这两种方式其实都是const修饰的两种写法,也就是说,const修饰某一个普通类型时(不包括指针),既可以写在类型之前,也可以写在类型之后,只要没有指针的话,const的位置时可以随意颠倒的。
const int& ci = 10;
int const& cj = 10;//const修饰引用也有这2种写法,const既可以写到类型的前面,也可以写到类型的后面。
return 0;
}
通过上述我们这里所讲述的这里的关于函数模板的特化这个知识,我们发现函数模板的特化使用时是非常麻烦的,有很多的问题需要我们去注意,而且一旦处理不妥当的话,就会让编译器报错,让人感觉到很恶心,一般情况下,如果我们遇到需要我们进行特化去解决的一些问题,能使用函数重载解决的,我们这里尽量去使用函数重载来解决,能不使用函数模板的特化就尽量不要用,但有时我们必须使用函数模板的特化才能解决的问题,就只能使用特化去解决了,没有其他办法。因此,关于函数模板的特化,这部分知识我们还是需要掌握的。
3.3 类模板的特化
这个类模板的特化相较于函数模板的特化来说就显得更有用了。
3.3.1 全特化
全特化就是将模板参数列表中所有的参数都进行特化处理。
template<class T1,class T2>
class Date
{
public:
Date()
{
cout << "Date<T1,T2>" << endl;//由于我么你这里是写特化,因此必须要有一个基础的类模板。
}
private:
T1 _a1;
T2 _a2;
};//写好一个基础的类模板,这里对这个类模板进行全特化操作。
template<>
class Date<int, char>//Date类类型的模板中有两个类型,分别是T1和T2,全特化操作就是将这两个类型(T1和T2)全部进行特化的操作,我们将T1特化成了int类型,T2这个类型给特化成了char类型。
{
public:
Date()
{
cout << "Date<int,char>" << endl;
}
private:
int _a1;
char _a2;//在全特化后的类种,我们这里其实也可以不用写这个成员变量,这里说一下,以后难免会遇到,这是C++语法规定的,这个知识知道就可以了。
};//这个全特化的意思是,我定义一个Date类类型的对象,只要传过来的模板参数列表中第一个类型是int类型并且第二个参数是char类型的话,就不去走模板了,而是直接就走我们这里写的这个全特化后的类去构造相应的对象了。
template<>
class Date<double,int>//只要传过来的两个参数中第一个参数是double类型而且第二个参数是int类型的话,那么就走我们就走这个特化后的类。
{
public:
Date()
{
cout << "Date<double,int>" << endl;
}
};
int main()
{
Date<int, int> d1;//Date<T1,T2>
Date<int, char> d2;//Date<int,char>
Date<double, int> d3;//Date<int,char>
return 0;
}
3.3.2 偏特化(半特化)
任何针对模板参数进一步进行条件限制设计的特化版本。
1>.部分特化:也就是将模板特化参数表中的一部分参数进行特化操作。
template<class T1,class T2>
class Date
{
public:
Date()
{
cout << "Date<T1,T2>" << endl;//由于我们这里是写特化,因此必须要有一个基础的类模板。
}
};//写好一个基础的类模板,这里对这个类模板进行半特化操作。
template<class T1>//我们特化哪个类型,哪个类型在<>中就不用写了,我们这里特化的是T2,就不用在<>里面写T2了。
class Date<T1, int>
{
public:
Date()
{
cout << "Date<T1,int>" << endl;
}
};//偏特化的意思就是说:我定义了一个Date类类型的对象,只要传过去的模板参数列表中的第一个类型是int类型的话,我们这里就不会走类模板去构造相应的对象了,而是会去走我们这里的这个偏特化后的类去构造相应的对象。
template<class T2>//我们这里特化的是T1,就不用在<>里面写T1了。
class Date<double,T2>
{
public:
Date()
{
cout << "Date<double,T2>" << endl;
}
};//传过去的两个类型中只要第一个类型是double类型的话,就走我们这里的这个偏特化后的类。
int main()
{
Date<char, double> d1;//Date<T1,T2>;偏特化的类均不匹配,走模板。
Date<double, double> d2;//Date<double,T2>;
Date<double, char> d3;//Date<double,T2>;
Date<int, int> d4;//Date<T1,int>;
Date<char, int> d5;//Date<T1,int>;
//Date<double, int> d6;//编译器报错,“Date<double,int>”: 多个部分专用化与模板参数列表匹配
//注:我们前面半特化了一个T1(double),也半特化了一个T2(int),在这个情况下,我们不能传:Date<double, int>;如果这样写的话,编译器在这里会报错的,它不知道这里应该调用哪个半特化后的类,因此会报错,若非要构造Date<double, int>这样类型的一个类类型的对象的话,那么就只能另外写一个全特化的类了。
return 0;
}
2>.参数更进一步的限制:偏特化并不仅仅是指特化部分参数,而是还可以针对模板参数更进一步的条件限制所设计出来的一个特化版本。
template<class T1, class T2>
class Date
{
public:
Date()
{
cout << "Date<T1,T2>" << endl;
}
};
template<class T1,class T2>//这里是对参数进行一定程度上的限制,并不是替换某个参数类型,因此特化的类的<>中也要写上对应的两个类型,T1和T2
class Date<T1*, T2*>
{
public:
Date()
{
cout << "Date<T1*,T2*>" << endl;
}
};//只要传过去的两个类型均为指针,那么这里就不会走模板了,而是会走我们这里写的这个偏特化的这个版本的类。
template<class T1,class T2>
class Date<T1&, T2&>
{
public:
Date()
{
cout << "Date<T1&,T2&>" << endl;
}
};//只要传过去的两个类型均为引用,那么这里就不会走模板了,而是会走我们这里写的这个偏特化的这个版本的类。
int main()
{
Date<int*, int*> d1;//Date<T1*,T2*>
Date<int*, char*> d2;//Date<T1*,T2*>
Date<int&, double&> d3;//Date<T1&,T2&>
Date<double&, char&> d4;//Date<T1&,T2&>
return 0;
}
4 模板分离编译
4.1 分离编译的介绍
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有的目标文件链接起来形成单一的可执行文件的过程我们将其称为分离编译模式。
我们在这里若是想要去了解并且弄明白这里的模板为何不能分离编译的话,我们就要在这里弄明白编译器的处理过程。(在讲解之前,我们先把三个文件中的相关代码写好再开始)
//Func.h文件
#include<iostream>
using namespace std;
template<class T>
T Func(T& left, T& right);//这里在Func.h这个文件中对函数模板做了一个简单的声明。
//Func.cpp文件
#include"Func.h"
template<class T>
T func(T& left, T& right)
{
return left + right;
}//对模板进行定义。
//Test.c文件
#include"Func.h"
int main()
{
Func(1, 2);
return 0;
}
接下来,就来为大家具体地讲解一下这里的这个问题:我们编译器它在这里处理文件时,一次只会处理一个相关的文件,并生成相对应的可执行程序。我们这里先来看Func.cpp这个文件,它经过预处理操作之后,将#include"Func.cpp"这个头文件全部展开,里面有声明就会进入到下一个操作中。经过编译,操作后生成 . s 文件,然后会经过编译器的汇编处理,生成相应的 . o 文件,它这里生成的 . o 文件是模板类型的 . o 文件。接下来,我们这里来看一下Test.cpp这个文件,它在main函数内部写了一个Func(1,2)去调用Func<int>这个函数。首先,经过预处理操作,在同文件中有声明就进入下一个操......。最后,生成的 . o 文件中,调用的是Func<int>这个类型的,最后回到链接这个步骤中,这个步骤中会将两个 . o 文件给合并到一起,编译器会到其中去找Func<int>的这个函数的地址,显然编译器他在这里并没有找到,因为在Func.o这个文件中,Func函数是以模板的形式存在的,也就是说他这里并没有进行实体化操作,自然就没有相关的Func<int>这个函数的定义,找不到定义的话,那么编译器在这里就会报错。
综上所述,我们在写模板时,最好将声明和定义放在同一个文件中。(.h文件)
解决方案:1>.cpp直接显示实例化模板。(不推荐,太麻烦)
2>.直接在.h文件中定义模板,用的地方直接就有定义,直接实例化(推荐)。
5 模板总结
5.1 优点
1>.模板复用了代码,很大程度上节省资源。
2>.增强了代码的灵活性。
5.2 缺点
1>.模板会导致代码膨胀的问题,也会导致编译时间变长。
2>.出现模板编译的错误时,错误信息非常凌乱,不易定位。
OK,今天我们就先讲到这里了,那么,我们下一篇再见,谢谢大家的支持!