一、typename的使用场合
typename主要用来表明“其后面跟的是一个类型”。
(1)在模板定义里,表明其后的模板参数是类型参数。
看一下函数模板的定义:
template<typename T,int a,int b> //可以这样写template<class T,int a,int b>
int funcaddv2(T c){...}
再看一个类模板的定义:
template<typename T> //可以这样写template<class T>
class myvector{...};
在这里,typename也可以写成class,这两个关键字在这里的功能一样。但是需要注意的是,这里如果写成class,和定义时用的class完全是两个不同的含义。
(2)用typename标明这是一个类型(类型成员)。
“::”是作用域运算符,当访问类中的静态成员变量时需要用到,即“类名::静态成员变量名”。例如,定义一个类的静态成员变量并赋予初值:“int Time::mystatic = 15;”。
(3)“::”还可以用来标明类型成员。
如下例子,在.h头文件中声明:
template<typename T>
class myvector
{
public:
typedef T* myiterator;
public:
myvector();
myvector& operator=(const myvector&);
public:
myiterator mybegin();
myiterator myend();
public:
void myfunc();
};
在.cpp中这样定义:
template<typename T>
myvector<T>::myvector()//实例化构造函数
{
}
template<typename T>
void myvector<T>::myfunc()//实例化构造函数
{
cout << "类模板成员函数!" << endl;
}
template<typename T>
typename myvector<T>::myiterator myvector<T>::mybegin()
{
}
int main()
{
myvector <int> tmpvector1;
tmpvector1.myfunc();
myvector <double> tmpvector2;
tmpvector1.myfunc();
myvector <string> tmpvector3;
tmpvector1.myfunc();
return 0;
}
我们着重看这一句:
template<typename T>
cmyvector<T>::myiterator myvector<T>::mybegin()
{
//......
}
这段代码首先要强调作用域运算符“::”的第二个用法--访问类型成员。myiterator 是一个类型,mybegin返回的就是这种类型。所以,在类模板声明之外要书写这种类型就要用到“::”,就是上面看到的myvector<T>::myiterator写法代表一个类型(T*类型)。
我们可以看到,在myvector<T>::myiterator之前,额外用到一个typename。首先,普通类型用不到,类模板才会用到。
编译器在遇到myvector<T>::myiterator时,不确定表示的是一个类型还是一个静态成员变量,而C++在默认情况下通过作用域运算符访问的是静态成员变量而不是类型,所以加typename是为了告诉编译器myiterator是一个类型,所以在其前面用typename来修饰。
但是需要注意的是:这里template不能用class来替换。
(4)用于修饰函数模版返回值类型。
template<typename T>
typename T::size_type getLenth(const T&c)//前面要加typename,否则报错
{
//这些诸如empty、size等成员函数,在函数模版未被实例化之前,谁也不知道这些成//员函数到底写得对还是不对,但一旦被实例化,自然可知
if(c.empty())
{
return 0;
}
return c.size();
}
在main主函数中,加入如下代码:
string mytest="I LOVE CHINA!";
//size_type类似unsigned int 类型,定义于string类中,一般和string配套使用,考虑到各种//机器中数据类型的差异,所以引入这个类型,保证程序与实际的机器匹配的目的。
string::size_type size2 = getLenth(mytest);
cout<<size2<<endl;//13
二、函数指针作为其他函数的参数和把函数指针改写成函数模板
下面写一个普通函数:
int fc(int tmp1,int tmp2)
{
//......
return tmp1+tmp2;
}
在.h头文件中声明一个函数指针类型:
typedef int(*funType)(int,int);
下面写一个函数,把funType作为参数传进去:
void testfunc(int i,int j,funType funcpoint)
{
int result = funcpoint(i,j);
cout<<result<<endl;
}
在main主函数中加入如下代码:
testfunc(10,20, fc);//等于30
因为拿到了函数fc的函数指针,所以可以通过这个指针调用函数fc。
把testfunc函数改写成函数模板如下:
template<typename T,typename F>
void testfunc(const T & i,const T & j,F funcpoint)
{
cout<<funcpoint(i,j)<<endl;
}
可以看到:
系统通过第一个参数10和第二个参数20,推断出testfunc的模板参数T是int类型,推断出模板参数F是函数指针类型,所以funcpoint就是函数指针,从而可以直接使用funcpoint来进行函数调用。
现在引入“可调用对象”的概念,这里先简单了解一下,如果一个类,重载了“()”运算符,那么如果生成了该类的一个对象,就可以用“对象名(参数.......)”的方式来使用该对象,看起来就像函数调用一样,那么用这个类生成的对象就是一种可调用对象(可调用对象很多种,这里先只说这一种)。
现写一个可调用对象所代表的类,只要重载“()”运算符即可,如下:
class kuohao
{
public:
kuohao() { cout << "构造函数执行!" << endl; }
kuohao(const kuohao& c) { cout << "拷贝构造函数执行!" << endl; }
int operator()(int v1, int v2)const
{
return v1 + v2;
}
};
在main主函数中这样调用:
kuohao kh;
testfunc(12, 25, kh);//这里调用拷贝构造函数
程序运行如下:
看上面主函数的第2行代码,函数testfunc的第3个参数传递进去了一个kh对象,系统推断模板参数F的类型应该为kuohao(类类型),因此testfunc函数模板这里会调用kh类的拷贝构造函数来生成一个叫作funcpoint的kuohao类型的对象。
经过调试得知:
代码执行到“cout<<funcpoint(i,j)<<endl;”实际执行的就是可调用对象,也就是kuohao类中重载的“()”运算符,所以这行代码打印出来的结果为37。
可以把“testfunc(12, 25, kh);”改为“testfunc(12, 25, kuohao());”,可以看到,这里并没有执行kuohao类的拷贝构造函数,只执行了一次kuohao类的构造函数。这说明系统推断F类型应该为kuohao(类类型),然后直接把代码kuohao()生成的临时对象构造到funcpoint对象形参中去,这样节省了一次拷贝构造函数的调用,也节省了一次对析构函数的调用。
总结:
同一个函数模版testfunc,根据传递进去的参数不同,就能够推断出不同的类型,上面的延时推断出了两种类型:
(1)推断出是函数指针;
(2)推断出的是一个对象,而且是一个可调用对象。
三、默认模板参数
1、类模板
类模板名后面必须用尖括号“<>”来提供额外信息,这和函数模板不一样,函数模板有很多时候不需要用“<>”(编译器能够推断出类型)的,而类模板中的尖括号“<>”必须有,“<>”表示类必须是从一个类模板实例化而来,所以“<>”有一个表示“这是一个模板”的强调作用。
如果一个模板参数也给一个默认值,代码如下:
template<typename T,int size = 10>
class myarray
{
private:
T arr[size];
};
注意:如果某个模板参数有默认值,那么从这个有默认值的模板参数开始,后面的所有模板参数都得有默认值(这一点和函数的形参默认值规则一样)。
调用的时候,如果完全用默认值,则可以直接使用一个空的尖括号(空的尖括号不能省):
myarray< >abc;
一般情况下,在程序中遇到这种类名后面带“<>”的形式,都表示这是一个类模板并且使用的是默认模板参数。
如果想提供一个模板参数,而另外一个模板参数要用默认值,可以这样写代码:
myarray<int a=10,b=20>def; 等价于 myarray<int>def;
2、函数模板
老的C++标准只允许为类模板提供默认模板参数,C++11新标准也可以为函数模板提供默认模板参数。
假如需要按如下要求进行函数调用:
testfunc(3,4);
可以看到,最后一个模版参数没有提供,需要给函数模版testfunc提供默认参数,让testfunc能够实现调用kuohao类里重载的“()”的能力,修改testfunc函数模版如下:
template<typename T, typename F = kuohao>
//F funcpoint = F()等价kuohao funcpoint = kuohao()
void testfunc(const T& i, const T& j, F funcpoint = F())
{
cout << funcpoint(i, j) << endl;
}
注意:
这里不但为模板参数F提供默认参数kuohao(类名),还为函数模板testfunc的第三个形参提供默认值,注意这个默认值的写法F functest=F()。这个等于默认在这里构造了一个临时的类kuohao的对象,直接构造到funcpoint所代表的空间,现在funcpoint就是一个类kuohao的对象。
有几点需要说明:
(1)必须同时为模板参数和函数参数指定默认值,一个也不能少,否则语法通不过且语义也不完整。
(2)kuohao类必须重载“()”运算符,必须保证funcpoint是一个可调用对象,否则代码运行“cout<<funcpoint(i,j)<<endl;”编译时会报错。
(3)一旦给函数提供了正常参数,默认参数就不起作用了,例如:testfunc(3,4,fc);
如果给函数模板testfunc提供不是类包含的默认函数,也保证代码testfunc(3,4);能正常运行,代码如下:
template<typename T, typename F = funType>
void testfunc(const T& i, const T& j, F funcpoint = fc)
{
cout << funcpoint(i, j) << endl;
}
默认模板参数F是一个函数指针类型(funType),函数参数funcpoint=fc中的fc是函数名,代表耗时首地址。
2022.08.11结。