从汇编的角度揭秘C++函数重载,原来这么简单
函数重载是指在同一个作用域内,有多个同名函数,但是它们的形参列表不同。在调用时,根据不同的实参,调用相应的函数。函数重载是一种静态多态形式。我们先来看一个函数重载的例子,然后分析其背后的原理。请看下面这段代码:
#include <stdio.h>int sum(int a, int b)
{int ret = a + b; printf("int type, sum = %d\r\n", ret); return ret;
}float sum(float a, float b)
{float ret = a + b; printf("float type, sum = %f\r\n", ret); return ret;
}int main()
{int i = 1, j = 2;float h = 1.0, k = 2.0; printf("first call\r\n");sum(i, j);printf("second call\r\n");sum(h, k);return 0;
}
上面这段代码定义了两个同名函数:sum,但是形参不一样。在main函数中分别用两个int形,两个float型的变量去调用sum函数。期望的结果是:两个int型变量作为型参,会调用第一个sum函数。两个float型变量作为型参,会调用第二个sum函数。编译以及运行结果:
从输出结果来看,第一次调用第一个sum函数,第二次调用第二个sum函数。实现了静态多态的一个实现。其背后的原理是什么呢?为什么在同一个作用域内,有两个相同名的函数不会有编译错误呢?查看一下可执行文件的汇编代码:
objdump -s -d function_loading > resultvi result
找到main函数汇编代码:
00000000000011f7 <main>:11f7: f3 0f 1e fa endbr6411fb: 55 push %rbp11fc: 48 89 e5 mov %rsp,%rbp11ff: 48 83 ec 10 sub $0x10,%rsp1203: c7 45 f0 01 00 00 00 movl $0x1,-0x10(%rbp)120a: c7 45 f4 02 00 00 00 movl $0x2,-0xc(%rbp)1211: f3 0f 10 05 33 0e 00 movss 0xe33(%rip),%xmm0 # 204c <_IO_stdin_used+0x4c>1218: 001219: f3 0f 11 45 f8 movss %xmm0,-0x8(%rbp)121e: f3 0f 10 05 2a 0e 00 movss 0xe2a(%rip),%xmm0 # 2050 <_IO_stdin_used+0x50>1225: 001226: f3 0f 11 45 fc movss %xmm0,-0x4(%rbp)122b: 48 8d 05 fe 0d 00 00 lea 0xdfe(%rip),%rax # 2030 <_IO_stdin_used+0x30>1232: 48 89 c7 mov %rax,%rdi1235: e8 26 fe ff ff callq 1060 <puts@plt>123a: 8b 55 f4 mov -0xc(%rbp),%edx123d: 8b 45 f0 mov -0x10(%rbp),%eax1240: 89 d6 mov %edx,%esi1242: 89 c7 mov %eax,%edi1244: e8 20 ff ff ff callq 1169 <_Z3sumii>1249: 48 8d 05 ec 0d 00 00 lea 0xdec(%rip),%rax # 203c <_IO_stdin_used+0x3c>1250: 48 89 c7 mov %rax,%rdi1253: e8 08 fe ff ff callq 1060 <puts@plt>1258: f3 0f 10 45 fc movss -0x4(%rbp),%xmm0125d: 8b 45 f8 mov -0x8(%rbp),%eax1260: 0f 28 c8 movaps %xmm0,%xmm11263: 66 0f 6e c0 movd %eax,%xmm01267: e8 38 ff ff ff callq 11a4 <_Z3sumff>126c: b8 00 00 00 00 mov $0x0,%eax1271: c9 leaveq1272: c3 retq1273: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)127a: 00 00 00127d: 0f 1f 00 nopl (%rax)
这里大家如果看不懂或者不熟悉汇编,也没关系。重点关注这两条命令:
1244: e8 20 ff ff ff callq 1169 <_Z3sumii>
1267: e8 38 ff ff ff callq 11a4 <_Z3sumff>
call指令用于函数的调用,所以这两条指令分别是调用_Z3sumii以及_Z3sumff函数。对照main函数源码,可以看出,是这两条源码:
sum(i, j);
sum(h, k);
但是这里有点奇怪,源代码中是sum函数,在汇编里面怎么是_Z3sumii以及_Z3sumff呢。其实原理是,c++编译器会对函数名进行修饰。sum函数的后缀ii以及ff分别是代表形参分别是int和int,以及float和float。即一个sum函数经过修饰之后,变成了两个不同名的函数_Z3sumii以及_Z3sumff。为了验证这个结论,可用如下命令:
> nm function_loading | grep sum
输出如下:
00000000000011a4 T _Z3sumff
0000000000001169 T _Z3sumii
说明function_loading中定义了_Z3sumff以及_Z3sumii这两个符号。但这两个符号是修饰后的结果,那么修饰前的符号是怎么样的呢?通过以下命令:
>c++filt _Z3sumff
输出如下:
sum(float, float)
>c++filt _Z3sumii
输出如下:
sum(int, int)
跟源码正好匹配上。说明_Z3sumff是sum(float, float)修饰后的结果,_Z3sumii是sum(int, int)修饰后的结果。验证了前面的结论:sum函数的后缀ii以及ff分别是代表形参分别是int和int,以及float和float。即一个sum函数经过修饰之后,变成了两个不同名的函数_Z3sumii以及_Z3sumff。
从此,我们可以看出C++的函数重载的原理是C++编译器会对符号进行修饰。虽然函数名是一样的,但是经过修饰之后,函数名就不一样了。也就不会造成符号表冲突。
此文章的B站视频:用汇编深入剖析C++函数重载机制,原来这么简单_哔哩哔哩_bilibili