C++学习 十、函数重载,函数模板

C++学习 十、函数重载,函数模板

  • 前言
  • 函数重载
    • 二义性
      • 强制类型转换
      • 类型与类型引用
      • 默认参数
    • const指针参数和const引用参数
  • 函数模板
    • 模板函数声明与定义
    • 函数模板重载
    • 显式实例化
    • 显式具体化
  • 重载解析
    • 引导编译器使用函数模板
  • 后记

前言

本篇继续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形参函数,而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++需要判断使用哪一个函数,称为重载解析。

重载解析的过程是:

  1. 挑选与调用函数同名的函数重载,函数模板和模板重载,形成候选函数列表;
  2. 从候选函数列表中,挑选参数列表匹配的候选函数,形成匹配函数列表;
  3. 从匹配函数列表中,挑选最佳函数。

最佳函数的判定顺序如下:

  1. 参数类型完全匹配,但常规函数优于模板;
  2. 提升转换,如短整型变长整型,单精度变双精度等;
  3. 标准转换,如整型变浮点型等;
  4. 其它转换。

此外,除了类型完全一致外,C++认为以下类型也属于完全匹配:

  • TypeName与TypeName&
  • TypeName[]与TypeName*
  • TypeName(实参)-> const or volatile TypeName(形参)
  • 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(x, y)明确要求进行int的显式实例化。

后记

本篇比较详细的记录了C++函数重载和函数模板的内容。下篇将记录C++ const关键字与volatile关键字。

你可能感兴趣的:(C++与数据结构,c++,开发语言,后端)