1.array
(1)普通数组的劣势
当我们直接越界修改值时,一般会在编译时就被拦截
但是越界访问,只要访问距离不算特别大,那么也可以越界访问
当我们不直接越界修改或访问,间接去访问和修改能越界非常远
这里的i直到342才被拦截下来,这足以反映出普通的数组是有一定的风险的,在有的情况下会导致数据被篡改,下面以VS编译器为例,在VS中栈区变量由低地址向高地址存,所以向上越界修改会影响其它变量的值,如果这个值很重要,那么使用普通数组就很危险,我们需要一种更安全的方式,这就是array。
(2)array检查方式和普通数组的区别
array是用类封装静态数组,其中类最大的好处是可以重载operator[],可以在重载函数里面添加断言检查等,每一次调用都会进行检查是否越界,如果越界会直接报错。
而普通数组是在数组界外设置了一些标志位,如果标志位被访问或者修改了就会报错,但整体上而言,我们能够进行不同程度上的越界访问和修改,依然有风险,就像上面展示的那样。
当我们使用array时,就能很好地避免这个问题。
但是arrary不如使用vector,因为在处理上和vector几乎没什么区别,但array是在栈区开辟,而vector是在堆区开辟空间,堆区的空间要远远大于栈区的空间。所以推荐使用vector,其相较于普通数组的优势和array的一模一样。
2.非类型模板参数
定义模板时要传模板参数,其中有类型模板参数,如class T参数接受的就是一个类型(如int、double),类型模板参数实施例化出函数或类的关键,也是模板的精髓所在。但除此之外,在模板参数处还可以定义非类型模板参数,如int a,char b接收的就是值而非类型。
利用非类型模板参数定义的是常量而不是变量,不能有任何修改操作
非类型模板参数在C++20前只能是整型家族的,如char、size_t、int等,而指针、double类型是不支持的
切换到C++20,就可以使用内置类型作为非类型模板参数了,包括内置类型的指针、double等
设置界面如下:
但是需要注意的是,C++20后仍不支持自定义类型作为非类型模板参数,像string这些都不支持
3.模板的按需实例化
模板有个特点是按需实例化,在我们没有调用这个模板类或模板函数时,它是不会实例化的。这意味着编译器只会检查最基本的语法错误,而不会去检查里面的细节
(1)普通函数和模板的检查严格程度的比较
我们来对比一下
对于普通函数,就算我们不去调用它,编译器在编译阶段是会比较深入的去检查的,这里由于"a"是常量字符串,不能修改,所以交给arr的指针不能*arr,因此要用const char*,所以报错了。这里同时也要注意1和3是有区别的,3是单独在栈区开了一块空间,所以不会报错,而1并没有开空间。
而对于模板来说,编译器的检查就很弱了。
在这里我们可以看到,char* arr = "a"本身就是个错误写法但没有报错。包括Fun()里面的N++也是经典的语法错误,但这些在编译阶段都不会被检查出来,只有在触发了很离谱的语法错误(忘写分号等)才会检查出来,而且这个时候的报错极为难看。
记住下面这种报错原因,等会有用
只有当实例化后我们才能检查到错误,就算类被实例化了,如果里面有模板函数,在被调用前这些模板函数也不会实例化,这就是按需实例化。
(2)typename声明
刚刚第二张模板类报错是因为arr b = "a"犯了严重的语法错误。即arr不是类型。看上去这个错误很荒谬,但是这引出了一个新的问题,如下图:
究竟是什么导致了错误,我们已经知道,模板类实例化之前检查语法很弱,但这里就有一个显然的语法错误,那就是Test<T>::iterator it中iterator究竟是静态成员变量还是一个类型呢?
这就跟我最开始提到的arr b = "a"出现了同样的问题,为了避免歧义,我们需要在最前面声明它是一个类型而非静态成员函数。
但是这里需要注意的是这是在模板的前提下才需要这么写,如果已经实例化出了Test<int>,这种语法就不会导致歧义,编译器能很轻松的判断这是一个类型
4.特化
(1)全特化
函数模板的特化和类模板的特化用法上都很好理解,实际就是针对一些很特殊的类型做特殊的处理,注意要写template<>,这其实是声明这是个模板,没有模板参数就不写,在函数名或类名后要写实例化的类型。
但这里需要特别注意的是全特化指针或引用时,要注意全特化的函数的传参要和模板函数的参数匹配
下面这种写法为什么有问题?
原因在于const T中const修饰的是ptr,即ptr不能修改,同理,ref也不能修改。而const int*的意思是*ptr而不是ptr本身,ref也是如此,所以const的位置要变,下面这种才是正确写法
在优先级上,如果已经定义了普通函数,就会最优先调用普通函数,其次找特化,如果实在没有,就会去实例化模板函数。一般来说特化用的不多,因为像刚才那样的坑很难理解,所以如果真要特殊处理,直接写普通函数是最好的选择。
(2)偏特化(只能针对类)
偏特化(半特化)是针对某一个模板参数而非全部模板参数进行特殊化处理。针对的方式可以是类型,也可以是某种修饰,如*、&等。
先介绍一个简单的,就是针对某一个或几个而非全部模板参数的特殊处理。在写法上和全特化很像,我们可以认为全特化是偏特化的一种特殊情况。
但是注意偏特化不能针对函数,只能在类上使用偏特化
偏特化中还有一种就是针对某种修饰方式进行特殊处理
看下面这段代码
#include <iostream>
using namespace std;template<class T1, class T2>
class test
{
public:test(){cout << "test" << endl;}
};template<class T1, class T2>
class test<T1*, T2&>
{
public:test(){cout << "T1 = " << typeid(T1).name() << endl << "T2 = " << typeid(T2).name() << endl;}
};int main()
{test<int, char>();test<int*, int&>();return 0;
}
我们发现当实例化是加上*或者&时,当实例化类型对上时就会走偏特化。而对于T1和T2而言还能反推。
需要注意的是,const也可作为修饰成分并严格要求匹配才会调用偏特化
偏特化的好处在于能针对处理的类型更灵活,并且通过修饰的类型能推导原类型,我们又可以通过这个原类型加一些修饰得到其它修饰的类型,在代码中选择更多。
5.编译模板
模板是不能声明和定义分到两个文件中的。
原因在于如果对于普通函数,虽然声明定义分离,但是定义处的函数代码仍然完整,可以顺利编译,链接时进符号表,如果声明定义没有分离,那在编译阶段就能进符号表。当调用函数时,会直接到符号表中找,效率很高。
但对于模板函数编译器是不会编译的,因为不知道编译成什么。如果让模板去遍历文件找实例化,大型项目文件多的情况效率大大降低,不会这么做。当编译器遇到调用模板函数时,就会实例化声明,但这个声明找不到定义,定义处直接被跳过编译了。而如果在同一文件或在定义处显式声明实例化类型,就能在第一时间实例化。
总结一下:唯一的方案是模板不要分离文件,声明处知道实例化成什么但没定义,定义处有定义但不知道实例化成什么
6.模板缺陷:编译时间变长,错误信息凌乱
7...cc就是.cpp,而.hpp是.cpp和.h结合,根据模板的性质,我们可以将模板写在.hpp文件中