函数重载及原理详解

默认参数

  1. 半缺省参数必须从右往左依次来给出,不能间隔着给
  2. 缺省参数不能在函数声明和定义中同时出现
  3. 缺省值必须是常量或者全局变量

函数重载

C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表参数个数 或 类型 或 顺序必须不同,可以通过函数重载实现编译时多态

注意事项

  1. 由于函数传参为值拷贝,所以在没有最合适的匹配函数时,会生成临时变量来匹配对应实参
1
2
3
4
5
6
void Fuc(int a) {}

int main() {
Fuc(3.14); // 生成临时变量3
return 0;
}
  1. 当没有最合适匹配,但能匹配的函数不唯一时,编译器会报错
1
2
3
4
5
6
7
void Fuc(long a) {}
void Fuc(int a) {}

int main() {
Fuc(3.14); // 此时编译器并不知道调用哪个fuc函数,会出错
return 0;
}

匹配成功的前提是:该函数每个实参的匹配都不劣于其他可行函数需要的匹配,至少一个实参的匹配优于其他可行函数提供的匹配
匹配等级:

  1. 精准匹配:实参类型和形参相同、实参从数组类型或函数类型转换成对应的指针类型、向实参添加顶层const或者从实参中删除顶层const
  2. 通过const转换实现的匹配
  3. 通过类型提升实现的匹配
  4. 通过算术类型转换或指针转换实现的匹配
  5. 通过类类型转换实现的匹配
    如:
1
2
3
4
5
6
7
void Fuc(const double a) {};
void Fuc(double a) {};

int main() {
Fuc(3.14); // 匹配时,不区分const,有两个最合适匹配,会出错
return 0;
}
1
2
3
4
5
6
7
void Fuc(const int a) {}
void Fuc(double a) {}

int main() {
Fuc(3.14); // 两个函数都能匹配,但是第二个函数最匹配
return 0;
}

重载引用/指针参数

1
2
3
4
5
6
7
8
9
10
void Fuc(int* a) {}
void Fuc(const int* a) {}

int main() {
const int a = 3;
int b = 1;
Fuc(&a); // 只有第二个最匹配,调用Fuc(const int* a)
Fuc(&b); // 两个都匹配,但第一个最匹配,调用void Fuc(int* a)
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
void Fuc(double& a) {}
void Fuc(const double& a) {}
void Fuc(double && a) {}

int main() {
double a = 3.14;
int b = 3;
Fuc(a); // 可以匹配前两个,但是第一个最匹配,调用Fuc(double& a)
Fuc(b); // 可以匹配后两个,但是和第三个最匹配,调用Fuc(double && a)
Fuc(3.14 + 1); // 可以匹配后两个,但是和第三个最匹配,调用Fuc(double && a)
Fuc(3.14); // 可以匹配后两个,但是和第三个最匹配,调用Fuc(double && a)
return 0;
}

原理

拿这段代码来说

1
2
3
4
5
6
7
void Print(int a, double b, char c) {}
void Print(int* a, double* b, char* c) {}

int main() {
Print(3, 3.14, 'a');
return 0;
}

在linux下执行命令objdump -d a.out |less反汇编。仅展示部分:

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
30
31
32
33
000000000040062d <_Z5Printidc>:
40062d: 55 push %rbp
40062e: 48 89 e5 mov %rsp,%rbp
400631: 89 7d fc mov %edi,-0x4(%rbp)
400634: f2 0f 11 45 f0 movsd %xmm0,-0x10(%rbp)
400639: 89 f0 mov %esi,%eax
40063b: 88 45 f8 mov %al,-0x8(%rbp)
40063e: 5d pop %rbp
40063f: c3 retq

0000000000400640 <_Z5PrintPiPdPc>:
400640: 55 push %rbp
400641: 48 89 e5 mov %rsp,%rbp
400644: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400648: 48 89 75 f0 mov %rsi,-0x10(%rbp)
40064c: 48 89 55 e8 mov %rdx,-0x18(%rbp)
400650: 5d pop %rbp
400651: c3 retq

0000000000400652 <main>:
400652: 55 push %rbp
400653: 48 89 e5 mov %rsp,%rbp
400656: 48 83 ec 08 sub $0x8,%rsp
40065a: 48 b8 1f 85 eb 51 b8 movabs $0x40091eb851eb851f,%rax
400661: 1e 09 40
400664: be 61 00 00 00 mov $0x61,%esi
400669: 48 89 45 f8 mov %rax,-0x8(%rbp)
40066d: f2 0f 10 45 f8 movsd -0x8(%rbp),%xmm0
400672: bf 03 00 00 00 mov $0x3,%edi
400677: e8 b1 ff ff ff callq 40062d <_Z5Printidc>
40067c: b8 00 00 00 00 mov $0x0,%eax
400681: c9 leaveq
400682: c3 retq

Print(int a, double b, char c)的函数签名变为<_Z5Printidc>
Print(int* a, double* b, char* c)的函数签名变为<_Z5PrintPiPdPc>

验证其他代码后,可以发现大概是int->i,long->l,char->c,string->Ss…基本上都是用首字母代表

编译器实际在底层使用被重新修饰过的一个比较复杂的名字,被重新修饰后的名字中包含了:函数的名字以及参数类型。这就是为什么函数重载中几个同名函数要求其参数列表不同的原因。
linux 下函数名修饰规则:_Z + 名称空间 + 函数字符个数 + 函数名 + 类型首字符

有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译

1
2
3
4
5
6
extern"C" void Print(int a, double b, char c) {}

int main() {
Print(3, 3.14, 'a');
return 0;
}

在linux下执行命令objdump -d a.out |less反汇编。仅展示部分:

1
2
3
4
5
6
7
8
9
000000000040062d <Print>:
40062d: 55 push %rbp
40062e: 48 89 e5 mov %rsp,%rbp
400631: 89 7d fc mov %edi,-0x4(%rbp)
400634: f2 0f 11 45 f0 movsd %xmm0,-0x10(%rbp)
400639: 89 f0 mov %esi,%eax
40063b: 88 45 f8 mov %al,-0x8(%rbp)
40063e: 5d pop %rbp
40063f: c3 retq

由于C语言只是单纯的函数名。因此当工程中存在相同函数名的函数时,就会产生冲突