导图社区 Cplusplus函数模板(上)
这是一篇关于Cplusplus函数模板(上)的思维导图,其中内容包括了Cplusplus模板函数使用大全。
编辑于2021-08-13 22:34:45C++函数模板(上)
函数模板定义
不是一个实实在在的函数
对逻辑功能相同,但数据类型不同的一组函数的统一描述
利用函数模板可以对函数的类型进行参数化处理
返回类型
参数类型
像变量一样改变
利用函数模板可以用一种逻辑过程,处理不同类型的数据,从而极大地提高编程的效率。
使用函数模板的意义
模板是对函数类型的参数化
函数的返回类型
函数的参数类型
强类型语言
C++是一种强类型语言,也就是说,C++中数据的类型一旦确定,就不能修改了,这种特性也体现在函数上。C++函数在声明和定义时,必须指定函数的返回类型以及各个参数的类型。调用函数时也必须按照函数的声明,传入适当类型的实参,并用相应类型的变量保存函数的返回值。如果要用这个函数处理其他类型的参数,则必须定义新的函数。
虽然调用函数时实参和形参的类型可以不一致,但由于存在类型转换,数据精度可能会受到影响,而这种影响通常是不能被接受的。所以必须针对不同的数据类型,重新定义函数的各个版本。
例如定义一个简单的求数组中最小值的函数,要求可以处理各种数据类型。如果不使用模板,那么开发者不得不针对每种类型写一个定义
示例
这里只写出了针对int型和double型数组的函数。为了适应各种情形,还应当编写针对float, char,short,long,unsigned int等所有数据类型的函数。除此之外,还要支持自定义数据类型(如结构体、类)。
如此一来,代码量就会大大增加。而且,一旦算法逻辑发生改变,或者要纠正某个错误,所有的重载函数都需要修改。显然这种面面俱到的方式并不是一种好的做法。
所有这些函数在算法逻辑上都是一样的,不同的只是所处理数据的类型。
如果能将函数的返回类型以及参数类型当做变量对待
当需要处理不同类型数据的函数时,只要将这些“类型变量”赋值为所需的类型就好了。
只编写一次通用算法逻辑就可以处理所有的数据类型,这就是函数模板的含义。
函数模板示意图
通过使用函数模板,避免了定义多余的重载函数,提高了开发效率。
示例
定义函数模板
抽取通用算法逻辑
动机:定义函数模板,常常是出于这样的动机:定义一个对各种数据类型都通用的算法逻辑。
以这里的通用算法逻辑就是两个参数相加,并返回其结果。
实际上,不仅仅类型可以作为模板参数,常量也可以作为模板参数。比如在上节中定义的求数组最小值的函数模板中,数组大小也可以作为模板参数。
特化处理
所谓通用算法逻辑只是一个相对的概念,对于某些特殊的数据类型,或许并不适合该算法。比如字符串的相加(表示字符串连接),直接使用加法运算符“+”就没有意义。对于这种特殊的情况,应当对函数模板进行特化处理
语法
定义函数模板使用template关键字
其后是用逗号分隔的模板参数列表
模板参数可以是一个类型参数,代表一种类型
也可以是一个非类型参数,代表一个常量。
可以是一个或多个类型参数,也可以是一个或多个非类型参数,甚至可以是两种参数的混合。
用尖括号“<”和“>”括起来,该列表不能为空
模板参数
类型参数
前面要带上 typename关键字
非类型参数
可以像声明普通参数一样声明。
template <typname T1, typename T2, int num, double value>;
上述模板参数列表中声明了两个类型参数T1和T2,代表两种数据类型,可以是内置的类型,也可以是自定义类型。同时,该模板参数列表还声明了两个非类型参数 num 和 value,表示该模板在定义过程中可以将num和value当做常量使用。
typename关键字也可以用class关键字代替,两者意义相同,都可以用来声明类型参数。但是用typename更好一些,可以明白地表示后面的参数是一个“类型名”。而且typename是C++标准化的产物,而class关键字则是为了支持C++标准化之前的程序而保留下来的。
在函数参数列表以及函数体的定义过程中,可以使用模板参数列表中的参数。
类型参数
函数返回值
形参
函数体临时变量
非类型参数
函数体中的常量
函数的默认实参
模板的类型参数 Type 与具体类型的关系有点儿类似变量和变量值的关系。Type是一个类型变量,其类型就是“类型”,而其值就是某种具体类型,如 int,double等。
C++标准规定模板参数必须在程序编译时确定。因此非类型的模板参数的值必须是一个常量,而且是编译时就能确认的常量,包括字面常量、符号常量(const常量)。
参数化的函数定义
参数化
函数返回类型参数化
参数类型的参数化
函数中常量类型的参数化
示例
定义一个求最小值的函数模板,对于整数、浮点数其计算过程都是一样的,不一样的只是参数和返回值的类型
第一个Type是函数返回类型的参数
第二个Type和后面的ary[]表示Type类型的数组形参
第四行的Type用来在函数体中定义一个临时变量,该变量的类型是Type类型。
Type是min模板的模板参数,Type可以用来表示多种数据类型,只是此时还未确定,要到模板min被使用时才能确定。
使用非类型参数
函数模板中使用非类型参数,其目的就是为函数引入一个常量,以供定义函数时使用。
然这个常量也可以作为函数参数的默认值使用
示例
使用函数模板
由于函数模板不是函数,所以使用函数模板也不是函数调用。
在使用函数模板之前,必须先确定模板的参数。在C++中,函数模板参数的确定可以由用户明确指定,也可以由编译器自行推导。
实例化函数模版
实例化过程
根据一组或更多具体类型或值构造出具体的函数。
过程是隐式的,编译器会根据使用模板时的情况自动构造。
函数模板
被实例化2次
一次是针对含5个int类型数的数组类型
另一次是针对含7个double类型数的数组类型
函数模板 min 被实例化后,形成了两个具体的函数,就像是程序员直接写成的那样。只不过这两个函数作为编译器的中间结果,只对编译器是可见的,而程序员看不到。
类型参数Type和非类型参数size都被用做函数参数。
为了判断用做模板实参的实际类型和值,编译器需要检查函数调用中提供的函数实参的类型。
示例
下面定义一个函数模板,用于在一个数组中查找某个值。如果找到了,则返回该值在数组中的位置;如果找不到,就返回-1。
取函数模板的地址
取函数模板的地址时也会导致函数模板的实例化。
因为函数模板并不是真正的函数,所以其本身也没有地址。但是 C++并不把“&函数模板名”(取地址符号‘&’也可以省略)这样的用法当做一个错误,而是先对该函数模板进行实例化,然后取实例化所生成函数的地址。
取函数模板的地址实际上是取函数模板实例的地址。函数模板在程序运行时并不存在,存在的只是其实例。函数模板实例化发生在编译期,而取地址则发生在程序运行期。
同取普通函数的地址一样,在取之前需要先定义一个指向该函数模板实例的函数指针。该函数指针的类型就是用来实例化函数模板的参数。
示例
函数指针pf用模板min的地址进行初始化。pf的类型是一个接受拥有3个整型元素的数组,并返回一个整型数的函数。
当用模板min的地址初始化函数指针pf的时候,编译器就会通过pf的类型定义来获取模板实例化参数,并进行实例化。(int min( int (&)[3] ))
实例化的结果是一个实实在在的函数,运行时就可以获取其地址。
在取函数模板实例的地址时,模板实参必须是确定的、唯一的类型或值。如果不唯一,则会导致编译错误。
在这个例子中,func 函数有两个重载版本,其接受的参数分别为两个不同类型的函数指针:INT_FUNC和DOU_FUNC。如果将函数模板min作为参数传递给func函数,则用这两种类型的函数指针去实例化 min 模板就会导致冲突。因为无法判断到底应该用哪种类型的函数指针去实例化模板
要解决这个问题,可以用以下两种方法:
一种是明确指明模板实例化参数。
一种是使用强制类型转换。
int main(){ func(min<int, 3>) }
明确指明模板实例化参数
显式模板实参
int main(){ func(static_cast<INT_FUNC>(min)); }
强制转换指定实参类型
函数模板的实参推演
当函数模板被调用时,编译器根据函数的实参决定模板实参,这个过程称为模板实参推演。
函数模板实参推演的具体步骤如下
依次检查每个函数实参,以确定在每个函数参数的类型中出现的模板参数。
找位置
如果找到模板参数,则通过检查函数实参的类型,推演出相应的模板实参。
匹配模板实参
函数的形参和实参的类型不必完全匹配,只要能够将实参转换为形参的类型即可。
示例
找到实参da和3.3,其类型都是double。
根据函数模板的声明,通过实参数组da得到第一个模板类型参数T的值是double
同时,数组da的大小是6,所以模板实参size的值也是
注意
多个函数实参可以参加同一个模板实参的推演过程。如果模板参数在函数参数表中出现多次,则每个推演出来的类型都必须相同。
示例
在调用函数模板min时,根据第一个参数推演出来的模板实参是unsigned int,而根据第二个参数推演出来的是int。这两个类型不同,因此这个函数模板不能这么使用,而应当如下调用:
min(ui, 1024u);
这样,根据两个函数实参推演出来的模板实参都是unsigned int,符合要求。
实参推演中的类型转换
在函数模板实参的推演过程中,函数实参的类型不一定要严格匹配相应模板实参的类型。只要能够将函数实参转换成相应的模板实参即可。
左值转换
限定符修饰转换
到基类的转换
该基类根据一个类模板实例化而来
左值转换
左值定义
就是可以放在赋值表达式左面的值,也就是可以改变值的程序实体,变量、引用就是左值。
指针
函数指针
普通变量
右值定义
就是放在赋值表达式右面的值,也就是可以从中读取数据的程序实体,例如各种常量、表达式等,变量也是右值。
数组名
函数名
const符号常量
左值 =右值
并不是将左值变成右值,而是说将左值当做右值使用。
包括
从左值到右值的转换
从数组名到指针的转换
从函数名到函数指针的转换
重点
示例
虽然传递给模板 Fun 的并不是符号常量和指针,但是编译器依然可以推导出正确的模板实参。
变量a是一个普通的整型变量,是一个左值。a作为实参,对应模板Fun的第一个参数。该参数是一个被const修饰的右值。由于左值可以转换为右值,所以可以推定T1的类型为int。同样,由于数组可以转换为指针,所以可以推定T2的类型是double。
限定修饰符转换
限定修饰符包括const和volatile。限定符转换指的是普通的指针变量可以转换为const或者volatile型指针。
如果模板实参和形参之间存在这样的转换,那么编译器也可以推导出正确的模板实参类型。
volatitle
volatile的含义是“易变的;变化多端的”,该关键字通常用在多线程程序中。如果一个变量没有被volatile修饰,则编译程序通常会对其取值过程进行优化。即不必每次都去内存中取值,而是尽可能在CPU的寄存器中取值。但是在多线程程序中,该变量在内存中的值可能会被其他线程修改,如果只是在寄存器中取值,有可能取到的不是最新的值。如果变量用volatile修饰,则编译程序不会优化,而是每次都到内存中取值。
限定符修饰转换指的是可以将限定修饰符const和volatile施加到指针变量上。在进行模板实参推演时,如果实际给出的参数是指针,但模板形参是被const或volatile修饰的指针则编译器仍然可以推导出正确的模板实参。
示例
在上述程序中,函数模板Foo接受的参数是一个const指针。但是在实际使用该模板时传入的函数实参却是普通的指针。不过,由于编译器在进行函数模板的实参推演时,允许const修饰转换。所以,依然可以精确地推定模板的实参,依次为int,char和Student。
到基类的转换
模板的函数参数是一个类模板,而该类模板的某个实例有派生类。在使用该函数模板时,传递的参数可能是其派生类的对象,而不是基类的对象。尽管不能跟函数模板声明中的形参精确匹配,但是由于允许将派生类转换为基类,所以编译器仍然能够推演出正确的模板实参。
所谓类模板,简单来说就是定义一个类时,类中的某些数据类型进行了参数化。
函数模板Foo的函数形参是类模板Base某个实例的对象。
在使用模板Foo时,传递的函数实参可以是Derived的某个实例的对象
例如Derived<int> iObj或者Derived<double> dObj。这个实参不必一定是Base的某个实例的对象。这是因为在进行模板实参推演时,允许派生类到基类的转换。
在上述程序中,虽然 Derived<double>并不是 Base<double>,但是编译程序依然
可以推演出正确的模板实参double。
显式指定函数模板的实参
显式指定函数模板的实参也称为显式实例化,其作用主要是解决模板实参推演时的二义性问题。既然指定了模板实参,那么在使用函数模板时就不必进行实参推演了,也就避免了实参推演的二义性问题。例如上节的min模板,可以如下显式指定其模板实参:
上述调用的第二个实参是字面常量1024,其类型是int。因为通过显式模板实参已经推定函数的参数类型为unsigned int,所以参数1024会被转换为类型为unsignedint的参数。