2024年1月12日发(作者:)
本文作者:黄邦勇帅学习本文需要对C++的构造函数,C++中的各种类成员,C++中类的继承,有一定的了解。模板是C++中的重点内容,因此应熟悉C++模板的使用,本文是C++的模板专题,因此本文集中介绍了C++的模板问题,主要介绍了类模板和函数模板的形参与实参(这是学习模板的关键内容),模板的具体化(特化)方法,模板实参推演,模板函数的匹配,类模板中的模板成员,模板与继承,模板与友元,typename关键字。本文内容全面,简单易懂,是对于学习模板模棱两可的读者的很好的参考文现。本文内容完全属于个人见解与参考文现的作者无关,其中难免有误解之处,望指出更正。声明:禁止抄袭本文,若需要转载本文请注明转载的网址,或者注明转载自“黄邦勇帅”。主要参考文献:1、C++..第五版.中文版[美]Stephen Prata著孙建春韦强译人民邮电出版社2005年5月2、C++..第四版.中文版Stanley n、Barbara 著李师贤等译人民邮电出版社2006年3月3、C++..第三版.中文版Stanley n等著潘爱民张丽译中国电力出版社2002年5月4、C++入门经典第三版[美]Ivor Horton著李予敏译清华大学出版社2006年1月5、C++参考大全第四版[美]Herbert Schidt著周志荣朱德芳于秀山等译电子工业出版社2003年9月6、21天学通第四版C++ [美]Jesse Liberty著康博创作室译人民邮电出版社2002年3月第17章模板使用模板的目的就是能够让程序员编写与类型无关的代码。比如编写了一个交换两个整型int类型的swap函数,这个函数就只能实现int型,对double,字符这些类型无法实现,要实现这些类型的交换就要重新编写另一个swap函数。使用模板的目的就是要让这程序的实现与类型无关,比如一个swap模板函数,即可以实现int型,又可以实现double型的交换。模板可以应用于函数和类。下面分别介绍注意:模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。一、模板函数通式1、模板函数的通用形式为:template 反回类型函数名(参数列表){函数体}。其中template和class是关见字,class可以用typename关见字代替,在这里typename和class没区别,<>括号中的参数叫模板形参,模板形参和函数形参很相像,模板形参不能为空。一但声明了模板函数就可以用模板函数的形参名声明类中的成员变量和成员函数,即可以在该函数中使用内置类型的地方都可以使用模板形参名。模板形参需要调用该模板函数时提供的模板实参来初始化模板形参,一旦编译器确定了实际的模板实参类型就称他实例化了函数模板的一个实例。比如swap的模板函数形式为template void swap(T& a, T& b){},当调用这样的模板函数时类型T就会被被调用时的类型所代替,比如swap(a,b)其中a和b是int型,这时模板函数swap中的形参T就会被int所代替,模板函数就变为swap(int &a, int &b)。而当swap(c,d)其中c和d是double类型时,模板函数会被替换为swap(double
&a, double &b),这样就实现了函数的实现与类型无关的代码。2、注意:对于函数模板而言不存在h(int,int)这样的调用,不能在函数调用的参数中指定模板形参的类型,对函数模板的调用应使用实参推演来进行,即只能进行h(2,3)这样的调用,或者int a, b; h(a,b)。二、类模板通式1.类模板的通用形式为:template class 类名{},类模板和函数模板都是以template开始后接模板形参列表组成,模板形参不能为空,一但声明了类模板就可以用类模板的形参名声明类中的成员变量和成员函数,即可以在类中使用内置类型的地方都可以使用模板形参名来声明。比如template class A{public:
T a; T b; T hy(T c, T &d);};在类A中声明了两个类型为T的成员变量a和b,还声明了一个反回类型为T带两个类型为T的函数hy。2.类模板对象的创建:比有一个模板类A,则使用类模板创建对象的方法为A m;在类A后面跟上一个<>尖括号并在里面填上相应的类型,这样的话类A中凡是用到模板形参的地方都会被int所代替。当类模板有两个模板形参时创建对象的方法为A m;类型之间用逗号隔开。3、对于类模板,模板形参的类型必须在类名后的尖括号中明确指定。比如A<2> m;用这种方法把模板形参设置为int是错误的,类模板形参不存在实参推演的问题。也就是说不能把整型值2推演为int型传递给模板形参。要把类模板形参调置为int型必须这样指定A m。
4、在类模板外部定义成员函数的方法为:template<模板形参列表> 函数反回类型类名<模板形参名>::函数名(参数列表){函数体},比如有两个模板形参T1,T2的类A中含有一个void h()函数,则定义该函数的语法为:template
T1,class T2> void A::h(){}。注意当在类外面定义类的成员时template后面的模板形参应与要定义的类的模板形参一致。5、再次提醒注意:模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。三、模板的形数有三种类型的模板形参:类型形参,非类型形参和模板形参。1、类型形参1.1 类型模板形参:类型形参由关见字class或typename后接说明符构成,如template void h(T a){};其中T就是一个类型形参,类型形参的名字由用户自已确定。模板形参表示的是一个未知的类型。模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型说明符或类类型说明符的使用方式完全相同,即可以用于指定反回类型,变量声明等。1.2 不能为同一个模板类型形参指定两种不同的类型,比如templatevoid h(T a, T b){},语句调用h(2, 3.2)将出错,因为该语句给同一模板形参T指定了两种类型,第一个实参2把模板形参T指定为int,而第二个实参3.2把模板形参指定为double,两种类型的形参不一致,会出错。2、非类型形参2.1 非类型模板形参:模板的非类型形参也就是内置类型形参,如template class B{};其中int a就是非类型的模板形参。2.2 非类型形参在模板定义的内部是常量值,也就是说非类型形参在模板的内部是常量。2.3 非模板类型的形参只能是整型,指针和引用,像double,String, String **这样的类型是不允许的。但是double &,double
*,对象的引用或指针是正确的。2.4 调用非类型模板形参的实参必须是一个常量表达式,即他必须能在编译时计算出结果。2.5 注意:任何局部对象,局部变量,局部对象的地址,局部变量的地址都不是一个常量表达式,都不能用作非类型模板形参的实参。全局指针类型,全局变量,全局对象也不是一个常量表达式,不能用作非类型模板形参的实参。2.6 全局变量的地址或引用,全局对象的地址或引用const类型变量是常量表达式,可以用作非类型模板形参的实参。2.7 sizeof表达式的结果是一个常量表达式,也能用作非类型模板形参的实参。2.8 当模板的形参是整型时调用该模板时的实参必须是整型的,且在编译期间是常量,比如template class
A{};如果有int b,这时A m;将出错,因为b不是常量,如果const int b,这时A m;就是正确的,因为这时b是常量。2.9非类型形参一般不应用于函数模板中,比如有函数模板template voidh(T b){},若使用h(2)调用会出现无法为非类型形参a推演出参数的错误,对这种模板函数可以用显示模板实参来解决,如用h(2)这样就把非类型形参a设置为整数3。显示模板实参在后面介绍。2.9 非类型模板形参的形参和实参间所允许的转换1、允许从数组到指针,从函数到指针的转换。如:template class A{}; int b[1]; A m;即数组到指针的转换2、const修饰符的转换。如:template class A{}; int b; A<&b> m; 即从int *到const int *的转换。3、提升转换。如:template class A{}; const short b=2; A m; 即从short到int的提升转换4、整值转换。如:template class A{}; A<3> m; 即从int到unsigned int的转换。5、常规转换。四、类模板的默认模板类型形参1、可以为类模板的类型形参提供默认值,但不能为函数模板的类型形参提供默认值。函数模板和类模板都可以为模板的非类型形参提供默认值。2、类模板的类型形参默认值形式为:template class A{};为第二个模板类型形参T2提供int型的默认值3、类模板类型形参默认值和函数的默认参数一样,如果有多个类型形参则从第一个形参设定了默认值之后的所有模板形参都要设定默认值,比如templateclass A{};就是错误的,因为T1给出了默认值,而T2没有设定。4、在类模板的外部定义类中的成员时template后的形参表应省略默认的形参类型。比如template
T2=int>class A{public: void h();}; 定义方法为template void A::h(){}。类模板非类型形参示例//模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。//类模板的定义templateclassA{public:T g(T a, T b); A();}; //定义带有一个类模板类型形参T的类AtemplateclassB{public:voidg();}; //定义带有两个类模板类型形参T1,T2的类B//定义类模板的默认类型形参,默认类型形参不适合于函数模板。template classD{public: voidg();}; //定义带默认类型形参的类模板。这里把T2默认设置为int型。//templateclass E{}; //错误,为T1设了默认类型形参则T1后面的所有形参都必须设置认默值。
//以下为非类型形参的定义//非类型形参只能是整型,指针和引用,像double,String, String **这样的类型是不允许的。但是double &,double *对象的引用或指针是正确的。template classCi{public:voidg();}; //定义模板的非类型形参,形参为整型templateclassCip{public:voidg();};
template* m> classCc{public:voidg();}; //定义模板的模板类型形参,形参为int型的类A的对象的指针。templateclassCd{public:voidg();}; //定义模板的非类型形参,形参为double类型的引用。classE{};template classCe{};//非类型模板形参为对象的引用。//以下非类型形参的声明是错误的。//templateclass Cc{}; //错误,对象不能做为非类型形参,非类型模板形参的类型只能是对象的引用或指针。//templateclass Cc{}; //错误,非类型模板的形参不能是double类型,可以是double的引用。//template m>class Cc{}; //错误,非类型模板的形参不能是对象,必须是对象的引用或指针。这条规则对于模板型参也不例外。//在类模板外部定义各种类成员的方法,//typeid(变量名).name()的作用是提取变量名的类型,如int a,则cout< A::A(){cout<<"class A goucao"< T A::g(T a,T b){cout<<"class A g(T a,T b)"< voidB::g(){cout<<"class g f()"< voidCi::g(){cout<<"class Ci g()"< voidCip::g(){cout<<"class Cip g()"<
//在类外部定义类的成员时,template后的模板形参应与要定义的类的模板形参一致template *m> voidCc::g(){cout<<"class Cc g()"< voidCd::g(){cout<<"class Cd g()"< voidD::g(){cout<<"class D g()"< void D::g(){cout<<"class D k()"< mw;A *pec=&mw;double*pe=&ed;E me;//main函数开始intmain(){// templatevoid h(){} //错误,模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行。//A<2> m; //错误,对类模板不存在实参推演问题,类模板必须在尖括号中明确指出其类型。//类模板调用实例A ma; //输出"class A goucao int"创建int型的类模板A的对象ma。B mb; mb.g(); //输出"classB g() int int"创建类模板B的对象mb,并把类型形参T1和T2设计为int//非类型形参的调用//调用非类型模板形参的实参必须是一个常量表达式,即他必须能在编译时计算出结果。任何局部对象,局部变量,局部对象的地址,局部变量的地址都不是一个常量表达式,都不能用作非类型模板形参的实参。全局指针类型,全局变量,全局对象也不是一个常量表达式,不能用作非类型模板形参的实参。//全局变量的地址或引用,全局对象的地址或引用const类型变量是常量表达式,可以用作非类型模板形参的实参。//调用整型int型非类型形参的方法为名为Ci,声明形式为template class CiCi mci1; mci1.g(); //正确,因为a2在这里是const型的常量。输出"class Ci g() int"//Ci mci; //错误,int型变量a是局部变量,不是一个常量表达式。//Ci mci; //错误,全局int型变量e也不是一个常量表达式。//调用int&型非类型形参的方法类名为Cip,声明形式为templateclass CipCip mcip; //正确,对全局变量的引用或地址是常量表达式。//Cip mcip1; //错误,局部变量的引用或地址不是常量表达式。//调用double*类型的非类形形参类名为Cd,声明形式为templateclass CdCd mcd; //正确,全局变量的引用或地址是常量表达式。//Cd mcd1; //错误,全局变量指针不是常量表达式。//double dd=aNGMIITbULcdefbbHIJKbgMIhh错误,局部变量的地址不是常量表达式,不能用作非类型形参的实参//Cd mcd; //错误,非类型形参虽允许一些转换,但这个转换不能实现。//调用模板类型形参对象A *的方法类名为Cc,声名形式为template* m> class CcCc mcc; mcc.g(); //正确,全局对象的地址或者引用是常量表达式//Cc mcc; //错误,局部变量的地址或引用不是常量表达式。//Cc mcc2; //错误,全局对象的指针不是常量表达式。
//调用非类型形参E&对象的引用的方法类名为Ce。声明形式为template class CeE me1;//Ce mce1; //错误,局部对象不是常量表达式Ce mce; //正确,全局对象的指针或引用是常量表达式。//非类型形参的转换示例,类名为Ci//非类型形参允许从数组到指针,从函数到指针的转换,const修饰符的转换,提升转换,整值转换,常规转换。constshorts=„MCi mci…MI†//正确,虽然short型和int不完全匹配,但这里可以将short型转换为int型五、模板的实例化,区别声明,定义,实例化的概恋1、声明就是让编译器知道有这么一个函数或者类,比如void h(); class A;就声明了一个无参函数h和一个类A。定义就是对函数或者类的实现。比如void h(){},class A{int a;}; 就定义了一个什么也不做的函数h和有一个变量a的类A。2、声明和定义都不会创建实例,只有在使用该函数或者类的时候才会创建一个实例,比如在main函数中调用函数h,创建类A的对象时都会创建函数h和类A的实例,如果函数h是外部函数则如果在main函数中没有调用函数h,就不会创建函数h的实例。3、模板的声明,定义,实例化:声明一个模板的形式为template void h(T a); 或template class A;这就声明了一个模板函数h和一个模板类A。注意后面有个分号。模板的定义和函数或类的定义相同。模板的实例化发生在调用该模板函数和模板类时,比如h(2)或A m;就创建了一个int型的h函数实例和类A的int实例版本。当模板被实例化之后就会创建该模板的一个实例,在下次调用到相同的模板实例时就不会生成新的实例,而会调用以前创建的那个实例。比如有模板函数template void h(T a){},则有调用h(2)会生成一个int型的模板函数实例,当第二次调用如h(44)时会使用以前生成的int型模板函数实例而不会创建新的实例。4、当我们只是声明一个类模板的指针或引用时就没必要知道类模板的定义,也就是说创建类模板的指针或引用时不会创建类模板的实例,只有指针被解引用或者该问类中的成员时才需要知道类模板的定义,才会实例化该类模板。比如class
A ; A *m; A &n;都不需要知道该类的定义,也不会实例化该类。但是m->a或n->a时就会需要知道该类的定义了,因为这里指针访问类中的成员,须要类的定义,这时也要创建一个类的实例。六、模板类型形参与实参间所允许的转换或模板实参推演1、模板实参推演:当函数模板被调用时,对函数实参类型的检查决定了模板实参的类型和值这个过程叫做模板实参推演。比如template void h(T a){};h(1);h(2.2),第一个调用因为实参1是int型的,所以模板形参T被推演为int型,因此函数体中的所有T被替换为int。而第二个调用中double类型的实数3.2决定了T的类型为double。2、在模板被实例化后就会生成一个新的实例,但这个新生成的实例不存在类型转换。比如有函数模板template
void h(T a){}。int a=2; short b=3; 第一次调用h(a)生成一个int型的实例版本,但是当用h(b)调用时不会使用上次生成的int实例把short转换为int,而是会另外生成一个新的short型的实例。3、在模板实参推演的过程中有时类型并不会完全匹配,这时编译器允许以下几种实参到模板形参的转换,这些转换不会生成新的实例。3.1、数组到指针的转换或函数到指针的转换:比如template void h(T * a){},int b[3]={1,2,3};h(b);这时数组b和类型T *不是完全匹配,但允许从数组到指针的转换因此数组b被转换成int *,而类型形参T被转换成int,也就是说函数体中的T被替换成int。3.2、限制修饰符转换:即把const或volatile限定符加到指针上。比如template void h(const T* a){},int b=3;
h(&b);虽然实参&b与形参const T*不完全匹配,但因为允许限制修饰符的转换,结果就把&b转换成const int *。而类形型参T被转换成int。如果模板形参是非const类型,则无论实参是const类型还是非const类型调用都不会产生新的实例。3.3、到一个基类的转换(该基类根据一个类模板实例化而来):比如tessmplateclass A{}; template
class B:public A{}; template void h(A& m){},在main函数中有B n; h(n);函数调用的子类对象n与函数的形参A不完全匹配,但允许到一个基类的转换。在这里转换的顺序为,首先把子类对象n转换为基类对象A,然后再用A去匹配函数的形参A&,所以最后T2被转换为int,也就是说函数体中的T将被替换为int。4、再次提醒:对于函数模板而言不存在h(int,int)这样的调用,不能在函数调用的参数中指定模板形参的类型,对函数模板的调用应使用实参推演来进行,即只能进行h(2,3)这样的调用,或者int a, b; h(a,b)。模板实参推演示例//模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。//函数模板的定义templatevoidh(T a){cout<<"hansu h()"<voidk(T a,T b){T c;cout<<"hansu k()"< voidf(T1 a, T2 b){cout<<"hansu f()"< voidg(constT* a){T b;cout<<"hansu g()"<
//template void g(){} //错误,默认模板类型形参不能用于函数模板,只能用于类模板上。//main函数开始intmain(){ // templatevoid h(){} //错误,模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行。
//函数模板实参推演示例。// h(int); //错误,对于函数模板而言不存在h(int,int)这样的调用,不能在函数调用的参数中指定模板形参的类型,对函数模板的调用应使用实参推演来进行,即只能进行h(2,©Q这样的调用,或者int a, b; h(a,b)。//h函数形式为:templatevoid h(T a)h(2);//输出"hansu h() int"使用函数模板推演,在这里数值2为int型,所以把类型形参T推演为int型。h(2.¬QMI//输出"hansu h() double",因为2.®为double型,所以将函数模板的类型形参推演为double型//k函数形式为:templatevoid k(T a,T b)k(2,°QMI//输出"hansu k() int"//k(2,±N¬QMIhh错误,模板形参T的类型不明确,因为k()函数第一个参数类型为int,第二个为double型,两个形参类型不一致。//f函数的形式为:template void f(T1 a, T2 b)f(¶e…N¬QMI//输出"hansu f() int,double",这里不存在模板形参推演错误的问题,因为模板函数有两个类型形参T1和T2。在这里将T1推演为int,将T2推演为double。inta=¼Mdoubleb=½Mf(a,b); //输出同上,这里用变量名实现推板实参的推演。//模板函数推演允许的转换示例,g函数的形式为template void g(const T* a)inta1ÁÂÃÄÅge†Mg(a1); //输出"hansu g() int",数组的地址和形参const T*不完全匹配,所以将a1的地址T &转换为const T*,而a1是int型的,所以最后T推演为int。g(&b); //输出"hansu g() double",这里和上面的一样,只是把类型T转换为double型。h(&b); }//输出"hansu h() double *"这里把模参类型T推演为double *类型。七、显示实例化,显示模板实参,显示具体化,模板特化,模板函数重载7.1 函数模板的显示实例化1、隐式实例化:比如有模板函数template void h(T a){}。h(2)这时h函数的调用就是隐式实例化,既参数T的类型是隐式确定的。2、函数模板显示实例化声明:其语法是:template 函数反回类型函数名<实例化的类型> (函数形参表); 注意这是声明语句,要以分号结束。例如:template void h (int a);这样就创建了一个h函数的int实例。再如有模板函数template T h( T a){},注意这里h函数的反回类型为T,显示实例化的方法为template int h(int a); 把h模板函数实例化为int型。3、类模板的显示实例化:和函数模板的显示实例化一样都是以template开始。比如template class A;将类A显示实例化为两个int型的类模板。这里要注意显示实例化后面不能有对象名,且以分号结束。4、显示实例化可以让程序员控制模板实例化发生的时间。5、对于给定的函数模板实例,显示实例化声明在一个文件中只能出现一次。6、在显示实例化声明所在的文件中,函数模板的定义必须给出,如果定义不可见,就会发生错误。7、注意:不能在局部范围类显示实例化模板,实例化模板应放在全局范围内,即不能在main函数等局部范围中实例化模板。因为模板的声明或定义不能在局部范围或函数内进行。7.2 显示模板实参1、显示模板实参:适用于函数模板,即在调用函数时显示指定要调用的时参的类型。2、格式:显示模板实参的格式为在调用模板函数的时候在函数名后用<>尖括号括住要显示表示的类型,比如有模板函数template void h(T a, T b){}。则h(2, 3.2)就把模板形参T显示实例化为double类型。3、显示模板实参用于同一个模板形参的类型不一致的情况。比如template void h(T a, T b){},则h(2, 3.2)的调用会出错,因为两个实参类型不一致,第一个为int型,第二个为double型。而用h(2, 3.2)就是正确的,虽然两个模板形参的类型不一致但这里把模板形参显示实例化为double类型,这样的话就允许进行标准的隐式类型转换,即这里把第一个int参数转换为double类型的参数。4、显示模板实参用法二:用于函数模板的反回类型中。例如有模板函数template T1 h(T2 a, T3
b){},则语句int a=h(2,3)或h(2,4)就会出现模板形参T1无法推导的情况。而语句int h(2,3)也会出错。用显示模板实参就参轻松解决这个问题,比如h(2,3)即把模板形参T1实例化为int型,T2和T3也实例化为int型。5、显示模板实参用法三:应用于模板函数的参数中没有出现模板形参的情况。比如templatevoid h(){}如果在main函数中直接调用h函数如h()就会出现无法推演类型形参T的类型的错误,这时用显示模板实参就不会出现这种错误,调用方法为h(),把h函数的模板形参实例化为int型,从而避免这种错误。6、显示模板实参用法四:用于函数模板的非类型形参。比如template void h(T b){},而调用h(3)将出错,因为这个调用无法为非类型形参推演出正确的参数。这时正确调用这个函数模板的方法为h(4),首先把函数模板的类型形参T推演为int型,然后把函数模板的非类型形参int a用数值3来推演,把变量a设置为3,然后再把4传递给函数的形参b,把b设置为4。注意,因为int a是非类型形参,所以调用非类型形参的实参应是编译时常量表达式,不然就会出错。6、在使用显示模板实参时,我们只能省略掉尾部的实参。比如template T1 h(T2 a, T3 b){}在显示实例化时h(3, 3.4)省略了最后两个模板实参T2和T3,T2和T3由调用时的实参3和3.4隐式确定为int型和double型,而T1被显示确定为int型。h<2,3.4>是错误的,只能省略尾部的实参。7、显示模板实参最好用在存在二义性或模板实参推演不能进行的情况下。//函数显示模板实参示例适用于函数模板templatevoidg1(T a, T b){cout<<"hansu g1()"<
{T1c=a;cout<<"hansug2()"< voidgÛPÔgIØQÅKÜÝdUU"hansu gÞPQß< voidgàPÔgIÖedoublec){cout<<"hansu gáPQß< classA{public:voidg();};//模板显示实例化示例。//因为模板的声明或定义不能在局部范围或函数内进行。所以模板实例化都应在全局范围内进行。templatevoidg1(doublea,doubleb); //把函数模板显示实例化为int型。templateclassA; //显示实例化类模板,注意后面没有对象名,也没有{}大括号。//template class A{}; //错误,显示实例化类模板后面不能有大括号{}。//template class A m; //错误,显示实例化类模板后面不能有对象名。//main函数开始intmain(){//显示模板实参示例。显示模板实参适合于函数模板//1、显示模板实参用于同一个模板形参的类型不一致的情况。函数g1形式为templatevoid g1(T a, T b)g1(2,ïNÂQMI//输出"hansu g1() int"两个实参类型不一致,第一个为int第二个为double。但这里用显示模板实参把类型形参T指定为double,所以第一个int型的实参数值2被转换为double类型。//g1(2,öNÂQMIhh错误,这里没有用显式模板实参。所以两个实参类型不一致。//2、用于函数模板的反回类型中。函数g2形式为template T1 g2(T2 a,T3 b)//g2(2,øQMIhh错误,无法推演类型形参T1。//int g2(2,ûQMIhh错误,不能以这种方法试图推导类型形参T1为int型。//int a=g2(2, !"#$$错误,以这种方式试图推演出T1的类型为int也是错误的。g2(2,-!"#//正确,将T1,T2,T/显示指定为int型。输出"hansu g2() intintint"//3、应用于模板函数的参数中没有出现模板形参的情况其中包括省略的用法,函数g3的形式为template void g3(T1 a)//g567!"#$$错误,无法为函数模板的类型形参T2推演出正确的类型//gC67D !"#$$错误,岂图以这种方式为T2指定int型是错误的,因为函数只有一个参数。//gMNDOPQR67!"#$$错误,这里起图用数值2来推演出T1为int型,而省略掉第一个的显示模板实参,这种方法是错误的。在用显示模板实参时,只能省略掉尾部的实参。//gfNOPQR67!"#$$错误,虽然用了显示模板实参方法,省略掉了尾部的实参,但该方法只是把T1指定为int型,仍然无法为T2推演正确的类型。gnNint,int>(2);//正确,显示指定T1和T2的类型都为int型。//4、用于函数模板的非类型形参。g4函数的形式为template voidg4(T1 b,double c)//gp6 D q7!"#$$错误,虽然指定了两个参数,但是这里仍然无法为函数模板的非类型形参int a推演出正确的实参。因为第二个函数参数x.2是传递给函数的参数double c的,而不是函数模板的非类型形参int a。//g~6 D7!"#$$错误,起图以整型值把实参传递给函数模板的非类型形参是不行的,这里数值2会传递给函数形参double c并把int型转换为double型。所以非类型形参int a仍然无实参。//int d=1; g†NOPQD‡R6 D q2); //错误,调用方法正确,但对于非类型形参要求实参是一个常量表达式,而局部变量c是非常量表达式,不能做为非类型形参的实参,所以错误。g•Nint,1>(2,‘q7!"#//正确,用显示模板实参,把函数模板的类型形参T1设为int型,把数值1传给非类型形参int a,并把a设为1,把数值2传给函数的第一个形参T1 b并把b设为2,数值•.2传给函数的第二个形参double c并把c设为—.2。constintd=1; g˜Nint,d>(™D q7!"#š//正确,这里变量d是const常量,能作为非类型形参的实参,这里参数的传递方法同上面的语句。7.3 显示具体化(模板特化,模板说明)和函数模板的重载1、具体化或特化或模板说明指的是一个意思,就是把模板特殊化,比如有模板templatevoid h(T a){},这个模板适用于所有类型,但是有些特殊类型不需要与这个模板相同的操作或者定义,比如int型的h实现的功能和这个模板的功能不一样,这样的话我们就要重定义一个h模板函数的int版本,即特化版本。特化函数模板:2、显示特化格式为:template<> 反回类型函数名<要特化的类型>(参数列表) {函数体},显示特化以template<>开头,表明要显示特化一个模板,在函数名后<>用尖括号括住要特化的类型版本。比如template void h(T a){},其int类型的特化版本为template<> void h(int a){},当出现int类型的调用时就会调用这个特化版本,而不会调用通用的模板,比如h(2),就会调用int类型的特化版本。3、如果可以从实参中推演出模板的形参,则可以省略掉显示模板实参的部分。比如:template<> void h(int a){}。注意函数h后面没有<>符号,即显示模板实参部分。4、对于反回类型为模板形参时,调用该函数的特化版本必须要用显示模板实参调用,如果不这样的话就会出现其中一个形参无法推演的情况。如template T1 h(T2 a,T3 b){},有几种特化情况:情况一:template<> int h(int a, in b){}该情况下把T1,T2,T3的类型推演为int型。在主函数中的调用方式应为h(2,3)。情况二:template<> int h(int a, int b){},这里把T2,T2推演为int型,而T1为int型,但在调用时必须用显示模板实参调用,且在<>尖括号内必须指定为int型,不然就会调用到通用函数模板,如h(2,3)就会调用函数模板的特化版本,而h(2,3)调用会出错。h(2,3)调用则会调用到通用的函数模板版本。这几种情况的特化版本是错误的,如template<> T1 h(int a,int b){},这种情况下T1会成为不能识别的名字,因而出现错误,template<> int h(int a,int b){}在这种情况下反回类型为int型,把T1确定为int而尖括号内又把T1确定为double型,这样就出现了冲突。5、具有相同名字和相同数量反回类型的非模板函数(即普通函数),也是函数模板特化的一种情况,这种情况将在后面参
数匹配问题时讲解。函数模板重载1、函数模板可以重载,注意类模板不存在重载问题,也就是说出现这两条语句时templateclass A{};
templateclass A{};将出错。2、模板函数重载的形式为:template void h(T a, int b){}。Templatevoid h(T a, double b){}等。3、重载模板函数要注意二义性问题,比如template void h(T a, int b){}和templatevoid h(T a, T b){}这两个版本就存在二义性问题,当出现语句h(2,3)时就不知道调用哪个才正确,在程序中应避免这种情况出现。4、重载函数模板的第二个二义性问题是templatevoid h(T a, T b){}与templatevoid h(T1 a,T2
b){},当出现h(2,4)这样的调用时就会出现二义性。解决这个问题的方法是使用显示模板实参,比如要调用第一个h函数,可以使用语法h(2,3),调用第二个h函数的方法为h(2,3)。5、函数模板的特化也可以理解为函数模板重载的一种形式。只是特化以template<>开始。6、重载的特殊情况:比如template void h(T1 a, T2 b){},还有个版本如templatevoid h(T1 a,
int b){}这里两个函数具有两同的名字和相同的形参数量,但形参的类型不同,可以认为第二个版本是第一个版本的重载版本。7、函数模板的重载和特化很容易混晓,因为特化很像是一个函数的重载版本,只是开头以template<>开始而已。特化类模板:6、特化整个类模板:比如有template class A{};其特化形式为template<> class A{};特化形式以template<>开始,这和模板函数的形式相同,在类名A后跟上要特化的类型。7、在类特化的外部定义成员的方法:比如template class A{public: void h();};类A特化为template<> class
A{public: void h();};在类外定义特化的类的成员函数h的方法为:void A::h(){}。在外部定义类特化的成员时应省略掉template<>。8、类的特化版本应与类模板版本有相同的成员定义,如果不相同的话那么当类特化的对象访问到类模板的成员时就会出错。因为当调用类的特化版本创建实例时创建的是特化版本的实例,不会创建类模板的实例,特化版本如果和类的模板版本的成员不一样就有可能出现这种错误。比如:模板类A中有成员函数h()和f(),而特化的类A中没有定义成员函数f(),这时如果有一个特化的类的对象访问到模板类中的函数f()时就会出错,因为在特化类的实例中找不到这个成员。9、类模板的部分特化:比如有类模板template class A{};则部分特化的格式为template class
A{};将模板形参T2特化为int型,T1保持不变。部分特化以template开始,在<>中的模板形参是不用特化的模板形参,在类名A后面跟上要特化的类型。如果要特化第一个模板形参T1,则格式为template class
A{};部分特化的另一用法是template class A{};将模板形参T2也特化为模板形参T1的类型。10、在类部分特化的外面定义类成员的方法:比如有部分特化类template class A{public: void h();};则在类外定义的形式为template void A::h(){}。注意当在类外面定义类的成员时template后面的模板形参应与要定义的类的模板形参一样,这里就与部分特化的类A的一样template。其他说明:11、可以对模板的特化版本只进行声明,而不定义。比如template<> void h(int a);注意,声明时后面有个分号。12、在调用模板实例之前必须要先对特化的模板进行声明或定义。一个程序不允许同一模板实参集的同一模板既有显示特化又有实例化。比如有模板template void h(T a){}在h(2)之前没有声明该模板的int型特化版本,而是在调用该模板后定义该模板的int型特化版本,这时程序不会调用该模板的特化版本,而是调用该模板产生一个新的实例。这里就有一个问题,到底是调用由h(2)产生的实例版本呢还是调用程序中的特化版本。13、注意:因为模板的声明或定义不能在局部范围或函数内进行。所以特化类模板或函数模板都应在全局范围内进行。14、在特化版本中模板的类型形参是不可见的。比如template<> void h(int a,int b){T1 a;}就会出现错误,在这里模板的类型形参T1在函数模板的特化版本中是不可见的,所以在这里T1是未知的标识符,是错误的。//函数模板特化和类模板特化示例//定义函数g1,g2和类Atemplate voidg1(T1 a,T2 b){cout<<"g1洞"< voidg1(inta,intb){cout<<"g1一"< voidg1(inta,doubleb){cout<<"g1二"<
template<> voidg1(doublea,doubleb){cout<<"g1三"< void g1(double a,int b){cout<<"g·一"< intg2(inta,intb){intc=1;cout<<"g2一"< doubleg2(inta,intb){intc=1;cout<<"g2二"< int g2(int a, T2 b){int c=1;cout<<"g2三"<
//下式错误,函数反回类型和尖括号中的double类型不同,发生冲突。//template<> int g2(int a,int b){int c=1;cout<<"two"<
//下式错误,函数模板的类型形参在特化版本中是不可见的,也就是说这里的会把类型形参T1理解为未声明的标识符//template<> T1 g2(int a,int b){int c=1;cout<<"two"<
//类模板的特化和部分特化template<> classA{public:voidh();}; //特化整个类模板的格式,注意类名后的尖括号中必须指定所有的类模板的类型形参。//template<> class A{}; //错误,在特化的类名后的尖括号中指定的类模板类型形参的数量不够。要想只特化其中一个类模板的类型形参,就要使用类模板的部分特化。template::h(){cout<<"class A tehua"< void A::h(){} //错误,在类模板的特化版本外面定义类模板的成员时应省略掉template<>template void g1(int a,int b){cout<<"g1一"< void g1(int a,double b){cout<<"g1二"< void g1(double a,double b){cout<<"g1三"<(TO&HU-J(//输出"g1三",这里用显示模板实参把第一个实参指定为double型,这样g1的两个实参都是double型,所以将调用g1的第三个特化版本。// g2(bO&-J(cc错误,在调用反回类型为类型形参的时候必须用显示模板实参的形式为反回类型的形参显示指定类型。在这里就会出现无法为T1确定类型的情况。g2(2,u-J((//正确,把g2的类型形参T1设显示指定为int,调用g2的第一个特化版本。template<> int g2(int a,int b){int
c=1;cout<<"g2一"<(2,x-J//正确,把g2的类型形参T1设显示指定为double,调用g2的第二个特化版本。template<> double g2(int a,int b){int
c=1;cout<<"g2二"<(2,|-J((//正确,把g2的类型形参T1设显示指定为char,对于char版本的g2函数没有特化版本,因此调用g2的通用版本。template m1; m1.h();//正确,调用类模板的特化版本。A m; m.h(); //正确,调用类模板的部分特化版本。//A m2; //错误,类模板有三个类型形参,这里只提供了两个,数量不够,错误。A m•J(–&H+,-J(//调用类A的部分特化版本。A m˜J(–PH+,-J™//调用类A的普通版本,在这里没有A型的特化或者部分特化版本可用。八、匹配问题即函数的重载解析问题1、当模板函数即有重载版本又有非模板函数的重载版本时就会出现参数的匹配问题,模板函数最后会选择最佳匹配的函数调用。过程为先进行参数匹配,参后从匹配好的函数列表中选择完全匹配的函数,最后再在完全匹配的函数中选择最佳匹配的函数。2、参数的匹配过程如下。a、创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数(其中模板函数推演成功才会加入到候选函数列表中)。b、使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。c、确定是否有最佳的可行函数。如果有则使用之,否则出错。3、确定最佳可行函数的步骤,首先应确定哪些是完全匹配的,完全匹配从最佳到最差的顺序为a、完全匹配,但普通函数优于模板函数及模板函数的特化版本b、提升转换(如:char和short转换为int,及float转换为double)c、标准转换(如:int转换为char,及long转换为double)d、用户定义的转换,如类声明中定义的转换。注意,完全匹配允许一些无关紧要的转换,这些无关紧要的转换如下。其中包括const type到const type&的转换形参实参形参实参形参实参形参实参typetype &type[]type(参数列表)type(*)(参数列表)type &typetype *typetypetype *const typevolatile typeconst type *type *volatile type*比如:有函数void h(int a), void h(int &a),其中int b=3;则调用h(b)将出错。因为两个函数都是完全匹配且是最佳匹配的。完全匹配示例:比如有如下函数定义:#1 void may(int a); #2 float may(float a, float b=3); #3 void may(char a); #4 char *may(const char* a);
#5 char may(const char & a); #6 templatevoid may(const T &a); #7 templatevoid may(T * a);
则如调用may(‘b’); 对这个调用#4和#7不可行,因为整数不能被隐式转换为指针类型。#1调用优先于#2,因为char到int是提升转换,而char到float是标准转换,提升转换优先于标准转换,所以#1优先于#2。#3,#5,#6都优先于#1和#2,因为#3,#5,#6都是完全匹配,其中#3和#5优先于#6,因为#6是模板函数。其中#3和#5都是完全匹配,且是最佳的,这样就出现了二义性问题的错误。注意#5是const char &类型的形参,在这里应用了完全匹配的无关紧要的转换,type &形参到type实参的转换和const type形参到type实参的转换。4、但是完全匹配并不是最佳匹配,最佳匹配的原则如下a、如果多个函数都是完全匹配,则非const数据的指针或引用形参优先于const的指针和引用的参数匹配。这条规则只适合于指针或引用参数,当参数是非指针或引用时将出现二义性。比如void h(int& a)和void h(const int & a)其中int
b=2;当有调用h(b)时,h(int & a)优先于h(const int& a)因为在这里变量b没有被声明为const,而第一个函数又是非const指针函数,所以第一个函数优先于第二个函数。但对于h(2)这样的调用,则第二个函数将优先于第一个函数的调用,因为这里数值2本身就是const常量,所以第二个函数更优先。而h(int a)和h(const int a)当出现h(b)时就会出现二义性问题。b、注意这四个函数的调用1# h(int),2# h(const int),3# h(int &),4# h(const int&),情况1:如果只有#1和#2则调用h(2)和h(b)的调用都会出错。情况2:如果只有1#和3#则h(2)将调用1#,而h(b)调用会出错。情况3:如果只有1#和4#则h(3)和h(b)都将出错。情况4:如果只有2#和3#则h(2)将调用2#,而h(b)将出错。情况5:如果只有2#和4#,则h(3)和h(b)都将出错。对于以上五种情况h(b)的调用都是错误的,而对h(3)的调用中只要引用参数不带const则不会出错。具体原因还不清楚,有待考证。可能与按值传递有关。情况6:如果只有3#和4#,则h(3)将调用4#,因为数值3是const常量;h(b)将调用3#,因为就量b本身不是const常量。c、普通函数优先于模板函数及特化的模板函数。d、当完全匹配的函数都是模板函数时,则更具体模板函数优先。更具体指的是编译器推断使用哪种类型时执行的转换更少。比如:templatevoid h(T* a){}与templatevoid h(T a){} ,其中int b=3;调用h(&b)将调用第1个模板函数,因为第一个模板函数被转换为h(int *)传送给函数,而第二个模板函数被转换为h(int *)传送给函数,第二个转换把模板形参T具体化为int型的指针,而第一个模板函数的形参T被转换为int型,因此第一个转换更具体,需要的转换更少。注意,如果把T* a换成T& a则会出现类似于b的情况。//函数模板的参数的匹配问题示例template voidg1(T1 a,T2 b){cout<<"g1一"< voidg1(inta,intb){cout<<"g1二"< voidg2(constT& a){cout<<"g2®¯¤< voidg2(constT* a){cout<<"g2±²¤<
voidg¶,floata){cout<<"g·£I¤<voidg¸,…¹(ˆ-./01455"gº一"< voidg»,…(ˆ-./01455"g¼二"<
//完全匹配的模板函数的调用intc=l_`mn%op,_}//输出"gq一",因为gs的第一个模板函数更具体。九、类模板中的模板成员(模板函数,模板类)和静态成员1、类模板中的模板函数和模板类的声明:与普通模板的声明方式相同,即都是以template开始2、在类模板外定义类模板中的模板成员的方法:比如template class A{public:template class B;
template void g(T3 a);};则在类模板外定义模板成员的方法为,template template class
A::B{};定义模板函数的方法为:template template void A::g(T3 a){}其中第一个template指明外围类的模板形参,第二个template指定模板成员的模板形参,而作用域解析运算符指明是来自哪个类的成员。3、实例化类模板的模板成员函数:比如上例中要实例化函数g()则方法为, A m; m.g(2);这里外围类A的模板形参由尖括号中指出,而类中的模板函数的参数由整型值2推演出为int型。4、创建类模板中的模板成员类的对象的方法:比如上例中要创建模板成员类B的方法为,A::B m1;A::Bm2; A::B m3;在类模板成员B的前面要使用作用域解析运算符以指定来自哪个外围类,并且在尖括号中要指定创建哪个外围类的实例的对象。这里说明在类模板中定义模板类成员时就意味意该外围模板类的一个实例比如int实例将包含有多个模板成员类的实例。比如这里类A的int实例就有两个模板成员类B的int和double两个实例版本。5、要访问类模板中的模板成员类的成员遵守嵌套类的规则,因为类模板中的模板成员类就是一个嵌套类。即外围类和嵌套类中的成员是相互独立的,要访问其中的成员只能通过嵌套类的指针,引用或对象的方式来访问。具体情况见嵌套类部分。6、类模板中的静态成员是类模板的所有实例所共享的。//类模板中的模板成员示例template classA{public:inta,b; staticinte;templatetemplate::B{public:voidgb(){cout<<"moban class B gb()"<
//在类模板外面定义类模板的模板成员类的方法templatetemplate::g(T†`<{••`',‡p;ˆ+‰‰"moban g()"< intA::e=Š_`//在类模板外面定义静态成员的方法。intmain(){A ma;ma.g(2,‘,_``//创建模板类中模板成员函数的方法,在这里模板类A的模板形参被设为int,而模板成员函数的模板形参则由两个int型的整数推演为int型。ma.e=1; A::e=2; //把类模板A的int,int型实例的静态成员设为。cout<<"ma.e="<
A ma1;cout<<"ma.e="<::e<::e<::B mb; //声明模板类中模板成员类的方法。();//调用嵌套类B的成员函数//mb.g(); //错误,函数g()是外围类的成员,嵌套类不能访问外围类的成员}十、友元和类模板1、类模板中有普通友元函数,友元类,模板友元函数和友元类,普通友元函数和友元类不做介绍。2、可以建立两种类模板的友元模板,即约束型的友元模板和非约束型的友元模板。3、非约束型友元模板:即类模板的友元模板类或者友元模板函数的任一实例都是外围类的任一实例的友元,也就是外围类和友元模板类或友元模板函数之间是多对多的关系4、约束型友元模板:即类模板的友元模板类或友元模板函数的一个特定实例只是外围类的相关的一个实例的友元。即外围类和友元模板类或友元模板函数之间是一对一的关系。3、约束型友元模板函数或友元类的建立:比如有前向声明:template void g(T1 a); template void g1();
templateclass B;则templateclass A{friend void g<>(T a); friend void g1(); friend class B;};就建立了三个约束型友元模板,其中g和g1是函数,而B是类。注意其中的语法。这里g型和类A型是一对一的友元关系,g和A是一个一对一的友元关系。6、非约束型友元模板函数或友元类的建立:非约束型友元模板和外围类具有不同的模板形参,比如templateclass
A{template friend void g(T1 a); template friend class B;}注意其中的语法,非约束型友元模板都要以template开头。要注意友元模板类,在类名B的后面没有尖括号。7、不存在部分约束型的友元模板或者友元类:比如template class A{templatefriend void g(T1 a, T b);
templatefriend class B;}其中函数g具有templatevoid g(T1 a,T2 b)的形式。其中的函数g试图把第二个模板形参部分约束为类A的模板形参类型,但是这是无笑的,这种语法的结果是g函数的非约束型类友元函数,而对类B的友元声明则是一种语法错误。
//友元模板示例。templateclassB;template classA{inta;
template voidg(T2 b);template; //注意语法,这里没有以template<>开始。//template<> friend class B; //错误,要把类友元类B约束为类A的T类型请使用以上的语法。friendvoidg<>(T b); //注意语法,这里没有以template<>开始,这里把友元模板函数的类型形参T2约束为类A的类型T//friend void g(T b);//笑果同上,如果函数没有形参的情况使用该语法。//template<> friend void g(T b);//错误,要把函数g与类A的类型形参T相关联请使用上面的语法。//类模板的非约束友元模板函数和友元类的声明形式template;//错误,这是语法错误,这里试图把T÷的类型约束为类A的T类型,这种方法是错误的,具体情况请参看类模板的部分特化部分//template voidg(T2 b){A d;A mg;mg.a=1;cout<classB{public:voidgb(){A mb; mb.a=2;cout< mg2;Amg22;mg22.a=x 0)yz{..|}~~•+..%&'*0(|}~•+€ 0(cout< mg1;A mg2; mg1.a=ƒ0)yz{..|}„•+..%&'*0…template md; md.a=ˆ0)yz{..|'•+..%&'*0……0template mb; mb.a=Š0)yz{..|‹•+..%&'*0……0intmain(){g(Œ•TD0}• • D0//正确,//g(ŽD011错误,函数g是约束型的友元函数,也就是说这里的调用只能访问类A的int型的对象的私有成员即A d对象的私有成员,但函数g中有A mg;对象的私有成员的访问,所以该调用将是错误的。同样如果该函数中有A d;对象对类A的私有成员的访问时上面的g函数调用也是错误的。B mbb;();B mbb1;// ();//该函数调用错语,错误原因同上面的g(¨D调用。g1(2); g1(©• D0(//这里的两种调用都是正确的,因为g1不是约束型的友元函数。g2(±C D0g2(²C • D0//从该调用可以看出,试图对函数g2进行部分约束将是不可行的。C mc; ();C mc1; ();//两种调用方式都正确,原因同g1函数的调用。D md; //(); //错误,类D不是类A的友元,不能访问类A的私有成员。}十一、模板与继承1、当从模板类派生出子类时必须注意的是:子类并不会从通用的模板基类继承而来,只能从基类的某一实例继承而来。这样的话就有以下几种继承方式:a、基类是模板类的一个特定实例版本比如templateclass B:public A{};b、基类是一个和子类相关的一个实例,比如templateclass B:public A{};这时当实例化一个子类B的时候基类就相应的被实例化为一个和基类相同的实例版本,比如B m;模板类B被实例化为int版本,这时基类A也相应的被实例化为int版本。c、如果基类是一个特定的实例的版本,这时子类可以不是一个模板,比如class B:public A{}。//模板与继承的示例template classA{ public: voidga(){cout<<"class A "< classB:publicA{public: voidgb(){cout<<"class B "< classC:publicA{public: voidgb(){cout<<"class C "<{}; //从特定实例继承时子类可以是一般的类intmain(){B m;(); //输出class A int;(); //输出class B double;C mc;
(); //输出class A ();} //输出class A double十二、模板中的关见字typename和class的区别1、首先typename是一个较新的关见字,而class是比较老的关见字,在类模板的声明中使用哪个都是一样的。2、必须使用typename关见字的情况:当有一个模板,且模板的类型形参和某一个类名同名,而且这个同名的类中又有一个嵌套类的时候就要使用关见字typename了,比如有类class T{public: class A{};}; 则template class B{T::A
m;};这时的语句就会出错,因为模板类B无法区别出T::A表示的是一个类型还是一个数据成员,在C++里面默认情况下是一个数据成员,在这里的语句T::A m就会出错,会出现m前缺少类型说明符的错误。要使该式正确就必须在T::A前加上typename关见字以说明T::A表示的是一个类型。即template class B{typename T::A m;}这样就正确地声明了一个类T的嵌套类A的对象m。当然如果模板的类型形参和那个类名不同名时不会出现这种错误,比如把嵌套类B的外围类的类名改为D,则在模板类B中的语句D::B m;将是正确的语句。//关见字typename和class的区别的示例classT{public:staticinte;classB{public:voidgb(){cout<<"class B"<classA{public:T::B a;}; //模板形参与带有嵌套的类T不同名,所以这里的语句T::B a是正确的。当然在前面加上typename关见字也不会出错。templateclassD{public: typenameT::B b;}; //模板形参与带有嵌套类的类名同名,在语句T::B b;前必须加上typename以指定这里的嵌套类B是一个类型而不是一个表达式。templateclassðvpublic: voidg(){inta=T::eñ~0)yz{..+..%&'*0……0(//这个范例可以看出如果不在T::e前加typename则系统默认表示的是e是一个数据成员,而不是一个类型。intmain(){A ma; (); //输出class BD md; (); //输出class Bù.int> mf; mf.g(); } //输出数字û,在这里可以清楚的看到T::e默认表示的是一个数据成员而不是类型。作者:黄邦勇帅
评论列表(0条)