模板/STL|【c++模板笔记一】模板的介绍及其重载

2015年2月11日 周三晴
有一段时间没有更新博客了,这几天在整理前段时间所学的c++知识点就没有更新了。最近开始研究c++的模板的STL,于是开始继续写下自己的一点所得吧。
模板和STL都是c++中比较实用的东西,能把我们省下很多事情(简化编码)。今天作为模板的第一讲,今天讲的模板其实主要是介绍函数模板。模板还有类模板(我们明天介绍)。
【模板/STL|【c++模板笔记一】模板的介绍及其重载】————————————————————分割线——————————————————————
一、为什么需要函数模板? 在介绍什么是函数模板之前,我们还是先一起想想,到底为什么需要这个东西呢?刚好可以复习前面的知识点。 现在我们一起来设计程序:用函数来求两个变量中的最大值(这两个变量,可能是int,double和const char*)。 首先,我们用最传统的 c语言的 解决方法来写代码:

#include #include int max_int(int a,int b){ return a > b ? a : b; } double max_double(double a,double b){ return a > b ? a : b; } const char* max_char(const char* a,const char* b){ return strcmp(a,b) > 0 ? a : b; } int main() { printf("%d\n",max_int(100,200)); printf("%f\n",max_double(1.2,2.2)); printf("%s\n",max_char("hello","world")); return 0; }

模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片
因为变量的类型不同,我们必须设计不一样的函数去分别解决他们。 一共有三种类型,就要去定义三个函数,如果有100种类型,就要去定义100个函数。这就是函数的一个缺点: 虽然能保证函数结果不会出一点错,但是必须为不同类型的进行不同的函数实现。 是不是感觉有点麻烦啊?你说有没有什么简单的方法啊?(c语言中没有函数重载哦,重载我们下面再讲。)其实在之前c程序猿就开始想这个问题了——如果做到 类型无关。 于是,在c语言中就提出了一个东西—— 函数宏(带参宏) 关于什么是函数宏我们这里不做过多的介绍,如果又不懂的同学请自觉向度娘学习。我们用函数宏的方法,解决上面的这个问题:
#include #include #define Max(a,b) ((a) > (b) ? (a) : (b)) int main() { printf("%d\n",Max(100,200)); printf("%f\n",Max(1.2,2.2)); printf("%s\n",Max("hello","world")); return 0; }

模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片

函数宏的原理就是,在预编译时所有Max(a,b) 都会自动被替换为((a) > (b) ? (a) : (b))。所以我们可以看到答案基本正确。但是请仔细看第三个答案,为什么“hello”比“world”大!按照字符串比较原则, 明明“world”大啊! 还记得我们在进行字符串比较的时候都是用strcmp()函数的吗?直接比较会有什么后果?直接比较的话,其实比较的是两个const char*变量,是在比较指针而不是指针所指向字符串的值!!!这就是函数宏的一个缺点: 虽然能适用于大多数参数类型, 不能保证所有对所有类型都安全。 看来在c中没有很好的解决这个问题啊! 我们用c++来试试!其实我知道你憋了很久了,你肯定迫不及待的想使用 函数重载了!(如果你对于c++的函数重载还不是很懂,欢迎点开我的 【c++笔记二】重载(overload)之一看你就懂) 下面我们就用函数重载写一写:
#include using namespace std; int Max(int a,int b){ return a>b?a:b; } double Max(double a,double b){ return a>b?a:b; } string Max(string a,string b){ return a>b?a:b; } int main() { cout<

模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片

在c++中我们用string类取代了c语言中的字符串数组(char*),直接使用“>”去比较两个字符串就更安全了。 或许你可以发现,针对三个类型我们还是写了三个函数,但是和c语言比较起来至少函数名都一样了(不用费脑筋去想各种各样的函数名了)。而且你发现没有,函数的实现体都是一模一样的,这样的重载函数多了,你会发现代码有点 冗余。 所以,c++正式引入—— 模板! 终于扯到我们今天的重点上来了,模板在经历了我们说的:函数、宏函数、重载之后,综合了三者所长。你想,你做个视频,网上有各种视频模板素材,你只要套着模板自己修改一下就可以做出高大上的视频了。做PPT也可以用模板、做简历也有模板。生活中充斥着各种各样的模板,模板给我们带来的便利自然不用我多说吧?童同样的,用上了c++的模板之后,我们也可以省很多事呢!下面正式开始讲解模板。
二、模板的定义和使用 1.定义语法:
template
返回类型 函数模板名 (调用形参表) {函数体; }
首先你要用一个template(模板的英文名)关键字表明你要开始写模板啦!然后在“<>”里面,写上你typename(类型名字)+函数中要用到的形参名,这样你就不用指定具体的形参的参数类型啦,可以写不止一个哦!换一行我们继续来写具体的函数实现体。返回类型和形参类型都可以用"<>"里面定义的那些名字来代替哦! 我们具体看看怎么做吧?把一开始写的那个代码,改成用模板来写!
template T Max(T a,T b){ return a>b?a:b; }

我们的函数体,就浓缩为这四行了。这就是模板的魅力之处——可通用所有的类型!这里你需要特别注意这个typename关键字定义的T,它代表的是任何合法的数据类型,无论是系统定义的(int、double之类的)或者是你自己定义的结构体或类类型都可以的!
2.模板的使用
模板是定义出来 了,我们怎么去使用他们呢?肯定有自己独特的使用方法吧! 函数模板名<类型实参1, 类型实参2, ...> (调用实参表);
使用函数模板其实也很简单,就比调用普通函数多了一个“<>”。这个"<>"里面需要你写上你具体想用哪种数据类型去 实例化模板!(原理下面再讲,先学会怎么去用) 所以我们调用模板,把最开始写的代码运行起来!
#include using namespace std; template T Max(T a,T b){ return a>b?a:b; } int main() { cout<(100,200)<(1.2,2.2)<

模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片

Max(100,200),我们是在告诉编译器,100和200是int类型的,你用int类型去使用模板就好了!函数模板中的T就会自动变成int类型啦。 那到底编译器是如何实现这种功能的呢?我们在一起看看模板的实现方法。
3.模板的实现
模板是如何做到这样强大的功能的呢?很简单,就四个字: 二次编译! 1)第一次编译,是编译器针对函数模板的 定义所做的编译, 一般性的语法检查,生成关于该模板的内部表示。
2)第二次编译,是编译器针对函数模板的 调用所做的编译,用所提供的类型实参结合其内部表示中的类型形参,做类型相关性检查,生成针对 具体函数的机器指令。

第一次编译,也就是在静态时期,编译器只是检查一下你的函数体实现有没有什么语法错误,这时候不考虑数据的类型。 第二次编译,是发生在运行时期,编译器会根据你提供的数据类型,去替换第一次编译时暂代的模板,实现具体的代码指令。 特别注意一点, 函数的模板不是函数,只是一个模板!只有这个模板被具体的数据类型实例化之后,才是一个可以使用的函数! 其实模板也就这么简单是吧!当然不是,还有很多需要注意的地方哦!继续往下看。
4.隐式推断
你有没有想过,如果这句话:Max(100,200)中的“<>”那部分你没有写会怎么样呢?我们一起试验一下。
#include using namespace std; template T Max(T a,T b){ return a>b?a:b; } int main() { cout<

模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片


我们在调用函数模板的时候,都把“<>”部分省略了。程序没有报错,运行起来了,不过结果和我们所想的有所偏差。输出的结果和用宏函数时一样,第三个输出结果出错了。 其实这里发生了“ 隐式推断”。编译器自动(隐式)地去推断了实参的类型,并告诉模板怎么具体的去实例化。 是不是真的这样呢?我们函数模板的实现体中检测一下参数类型不就可以证明到底编译器是不是真的做了隐式推断。这里我们需要用到typeid(不懂的还是度娘哦)。
#include #include using namespace std; template T Max(T a,T b){ cout<b?a:b; } int main() { cout<

模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片


我们在函数实现体中,先输出一下T的具体类型。我们在调用Max(100,200),编译器隐式推断出,100和200的类型为i(int)。同理,编译器也知道1.2和2.2类型为d(double)。我们注意,编译器认为“hello”和“world”的类型是PKc(const char*),所以他们实际上是在比较指针啊亲!(编译器可没那么聪明,知道把他们堪为string对象去比较)所以为什么比较结果显示“hello”大了(可能“hello”指针的值,就是指针的地址更大)。
隐式推断的定义: 如果函数模板的调用参数(圆括号中的参数)的类型和 模板的模板参数(尖括号中的参数) 相关 ,那么在调用该函数模板时,即使不显式指定模板实参,编译器也有能力根据调用参数的类型推断出正确的模板实参,以获得与普通函数调用一致的语法表达。 既然有隐式推断了,我们以后就不写模板参数了吧!这肯定不行,有 三种情况,是不能用隐式推断的: A.模板参数与调用参数的类型无关 我们看例子: 模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片

调用参数是T类型的,而模板参数中还有一个R。因为R和函数参数 无关,编译器根本不能隐式推断出R的具体类型,所以编译器报错了,编译器最后一行说了,不能推断出模板参数R!
B.隐式推断不允许隐式类型转换 我们先来看一个普通的代码:
#include using namespace std; void show(i a){ cout<

模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片
show函数是用来输入int类型的形参a的,但是我们在主函数中调用show函数的时候,传入的实参却是一个char类型的变量ch。但是程序还是运行起来了,并且正确的输出了字母a 的ASCII码。这里,发生了 隐式类型转换,将char隐式的转换为了int。 那么隐式推断能不能也能隐式类型转换呢? 模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片

我们同时传入了一个int和char参数。但是模板中要求的两个参数要相同,这里不能发生隐式类型转换了,所以不能隐式推断! 解决这种问题方法其实也很简单:强制类型转换和显示给定模板参数。如下:
#include using namespace std; template void show(T a,T b){ cout<(10,(int)'a'); return 0; }

模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片


C.返回类型不能隐式推断。 我们还是直接看代码: 模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片

我们调用模板add时候,只显示的给定了T参数的类型为int,编译器并不知道返回值的类型,所以记住,返回类型不能隐式推断。

三、函数模板的重载 1.定义:
在同一作用域中,函数名相同,参数表不同的函数模板和函数模板或者函数模板和普通函数之间构成重载关系。是不是和函数的重载的定义有几分类似?!模板的重载不仅可以发生在模板之间,还能发生在普通函数和模板之间,好神奇哦!
template T Max(T a,T b){ return a > b ? a : b; } const char* Max(const char& a,const char* b){ return strcmp(a,b) > 0 ? a : b; } template T Max(T a,T b,T c){ return Max(Max(a,b),c); }

第一和第三个是函数模板,第二个是普通函数。我们可以发现三者的函数名皆相同,他们构成了重载关系。 第一个模板函数和第二个普通函数,参数表不同所以是重载关系。 第一个模板函数和第三个模板函数,模板参数个数不同所以也是重载关系。
那么我问你一个问题哦:如果我们调用Max("hello","world"),你说编译器会调用第几个函数?这就牵涉到了模板重载的问题了。我们一起往下看重载的一些原则: 2.重载的规则:
(1)编译器优先选择普通函数,除非函数模板能够产生更好匹配性的函数。
#include #include using namespace std; template T Max(T a,T b){ cout<<"T Max(T,T): "; return a > b ? a : b; } const char* Max(const char* a,const char* b){ cout<<"const char* Max(const char*,const char*): "; return strcmp(a,b) > 0 ? a : b; } int main() { cout<

模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片
我们发现,我们调用Max("hello","world")的时候调用的却是普通函数而不是模板,按道理模板跟它也能匹配上。这就是模板重载的一大原则,优先选择普通函数。因为编译器觉得普通函数的 针对性更强一些。 但是我们调用Max(100,200)的时候却是调用的模板,因为这时候模板的匹配性明显更好一点。这就是我们说的第一条原则。 同时通过这种重载的方法,我们还很好的保证了如果比较的是两const char *时,函数实现体的正确性。
(2)在参数传递的过程中如果需要隐式类型转换,编译器将选择普通函数
#include using namespace std; template T Max(T a, T b){ cout<<"T Max(T,T): "; return a > b ? a : b; } int Max(int a,double b){ cout<<"int Max(int,double): "; return a > b ? a : b; } int main() { cout<

模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片

在上面我们说隐式推断的时候,提到了:隐式推断不允许隐式类型转换。如这个程序一样,一旦发生了隐式类型转换,编译器会调用普通函数,而非模板函数。
(3)通过模板参数列表告知编译器使用函数模板时,针对指针的版本比针对任意类型的版本更具有针对性
大家如果仔细观察的话, 前两条重载规则都是发生在不显示指定模板参数的情况下,模板自己做了隐式推断。 如果我们现在,显示的指定模板参数,那么情况又有一点不同了。如果我们实参是两个 指针类型,那么: 针对指针的版本比针对任意类型的版本更具有针对性! 我们一起看代码:
#include using namespace std; template T Max(T a, T b){ cout<<"T Max(T,T): "; return a > b ? a : b; } template T* Max(T* a, T* b){ cout<<"T* Max(T*,T*): "; return a > b ? a : b; } const char* Max(const char* a, const char* b){ cout<<"const char* Max(const char*,const char*): "; return a > b ? a : b; } int main() { const char* ch1 = "hello"; const char* ch2 = "world"; cout<(ch1,ch2)<

这里有两个函数模板,一个普通函数构成重载。如果我们调用调用Max<>(ch1,ch2)你说编译器会调用哪个函数? 你可能会说,这一看就调用第三个普通函数吗!两个都是const char*类型的,明显普通函数的针对性更强一些(我们在原则1中说的)!可是那时候我们调用的时候可没有"<>"这部分哦,那时候是编译器自己在隐式推断。现在我们现实调用模板参数列表"<>",编译器就一定会调用函数模板而不是普通函数了。我们一起来看看结果如何? 模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片
果然,编译器不去调用普通函数了。因为实参类型是指针类型的,而我们刚好有没有指定模板参数具体是什么(我们写的是的空的‘<>“),这里编译器又开始隐式推断了。它推断出这是两个const char*类型的实参,是两个指针,那么编译器就会觉得针对指针版的模板比普通模板 针对性更强,就会调用针对指针版的模板。 那我再问你一个问题,如果我们调用Max<>(&ch1,&ch2),你说编译器会调用哪个?ch1是一个一级指针,再对其&取地址,那就是二级指针啦!或许你会说,我们没有重载二级指针的模板啊!但是:N级指针不就是N-1级指针的一级指针吗?看看编译器到底用谁吧!
#include using namespace std; template T Max(T a, T b){ cout<<"T Max(T,T): "; return a > b ? a : b; } template T* Max(T* a, T* b){ cout<<"T* Max(T*,T*): "; return a > b ? a : b; } const char* Max(const char* a, const char* b){ cout<<"const char* Max(const char*,const char*): "; return a > b ? a : b; } int main() { const char* ch1 = "hello"; const char* ch2 = "world"; cout<(&ch1,&ch2)<

模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片

果不其然,编译器还是调用了针对指针版本的模板。
(4)显式指定的模板实参必须在所选择的重载版本中与调用参数的类型保持一致
你思考一下啊,上面(第三点原则)时我们调用Max<>(ch1,ch2)的时候没有写上具体的模板参数类型,编译器帮我们隐式推断了。如果我们这样写:Max(ch1,ch2),那你说,编译器会调用第几个?
#include using namespace std; template T Max(T a, T b){ cout<<"T Max(T,T): "; return a > b ? a : b; } template T* Max(T* a, T* b){ cout<<"T* Max(T*,T*): "; return a > b ? a : b; } const char* Max(const char* a, const char* b){ cout<<"const char* Max(const char*,const char*): "; return a > b ? a : b; } int main() { const char* ch1 = "hello"; const char* ch2 = "world"; cout<(ch1,ch2)<

模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片

奇怪,显示的指定模板参数类型是const char*之后,它居然不调用针对指针版本的了!按道理,指针版本的更适合调用函数的指针类型啊? 其实是这样的,我们显示的指定模板参数是const char* 之后,T就等价于const char* ,那你说T*等价于什么?当然是const char* *注意,这时候T*就是一个二级指针。 而我们的调用函数是一级指针,当然不太匹配啊。所以显示指定模板参数的时候,编译器会选择类型和调用参数保持一致的重载版本。

(5)在函数模板的实例化函数中,编译器仍然优先选择普通函数,前提是该函数必须在函数模板被第一次编译时可见。
我们先来一起分析代码:
#include #include using namespace std; templateT Max(T a, T b){cout<<"T Max(T,T): "; return a > b ? a : b; }const char* Max(const char* a, const char* b){cout<<"const char* Max(const char*,const char*)"<0?a:b; }templateT Max(T a,T b,T c){cout<<"T Max(T,T,T)"<(ch1,ch2,ch3)<

模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片

这次我们不再是比较两个变量的最大值了,而是三个,所以我们调用Max(ch1,ch2,ch3)的时候肯定会调用含有三个变量的函数模板。所以我们在第一行看见输出了T Max(T,T,T)。可是程序没有我们想的那么简答,而是继续输出了两行。为什么呢? 这和我们T Max(T,T,T)这个函数模板的实现有关。你可以看见,这个函数模板返回的是:Max(Max(a,b),c)。返回的时候还调用了Max含有两个参数的版本,还是递归调用。所以再输出两行也没什么好奇怪的。 但是你发现没有,我们在主函数中可是显示的指定了模板参数为const char*类型的哦,都现实调用模板参数了为什么最后两次还是调用的普通函数? 是,我们是显示指定模板参数了没错,但是这个const char*只是传到了T Max(T,T,T)这个函数模板的T中,但是我们返回的Max(Max(a,b),c)可没有显示指定模板参数,当然这里就不一定会调用模板函数啦!从调用Max(Max(a,b),c)开始,又和上面提到的规则一样了(普通函数的针对性更强)。 这里牵涉到我们前面说的模板的实现的知识。二次编译中,当我们给T Max(T,T,T)这个模板给定const char*实例化为函数的过程中,编译器还要去实例化Max(Max(a,b),c)这两个函数模板。这过程中还是会优先调用普通函数。
但是,如果我们把const char* Max(const char*,const char*)的位置放到T Max(T,T,T)这个函数模板之后,输出结果会还是这样吗?
#include #include using namespace std; template T Max(T a, T b){ cout<<"T Max(T,T)"< b ? a : b; } template T Max(T a,T b,T c){ cout<<"T Max(T,T,T)"<0?a:b; } int main() { const char* ch1 = "123"; const char* ch2 = "12"; const char* ch3 = "1"; cout<(ch1,ch2,ch3)<

模板/STL|【c++模板笔记一】模板的介绍及其重载
文章图片

你会发现,它不再去调用普通函数了。这是因为,普通函数在该函数模板之后,在第一次编译时,函数模板发现不了这个普通函数(无法完成静态的关联)。 所以以后再模板中嵌套模板的时候, 大家要多多注意。 综述, 在函数模板的实例化函数中,编译器仍然优先选择普通函数,前提是该函数必须在函数模板被第一次编译时可见。

———————————————————————————分割线—————————————————————————— 好了,今天的知识点就讲到这里了。 我们一起来稍微总结一下:首先我们介绍了为什么要引入模板。接着我们介绍了怎么去定义和使用模板,并且分析了模板的实现方法和一些隐式推断规则。最后我们还一起推断了一下模板重载的一些规则。 如果大家对本文有什么疑问,或者有什么不足之处,欢迎评论留言。

    推荐阅读