C++重难点:重载与模板

  在之前的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
#include <iostream>
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函数,其参数类型分别为intint *类型,返回值都是int类型,为了更好的说明,在每个函数中都加入输出提示。最终输出结果为:

1
2
int a int b : 3
int *a + int *b : 5

  这个例子很好理解,但如果此时在定义一个double类型变量,并调用fun函数,会显示什么?如下所示:

1
2
double a3 = 1.2, b3 = 2.9;
cout << fun(a3, b3) << endl;

输出结果:

1
int a int b: 3

  为什么此时没有报错呢?明明没有定义参数列表是double类型的fun函数。如果再定义个fun函数的重载,将参数类型设置为int &,即引用参数,如下所示:

1
2
3
4
5
6
int fun(int &, int &);
int fun(int & a, int & b)
{
cout << "& a + & b : ";
return a + b;
}

  此时再调用刚才的cout << fun(a1, b1) << endl;程序会报错:error C2668: “fun”: 对重载函数的调用不明确。这又有什么会报错呢?见下文解析。

原理

如何解决命名冲突

  编译器在编译当前作用域里的同名函数时,会根据函数形参的类型和顺序会对函数进行重命名(不同的编译器在编译时对函数的重命名标准不一样)。在visual studio编译器中,根据返回值类型(不起决定性作用)+形参类型和顺序(起决定性作用)的规则重命名并记录在map文件中。
  右击工程名,然后选择属性,在依次选择配置属性->链接器–>调试,将其中的生成映射文件映射导出都设置为映射文件名为生成的map文件名,可以自己命名也可以用默认的。点击确定,并运行程序后,会在工程文件夹中的Debug文件夹下生成一个.map的文件夹,将其拖拽至编译器内即可打开。
data
data

  从图中可以看到,虽然函数名相同,但在map中的生成的名称去不一样。表示名称开始,后边是函数名,@@YA表示参数表开始,后边的3个字符分别表示返回值类型参数类型@Z表示名称结束。由上述分析可知,函数重载仅仅是语法层面的,本质上它们还是不同的函数,占用不同的内存,入口地址也不一样。

如何解决调用匹配

  除了利用函数重载可以实现相同的函数名实现不同的功能,函数模板同样的也可以实现。为了更好的解释其中的调用匹配问题,在讲完函数模板后,再重新解释(刚才提到的问题属于调度匹配问题)。

作用及意义

  函数重载是属于多态中的静态多态,即在编译时的多态,而虚函数与虚继承属于动态多态,即在运行时的多态,其两者都是为了减少函数名的数量,避免名字空间的污染,提高程序的可读性。

模板

定义

  模板也是一种C++支持参数化多态的工具,使用模板可以为类或函数声明一种一般模式,使得类中的某些数据成员、成员函数的参数、返回值取得任意类型。
  模板通常有两种形式:函数模板和类模板,函数模板针对仅参数类型不同的函数;类模板针对仅数据成员和成员函数类型不同的类。(本博客只讨论函数模板。)
  模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。

函数模板

  函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如int或double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型。

示例

  先看一个简单的函数模板定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
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
2
double a2 = 1.2, b2 = 2.3;
cout << fun(a1, b2) << endl;

  此时再调用的话,就会报错:C2782 “double fun(T,T)”: 模板 参数“T”不明确,因为此时变量a1b2不是同一个类型,而函数模板定义中并没有说明这一点,所以正确的定义和调用应该如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
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
#include <iostream>
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
2
2.3
2.76 7.82

  那么面对如此多的相同名字的函数,编译器到底是如何选择的呢?

谢谢老板!