作者:几冬雪来
时间:2023年10月18日
内容:C++——模板进阶讲解
目录
前言:
在上一篇博客我们学习了反向迭代器之后,今天讲解的知识则是我们的老朋友——模板。但是并非是将原先的模板内容再讲解一遍,今天我们要了解的是模板进阶的内容。
模板(进阶):
在一开始讲解模板的时候,我们使用模板做了许多的操作。
那个时候的模板解决了学习C++过程中的两类问题,一类是可以用它来控制容器中的数据类型。第二类就不仅仅是控制数据的类型,而是控制某种设计逻辑,比如适配器模式。
也就是说,如果在代码中如果要传一个类型的话,就可以使用模板来搞定。
typename和class特殊场景:
在模板中我们一般使用class和typename来定义模板,在定义模板的时候class和typename的作用是相同的。
但是在某些场景下,class和typename会有一些区别所在。
在这里我们简单的写了一串代码,也就是插入值和输出指的操作。
但是在这个地方我们的Print却并不是那么好,因为Print里面的参数的vector类型的。如果要在写一个list类型的话,这个地方我们的Print就不能使用了。
因此这里我们的思想要往泛型上靠近。
因为学习过模板的原因,在这个地方可能会有人说:这里可以和适配器一样写一个模板class Container。
用Container来解决上面的问题,把原先vector<int>的位置给替换成Container。
这个方法理论是可以的,但是编译器会报错。
代码报错,这是因为在这个地方Container没有被实例化。在编译器中编译代码都是从上往下走的,因此编译器无法区分Container是什么类型。
之前能成功是因为已经清楚了类型是vector<int>,它已经被实例化出来了,我们可以去实例化的类中查找和使用。
而如上图这样子书写的话有两个地方可以使用这个语法,一个是静态成员变量,另一个是内部类,这个地方可以是类型也可能是对象。
而解决这个问题的方法就是加上一个typename,在编译器中typename是类型的名称。
在Container前面加上typename就是在告诉编译器这里是类型不是对象,是合乎语法的,等这个模板实例化了之后再去找。
非类型模板参数:
接下来我们来讲解一下非类型模板参数。
非类型模板参数顾名思义就是不是类型的模板参数,而有的时候需要我们去使用它。
那么在什么时候非类型模板参数会被使用到呢?比如在这里我们要定义静态的栈的时候就需要使用到它。
在这里我们就写了一个静态栈。
而栈的外面define了一个N,这里将它定义为10。
在开空间的过程中,如果我们要开的空间的大小刚好为10的话,N就不用进行修改。但是要开100个空间的时候N就办不到了,如果N定位为100的话,只开10个空间又太浪费了。
为了解决这个问题我们使用到了非类型模板参数。
实现非类型模板参数,这个地方只需要将模板参数中加入一个size_t来代表我们要传的常量的大小即可。
类似这里在下面书写的时候要开多大空间的话只需要将它的值一起写上就行了。
这里还要注意一个点那就是这个地方的N因为是常量所以不能被修改的。有些编译器中N被修改了可能不会报错,这里就应该发生了按需实例化的操作。(调用了才会实例化)
非类型模板参数条件:
1.常量。
2.必须是整形。(现阶段)
同时非类型模板参数也有许多的条件限制。
array(使用非类型模板参数):
而在C++中array(定常数组)就会用到非类型模板参数。
可是在C++中array和原生数组的使用几乎一样。原生数能做到的array也能做到,但同时array也保留了一些臭毛病。
比如原生数组定义的时候不会初始化,而array也不会进行初始化的操作。所以从某方面来说array非常的鸡肋。
唯一对比原生数组有优势的就是array对越界的检查非常的严格。
这是因为array中重载了operator[]在operator[]中就对越界进行了检查。
这也是array对比起原生数组的一个优势,但是如果数组不越界的话,那么二者的作用就是一样的没有区别。
模板的特化:
然后再下来讲解的是模板的特化,在C++中有时候可能要针对某些特殊化的处理,这里我们就把这个行为简称为特化。
接下来就来讲解一下模板特化的场景。
在这里我们简单的写一串代码。
这个地方是通过Less进行比较左值和右值之间的大小,这里第一次比较是自己传了两个常数进行比较,第二次则是先定义初始化了a和b,再传二者的地址进行比较。
第一次比较是比较二者的值,第二次比较就是比较的二者的地址。
为了解决这个问题我们编译了函数特化的代码,如果要传的类型是普通类型,这里我们就走原类模板的实例化,如果是int*的指针就会优先走我们上面的代码。
当然,这里也可以写一个现成的函数出来,与原模板构成实例化的函数重载,代码中有现成的就吃现成的。
所以我们的函数模板可以写成特化,也可以直接书写这个函数。
虽然函数模板可以直接书写函数来使用,但是类模板却没有办法,像上边就是我们一个简单的类模板。
如果这个地方要对<int,double>进行特殊化处理的话,那就只能使用特化操作了。
特殊化处理要有原模板存在。
与此同时在特殊化处理中我们还分全特化和半(偏)特化。
在这里偏特化就是不把所以的模板参数都限制死,一部分进行特化,另一部分还是用原模板的参数。
同时偏特化里面还分两种特化方式。
一种是特化部分参数,另外一种特化则是对类型的进一步限制。
像这里就是使用了特化来对类型做了进一步的限制。在这个地方的特化T1和T2都特化为指针类型。
如果下面传的是指针无论是什么类型的指针都会匹配,相反如果不是指针就不会匹配。
这就是特化对类型的限制,同时特化的类相比原来的类是一个全新的类,但是它不能独立存在。
模板的分离编译:
在C++甚至是C语言中我们都知道声明和定义是不能被分离的,二者如果简单被分离的话会发生链接错误。
在类模板声明和定义也是如此。
在这里要找到我们的类(Stack),但是这里我们还需要将类模板参数给传过去(T),与此同时因为这个地方没有模板参数的存在,因此还要写一个模板。
接下来继续讲我们的链接错误。
链接错误就是代码在编译的时候过了,但是在发表的时候却找不到它的地址。
接下来就来探讨为什么会出现编译错误的操作。
因为一开始二者都有声明,在C语言中声明只是类似一种承诺。使用编译器检查声明函数名参数返回的时候是可以对上的,再等到链接的时候,拿着修饰后的函数去其他文件符号中查找。
而且在C++中有一个方法可以解决这个问题,那就是显示实例化。
在这里显示实例化,对类模板显示实例化,那么这里成员也会被显示实例化。
但是类的类型从int变为double的话。
那么为了使这种情况也可以通过,我们要将显示实例化中整形int类型给更换为double类型,这里编译器才不会报错。
但是这样就会导致每新出一个这里我们就得加一个显示实例化。
在模板中,它不支持直接的声明和定义分离,但是在分离编译后显示实例化是可以的。
但是这个地方我们还有一种方法。
就算声明与定义分离,模板的话最好在当前文件进行声明与定义的分离。
模板总结:
最后这个地方赋上一张图来总结一下模板。
迄今为止学习到这里,我们的模板的大致学习内容就已然落幕了。
接下来我们来总结一下它的优点,有了模板以后我们写许多的代码就变得很方便了,原本应该是我们去做的事情交给了编译器去做。
其次它还增加了代码的灵活性,比如适配器和仿函数。
但是与此同时,它也有缺点的存在。
应该就是代码膨胀的问题,因为模板要走实例化的过程,因此编译的时间会变长。
而这里模板真正的缺点就是它的报错很凌乱,模板的报错正确率可能会一定程度的下降,模板提示的可信度会下降。
结尾:
到这里我们的模板进阶就已经讲解完毕了,C++所有的模板知识此刻也就正式的完结了。接下来我们将向C++更深层的知识发起进攻,最后希望这篇博客能为各位带来一些有用的帮助。