在之前的C语言编程中,一个函数实现一个功能,但有时候我们需要实现几个功能类似的函数,只是有些细节不同,如果按照C语言的编程方式,我们需要重新定义函数,这会使得代码十分不美观。但在C++中,我们可以使用重载或模板很好的解决这个问题。
函数重载
定义
在同一作用域类中,一组函数名相同
,参数列表(函数特征标)不同(参数个数不同/参数类型不同/参数排列顺序不同
),返回值可同可不同的函数。
重载函数通常用来命名一组功能相似的函数
,这样做减少了函数名的数量,对于程序的可读性有很大的好处。
示例
先看一个简单的加法函数重载:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using namespace std;
int fun(int, int);
int fun(int *, int *);
int fun(int a, int b)
{
cout << "int a + int b : ";
return a + b;
}
int fun(int * a, int * b)
{
cout << "int *a + int *b : ";
return *a + *b;
}
int main(void)
{
int a1 = 1, b1 = 2;
int a2 = 2, b2 = 3;
int *p = &a2, *q = &b2;
cout << fun(a1, b1) << endl;
cout << fun(p, q) << endl;
return 0;
}
定义了2个fun
函数,其参数类型分别为int
和int *
类型,返回值都是int
类型,为了更好的说明,在每个函数中都加入输出提示。最终输出结果为:
1 | int a int b : 3 |
这个例子很好理解,但如果此时在定义一个double
类型变量,并调用fun
函数,会显示什么?如下所示:
1 | double a3 = 1.2, b3 = 2.9; |
输出结果:
1 | int a int b: 3 |
为什么此时没有报错呢?明明没有定义参数列表是double
类型的fun
函数。如果再定义个fun
函数的重载,将参数类型设置为int &
,即引用参数,如下所示:
1 | int fun(int &, int &); |
此时再调用刚才的cout << fun(a1, b1) << endl;
程序会报错:error C2668: “fun”: 对重载函数的调用不明确
。这又有什么会报错呢?见下文解析。
原理
如何解决命名冲突
编译器在编译当前作用域里的同名函数时,会根据函数形参的类型和顺序会
对函数进行重命名(不同的编译器在编译时对函数的重命名标准不一样)。在visual studio
编译器中,根据返回值类型(不起决定性作用)+形参类型和顺序(起决定性作用)的规则重命名并记录在map
文件中。
右击工程名,然后选择属性
,在依次选择配置属性
->链接器
–>调试
,将其中的生成映射文件
和映射导出
都设置为是
,映射文件名
为生成的map
文件名,可以自己命名也可以用默认的。点击确定,并运行程序后,会在工程文件夹中的Debug
文件夹下生成一个.map
的文件夹,将其拖拽至编译器内即可打开。
从图中可以看到,虽然函数名相同,但在map
中的生成的名称去不一样。?
表示名称开始,?
后边是函数名,@@YA
表示参数表开始,后边的3个字符分别表示返回值类型
,参数类型
。@Z
表示名称结束。由上述分析可知,函数重载仅仅是语法层面的,本质上它们还是不同的函数,占用不同的内存,入口地址也不一样。
如何解决调用匹配
除了利用函数重载可以实现相同的函数名实现不同的功能,函数模板同样的也可以实现。为了更好的解释其中的调用匹配问题,在讲完函数模板后,再重新解释(刚才提到的问题属于调度匹配问题)。
作用及意义
函数重载是属于多态中的静态多态,即在编译时的多态,而虚函数与虚继承属于动态多态,即在运行时的多态,其两者都是为了减少函数名的数量,避免名字空间的污染,提高程序的可读性。
模板
定义
模板也是一种C++支持参数化多态的工具,使用模板可以为类或函数
声明一种一般模式,使得类中的某些数据成员、成员函数的参数、返回值
取得任意类型。
模板通常有两种形式:函数模板和类模板,函数模板针对仅参数类型不同的函数;类模板针对仅数据成员和成员函数类型不同的类。(本博客只讨论函数模板。)
模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。
函数模板
函数模板是通用的函数描述,也就是说,它们使用泛型
来定义函数,其中的泛型可用具体的类型(如int或double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。
由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型。
示例
先看一个简单的函数模板定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using namespace std;
template <typename T>
T fun(T a, T b)
{
return a * b;
}
int main(void)
{
int a1 = 1, b1 = 2;
cout << fun(a1, b1) << endl;
return 0;
}
程序输出:2
函数模板的一般定义格式如下:template <typename T>返回值 函数名(T 参数){}
,其中typename
可以替换为class
。
函数模板有两种类型的参数,第一种是模板参数,位于函数模板名称的前面,在一对尖括号内部进行声明;第二种是调用参数,位于函数模板名称之后,在一对圆括号内部进行声明。
如果可以由调用参数来决定模板参数,则模板函数调用是不需要指明模板参数,但如果不能则必须指明,例如以下情况:1
2double a2 = 1.2, b2 = 2.3;
cout << fun(a1, b2) << endl;
此时再调用的话,就会报错:C2782 “double fun(T,T)”: 模板 参数“T”不明确
,因为此时变量a1
和b2
不是同一个类型,而函数模板定义中并没有说明这一点,所以正确的定义和调用应该如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using namespace std;
template <typename T1, typename T2, typename T3>
T3 fun(T1 a, T2 b)
{
return a * b;
}
int main(void)
{
int a1 = 1, b1 = 2;
double a2 = 1.2, b2 = 2.3;
cout << fun<int, double, double>(a1, b2) << endl;
return 0;
}
总之,调用和定义时的类型必须保持一致,当编译器无法判断时,需要显示地指明参数类型。
当然模板也是可以重载的,比如再定义一个可以将2个数组中的各个元素相乘的函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using namespace std;
template <typename T1, typename T2>
void fun(T1 a, T2 b)
{
cout << a * b << endl;
}
template <typename T1, typename T2>
void fun(T1 *a, T2 *b, int n)
{
for (int i = 0; i < n; i++)
{
cout << a[i] * b[i] << ' ';
}
cout << endl;
}
int main(void)
{
int a1 = 1, b1 = 2;
double a2 = 1.2, b2 = 2.3;
double a3[2] = { 1.2, 2.3 }, b3[2] = { 2.3, 3.4 };
fun<int, double>(a1, b2);
fun<double, double>(a3, b3, 2);
return 0;
}
程序输出结果为:1
22.3
2.76 7.82
那么面对如此多的相同名字的函数,编译器到底是如何选择的呢?