本篇继续C++记录,函数重载与函数模板。
函数重载是C++的特色,允许一个程序中定义多个同名函数,但函数的参数列表(也称为特征标)必须不同。可以通过函数重载,使函数根据不同类型的参数实现不同的功能(相同的也行),示例如下:
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
void swap(double& a, double& b) {
double temp = a;
a = b;
b = temp;
}
void main(){
int x=1, y=2;
swap(x, y);
}
编译器会按照调用函数的参数个数和类型,决定使用哪个同名函数。
参数类型和个数完全匹配的同名函数将被优先使用;如果参数类型没有完全匹配,则查找能够通过强制类型转换达成要求的同名函数。否则会报找不到函数的错误。
注意:函数重载必须是参数列表不同,函数返回类型不能作为重载依据。编译只会根据函数名和参数列表,对函数名进行区分。
函数重载容易出现的问题就是二义性,即编译器不能根据参数区分同名函数。下面是几种常见的情况。
如果实参与参数列表不完全匹配,编译器在强制类型转换时,容易出现二义性,如下所示:
int func()(int a);
long func()(long b);
float r=1.;func(r);
将报错error: call of overloaded 'func(double)' is ambiguous
,这就是强制类型转换带来的二义性。
在同名函数选择中,强制类型转换也具有优先级,但这与编译器相关。一般而言,短整形转长整形,单精度转双精度浮点型会优于整形转浮点型,长整型转短整型,双精度转单精度浮点型。
比如下面的重载就不会报错:
int func(double a);
int func(int a);
void main(){
float r;
func(r);
}
而这样的重载会报错:
int func(double a);
int func(int a);
void main(){
long r;
func(r);
}
C++将类型与类型引用看作相同的特征标,如下所示:
void func(int a);
void func(int& a);
上面的func
参数列表看似不同,然而int x=1;func(x);
,两个同名函数都满足参数要求,出现了二义性。
如果两个同名函数的参数个数不同,参数多的函数带有默认参数,如下所示:
void func(int a, int b, int c=5);
void func(int a, int b);
void main(){
int x=1,y=2;
func(a, b);
}
则会出现二义性错误,编译器不知道应该使用哪个同名函数。
对于指针和引用参数而言:
普通函数中,const实参不能传给非const形参,而非const实参,可以传给const形参(原因在下篇const关键字中会提到)。
重载函数中,非const实参将优先匹配非const形参函数,而const实参当然只能匹配const实参函数了。
注意:对于非指针和引用类型,下面的定义直接报redefinition
重定义错误:
void func(int a);
void func(const int a);
函数模板也是C++的一大特色,泛型编程。如果一个函数对于多种类型都有相同的功能,那么使用函数模板能够节省时间,提升效率。
比如交换两个变量,如果使用函数重载,那么对于整型,浮点型,字符,字符串,数组等都需要写一个同名函数,定义类似:
void swap(int& a, int& b){
int temp = a;
a = b;
b = temp;
}
void swap(double& a, double& b){
double temp = a;
a = b;
b = temp;
}
...
函数模板则可以使用任何类型来定义函数:
template<typename T>
void swap(T& a, T& b){
T temp = a;
a = b;
b = temp;
}
void main(){
int x=1,y=2;
swap(x, y);
}
template
指出要建立一个模板,类型名为T
,然后定义了模板函数。此外,参数列表并不要求都是类型T
。
模板并不会创建函数,而只是告诉编译器怎样定义函数。随后,编译器在程序中查找调用swap模板的参数类型,根据模板生成相应类型的函数,这种操作叫做隐式实例化implicit instantiation。上面的程序中,编译器创建了int类型的swap函数。
最终生成的代码中并不带有模板函数,而是包含实际创建的函数。
在我之前的博客中记录了定义模板时的一个问题:把模板声明与定义放在.h和.cpp文件中,如果在其它源文件调用模板.h文件,会报undefined reference
的问题。
原因:由于隐式实例化,在模板的.cpp定义文件中,函数模板没有被调用,因此编译模板的.cpp定义文件时,不会创建真实的函数;而其它源文件中没有模板的定义文件,即使包含了模板.h声明文件,也不会创建真实的函数。因此,编译器就找不到这个函数的定义了。
解决方法一:把模板声明和定义都放在.h文件中,虽然这并不符合C++编程“美学”。
解决方法二:使用后面将提到的显式实例化。
函数模板也可以被重载,并也要求参数列表不同。
注意:在常规函数、重载函数、函数模板共存的情况下,如果调用实参与常规函数或者重载函数匹配,则不会调用模板创建函数。
隐式实例化导致模板声明.h,定义.cpp文件分离出错,可以通过显式实例化直接在.cpp文件中创建函数:
template
void swap<int>(int&, int&);
编译器看到上面的声明后,就根据函数模板创建一个int类型的swap函数实例。
上面的函数模板swap可以交换很多类型,比如整型,浮点型。但是,有的时候模板并不能完全适用于所有类型。比如我想交换结构myStruct
的成员c
,但其它成员保持不变,如下所示:
struct myStruct{
int a;
float b;
double c;
};
void main(){
myStruct t1{1,2,3}, t2{4,5,6};
swap(t1.c, t2.c);
}
当然可以采用上面的方案,以结构的成员作为swap
的参数。
然而,如果我想直接以结构作为参数,那么就需要采用模板具体化,针对某个类型提供具体的声明:
template <> void swap<myStruct>(myStruct& s1, myStruct& s2);
或者
template <> void swap(myStruct& s1, myStruct& s2); //equal
然后定义:
template <>
void swap<myStruct>(myStruct& s1, myStruct& s2) {
myStruct temp = s1;
s1.c = s2.c;
s2.c = temp.c;
}
乍一看,模板显式具体化与重载很像,都是给函数一个不同的实现方式。但是显式具体化是为模板的某种类型提供特殊的实现方法,由具体化类型(本例中是myStruct
)代替了T
;重载则与T
无关。
显式实例化与显式具体化的区别在于,显式实例化仍然是根据原模板函数,创建指定类型的实例;而显式具体化则是为指定类型提供了不同的定义。
注意:同一种类型的显式具体化与显式实例化不能同时存在,否则会报错。
隐式实例化、显式实例化、显式具体化统称为具体化,它们都创建了函数实例。而函数模板只是告诉编译器该怎样定义函数。
由于函数重载、函数模板、函数模板重载的共存,C++需要判断使用哪一个函数,称为重载解析。
重载解析的过程是:
最佳函数的判定顺序如下:
此外,除了类型完全一致外,C++认为以下类型也属于完全匹配:
const
or volatile
TypeName(形参)const
or volatile
TypeName*(形参)完全匹配的非模板函数优于模板生成的函数。完全匹配的函数模板,显式具体化优于实例化。
有时,可以指示编译器使用函数模板而不是常规函数:
void main(){
int x=1, y=2;
swap<>(x, y);
}
上面的语句中,swap<>(x, y)
明确指出使用函数模板创建函数实例。
另外,还可以要求编译器进行显式实例化:
void main(){
int x=1, y=2;
swap<int>(x, y);
}
上面的语句中,swap
明确要求进行int的显式实例化。
本篇比较详细的记录了C++函数重载和函数模板的内容。下篇将记录C++ const关键字与volatile关键字。