C++学习第三十五天

编程入门 行业动态 更新时间:2024-10-19 15:29:48

C++学习第三十五天

C++学习第三十五天

1.函数模板

         函数模板是通用的函数描述,即使用泛型来定义函数,其中的泛型可用具体的数据类型(如int,doubel等)替换。  

        由于模板允许以泛型的方式编写程序,所以有时也叫做通用编程

        由于类型是用参数表示的,因此模板特性有时也叫做参数化类型(parameterized types)。

template <typename AnyType>
void Swap(AnyType &a, AnyType &b)
{AnyType temp;temp = a;a = b;b = temp;
}

        上述代码是一个交换两个变量值的模板函数;第一行:AnyType是定义的一个类型,这个类型名可以任意选择,但也要符合c++命名规则;尖括号是必须的;另外两个是关键字,跟着规则学就行,其中typename关键字可以用class替换,这是由于在标准c++98后才添加了typename关键字;

        如果需要多个将同一种算法用于不同类型的函数,使用模板;

        如果不考虑向后兼容的问题,并愿意键入较长的单词,则声明类型参数时,使用关键字typename,不使用class;

        下面程序就是创建了一个交换数值的模板函数,第一次传递的是int类型的,第二次传递的是double类型的;

        说明一下,在调用模板函数时,编译器会自动生成对应的版本,实际上它只是使程序看起来更简单了,并没有缩短程序。

code_c++/funtemp.cpp · Kite/C和C++ - 码云 - 开源中国 (gitee)

2.重载的模板

        需要对多个不同类型使用同一种算法的函数时,可使用模板;

        但同样可以像重载常规函数定义那样重载模板定义;

        与常规重载一样,被重载的模板的函数特征标必须不同;

        还需注意一点,并非所有的模板参数都必须是模板参数类型。

3.模板的局限性

        假设有如下模板函数;

template <class T>
void f(T a, T b)
{……;
}a = b;//(1)if(a > b);//(2)T c = a * b;//(3)

        假设有(1)的赋值语句,但如果T是数组,这就是错误的,这是因为数组初始化之后不能直接赋值;

        假设有(2)的带有比较 > 的语句,但如果T是结构呢,这也是错误的;如果T是数组呢?由于数组名是地址,所以它比较的是数组的地址,我觉得这应该不是我们所需要的吧;

        假设有(3)的带有乘法运算符 * 的语句,如果T是数组、指针或结构呢?这也是错误的。

        假如结构中包含位置坐标,那么相加是有意义的,但是没有给结构定义运算符+,怎么解决呢?第一,允许重载运算符+,以便能够将其用于特定的结构或类;另一种,为特定类型提供具体化的模板定义。见下一小节介绍。

4.显式具体化

        假设定义了如下结构;

struct job
{char name[10];double salary;int floor;
};

        假如要交换两个上述这种结构的内容,c++是允许将一个结构赋值给另一个结构的,但是如果只想交换结构中的部分内容,那么模板就不可以使用了。

        但是,可以提供一个具体化函数定义----称为显式具体化(explicit specialization)。当编译器找到与函数调用相匹配的具体化定义时,将使用该定义,而不再寻找模板。

5.具体化机制----c++标准定义     

1.第三代具体化(ISO/ANSI C++标准)

        c++98标准;

第一,对于给定的函数名,可以有非模板函数、模板函数和显示具体化模板函数以及它们的重载版本;
第二,显式具体化的原型和定义应以template<>打头,并通过名称来指出类型;
第三,具体化优先于常规模板,而非模板函数优先于具体化和常规模板;

        下面是用于交换job结构的非模板函数、模板函数和具体化的原型;

void Swap(job &, job &);//非模板函数template <typename T>
void Swap(T &, T &);//模板函数template <> void Swap<job>(job &, job &);//具体化1
template <> void Swap(job &, job &);//具体化2

        这里具体化1和具体化2都是可以的。

2.显式具体化示例

        程序清单8.13;显式具体化; · 47a186d · Kite/C和C++ - Gitee

运行结果如下图;

6.实例化和具体化

         代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。

         编译器使用模板为特定类型生成函数模板时,得到的是模板实例。例如上面5小节的8.13程序,函数调用了Swap(i,j)导致编译器生成Swap()的一个实例,使用int类型。

        模板不是函数定义,但是如上述所说的根据模板生成了一个int类型的实例,这个实例就是函数定义了。这种实例化方式被称为隐式实例化

        最初,编译器只能通过隐式实例化,来使用模板生成函数定义。

        现在,c++还允许显式实例化,即通过直接命令编译器创建特定的实例。如Swap<int>()。语法是:声明所需的种类----用<>符号指示类型,并在声明前加上关键字template;

template void Swap<int>(int, int);

        下面说一下自己对隐式实例化和显式实例化的区别:隐式是在调用函数时直接传递的参数是有类型的 ;而显示是提前说明了参数的类型。

7.显式实例化和显式具体化的区别

        上面也说了,显式具体化那一节有说明:有两种声明方式,是等价的,有了显式具体化的定义,那么每一个具体化函数都应该有自己的原型且有函数定义;显式具体化在关键字temolate后包含<>,显式实例化没有

        试图在一个文件(或转换单元)中使用同一类型的显式实例和显式具体化将出错。

        还可通过在程序中使用函数来创建显式实例化。如下代码;

template <class T>
T Add<T a, T b>
{return a + b;
}……int m = 6;
double x = 10.2;
cout << Add<doubel>(x, m) << endl;

        这里模板和函数调用是不匹配的,因为在模板要求的参数类型是相同的;但通过使用 Add<doubel>(x, m),强制为doubel类型实例化,m参数强制转换为double类型,这样就可以了。

        隐式实例化、显式实例化和显式具体化统称为具体化。它们有一个共同点:表示的都是使用具体类型的函数定义,而不是通用描述。

        最后不要搞混了实例化和显式具体化,以及函数模板的区别。

8.编译器选择使用哪个函数版本

        看到这里,我们学了函数重载、函数模板和函数模板重载,大家都是一脸懵,那么对于c++如何去决定为函数调用使用哪一个函数定义呢?尤其在有多个参数时。很显然是有的,这个过程就叫做重载解析

        下面就了解一下这个过程是如何进行的;

第一步:创建候选函数列表。其中包含与被调用函数的名称相同的函数和函数模板。第二步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。第三步:确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。

        下面举一个例子;

例如:may(‘B’);

        首先,编译器将寻找候选人,即名称为may()的函数和函数模板。然后再看可以用一个参数调用的函数。

例如有下述这几个函数符合要求;void may(int);
float may(float, float = 3);
void may(char);
char * may(const char *);//4
char may(const char &);
template<class T> void may(cosnt T &);
template<class T> void may(T *);//7

        首先4和7不行,参数是指针类型的,但是这里整数类型不能被隐式的转换为指针类型。

        然后就要找出剩余5个中最佳的选择了。

        编译器会查看函数调用参数与可行的候选函数的参数匹配所需要进行的转换。

        通常来说,从最佳到最差的顺序如下;

1,完全匹配,但常规函数优先于模板;
2,提升转换,例如char或short转换为int,float转换为double;
3,标准转换,例如int转换为char,long转换为double;
4,用户定义的转换,如类声明中定义的转换;

        例如上面举得例子,1是提升转换,2是标准转换,3,5,6都是完全匹配;

        但是完全匹配中6是模板,所以现在感觉最优的是3和5;

        但是前面学习也学过,多个最优选择会被认为是错误的,那么这里是怎么回事呢?

        下面就要介绍一下完全匹配和最佳匹配了。

9.完全匹配和最佳匹配

        进行完全匹配时,某些无关紧要的转换是允许的。Type表示任意类型。

        Type意味着用作实参的函数名字与用作形参的函数指针只要返回类型和参数列表相同就是完全匹配。

        下表是完全匹配允许的无关紧要转换

        下面举个简单例子说明问题;

有下面的函数;
struct blot {int a; char b[10];};
blot ink = {25, "sports"};那么下面的原型都是完全匹配的;
void rec(blot);//1
void rec(const blot);//2
void rec(blot &);//3
void rec(const blot &);//4

         有多个匹配选项,编译器无法完成重载解析过程;如果没有最佳选择,编译器就会报错,生成一条错误消息,使用诸如“ambiguous(二义性)”这样的词语。

        但是有些时候,即使两个函数都完全匹配,仍可完成重载解析。

        例如,rec()示例,如果只定义了3和4完全匹配,就选择3了,因为ink未被声明为const。也就是说,指向非const数据的指针和引用优先与非const指针和引用参数匹配。但是注意了,const和非const这个区别只是适用于指针和引用指向的数据。例如,如果只定义了1和2就会出现二义性错误。

        非模板函数优先于具体化和常规模板,第5小节也强调过。

        那么如果都是模板函数呢?显式具体化优先于使用模板隐式生成的具体化

        “最具体”不一定就是显式具体化,而是指编译器推断使用哪种类型时执行的转换最少;例如;

template <class Type> void rec (Type T);//模板1
template <class Type> void rec (Type * T);//模板2struct blot {int a; char b[10];};
blot ink = {25, "sports"};rec(&ink);

        现在说明一下模板1,Type --> blot*;隐式实例rec<blot >(blot *);

        模板2,Type --> ink,也就是blot;隐式实例rec<blot *>(blot *);//更具体;

        用于找出最具体的模板的规则被称为函数模板的部分排序规则

        最后总结:

        第一,重载解析将寻找最匹配的函数。

        如果只存在一个这样的函数,就只能选择它;

        如果有多个这样的函数,但只有一个非模板函数则选择该非模板函数;

        如果存在多个合适的且都为模板函数,则选择其中最具体的函数;

        如果存在多个合适的非模板函数或模板函数,但具体程度都一样,则结果报错;

        如果不存在匹配函数,也是错误的。

10.创建自定义选择

        有些情况下,可以通过编写合适的函数调用,引导编译器做出您希望的选择。

11.多个参数的函数

        有多个参数的函数调用与有多个参数的原型进行匹配情况是很复杂的。编译器必须考虑所有参数的匹配情况;选择的优先级在前面已经多次说明过。

12.模板函数的发展

        模板函数和模板类:在c++98做出了一次修改,并添加了标准模板库;c++11继续做了修改。

第一,是什么类型

        c++98中,编写模板函数时,一个问题是不能总能知道应在声明中使用哪种类型。请看示例;

template<class T1, class T2)
void ft(T1 x, T2 y)
{?type? xpy = x + y;
}

        请问这里的xpy应为是什么类型的?结果是T1、T2或者别的类型。

        例如一个double,一个int,那么结果就是double;

        如果一个是short,一个char,那么结果就会使int,是因为这里导致了自动整型提升。

        另外,结构和类可能重载运算符+,这样的话问题更加复杂了。所以在c++98中,上面的xpy类型是没办法进行声明的。

第二,关键字decltype(c++11)

        c++11新增加了关键字decltype用于解决上面所说的问题;

int x;
decltype(x) y;//1decltype(x + y) xpy;
xpy = x + y;//2decltype(x + y) xpy = x + y;//3

        看1,这句代码的含义是,y的类型要取决于decltype参数x的类型,二者类型相同;

        看2,说明给decltype提供的参数可以是表达式;

        看3,的意思是说可以将两行代码缩写为一行;

        decltype要确定类型,编译器就要必须遍历一个核对表; 

假设有如下声明:
decltype(expression) var;

        下面对核对表的简化版做下介绍;

第一步,如果expression是一个没有用括号括起来的标识符,则var类型与该标识符的类型相同;

举例:

double x = 5.5;

double y = 3.5;

doubel &rx = x;

const double * pd;

decltpe(x)  w;

decltype(rx) u = y;

decltype(pd) v;

第二步,如果expression是一个函数调用,则var类型与函数的返回类型相同;

                这个并不会实际调用函数,编译器通过查看函数原型来获取返回类型;

举例:

long inder(int);

decltype (inder(3))  m;

第三步,如果expression是一个左值,则var为指向其类型的引用;

                那这是不是和第一步的有点冲突,w的类型是引用?因为x是左值;

                注意这里,expression是用括号括起来的标识符,否则就是第一步处理;

举例:

double xx = 4.4;

decltype( (xx) ) r2 = xx;

decltype ( (x) ) w = xx;

括号不会改变表达式的值和左值性。

举例:

xx = 98.6;

(xx) = 98.6;

第四步,如果前面的都不符合,则var类型和 expression类型相同;

        下面,虽然看k和n都是引用,但是表达式k+n不是引用,它是两个int的和,所以是int;

举例:

int j = 3;

int &k = j;

int & n = j;

decltype(k+ n) i3;

第三,另一种函数声明语法(c++后置返回类型)

        还有一种可能是decltype本身无法解决的;

double h(int x, float y);
对于上述的原型可以用新增的语法编写如下;
auto h(int x, float y) -> double;

        将返回类型移到了参数声明后面。->double 被称为后置返回类型。其中auto是一个占位符,表示后置返回类型提供的类型,这是c++11给auto新增的一种角色。同样,此语法也可用于函数定义:

auto h(int x, float y) -> double
{}

然后结合decltype,就可以给函数指定返回类型了;这就是上面所说的decltype本身无法解决的,gt()的返回类型是不确定的,因为x和y参数类型是不确定的。

template<class T1, class T2>
auto gt(T1 x,T2 y) -> decltype(x + y)
{return x + y;
}

更多推荐

C++学习第三十五天

本文发布于:2024-02-16 23:21:49,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1691804.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!