C++函数重载实现原理浅析
C++实现函数重载的技术手段是函数符号改名,所以我们可以通过分析编译器的函数符号改名机制来验证C++函数重载规则。
1.函数重载的概念
函数重载出现在相同作用域中的多个函数,具有相同的名字而形参表不同。
注意:不能仅仅基于不同的返回类型而实现函数重载。返回值是不影响函数签名的。
2.函数调用:
函数调用时会发生什么?学过8086汇编时,我们都知道函数调用是程序执行点跳转到一个符号所在的地方转而执行符号所在地址的代码,然后再跳回去。这个符号就是函数。
我们用一个简单的例子来说明一下函数调用
//在这个简单的实例中,我们只是简单的在main函数中调用了一下printhello函数来打印hello world!
- #include <stdio.h>
- void printhello()
- {
- printf("hello world! ");
- }
- int main()
- {
- printhello();//这里调用函数printhello
- return 0;
- }
- //函数调用部分对应的汇编代码为:
- main:.LFB1:
- .cfi_startproc
- pushq %rbp
- .cfi_def_cfa_offset 16
- .cfi_offset 6, -16
- movq %rsp, %rbp
- .cfi_def_cfa_register 6
- movl $0, %eax
- call printhello ;这里call printhello,跳转到符号printhello出执行
- movl $0, %eax
- popq %rbp
- .cfi_def_cfa 7, 8
C语言中函数符号名和对应的函数名是一样的,而C++为了支持函数重载,符号名是在对应的函数名上改编的。如下图所示,函数名为func,而对应的符号名为_Z4funcv。
3.C++的函数符号命名规则
在前面的的图示中,我们给出了C++函数编译符号实例,貌似函数名是对应符号的子串额。实际上函数的编译符号是根据函数名,函数的参数表(包括参数类型和数量)相关的。而且不同的编译器的命名规则不一样。只要能保证相同的函数名和不同的函数参数列表生成的符号名不一样就行。下面我们来感受一下GCC的C++编译器的命名规则。
3.1函数返回类型不影响生成的符号名
前面我们说不能仅仅基于不同的返回类型而实现函数重载,原因是函数返回值并不影响最后生成的符号。我现在就验证一下:
我们分别在两个cpp文件中定义两个同名但返回值不同的函数,看一看他们在汇编代码中的符号是否一样。
第一个函数返回类型为void,生成的符号名为:_Z4funcv
第二个函数名也为func,但返回类型为int,生成的符号名还是为:_Z4funcv
上面两个同名但返回值类型不同函数生成相同符号名,说明返回类型是不影响符号名的。如果你定义两个函数,只是返回类型不同,那么它们生成的符号一样,肯定会发生符号重定义错误。
3.2 函数名,参数列表(参数类型、数目)才是影响符号名的因数
下面我们观察多个参数列表不同的同名函数,看看它们对应的符号名是什么。这次我们不直接观察汇编代码(汇编代码太长了),而是用objdump -t命令直接观察代码对应的目标文件中的符号表。
假设有下面的这些函数(左),以及它们生成符号(右)
看来改编的符号名是在函数名前加了一个前缀,如果没有参数就在后面加一个字母v,如果是int参数就加一个i,如果是char参数就加一个c,float参数就加一个f,double参数加一个d。引用加R,指针加P。貌似我们找到了某种规则。不过不同的规律改编的方法不一样,我们没必要在意某个编译器使用的改名规则。只需要知道函数名+参数列表决定了符号名
就行。也可以看到第二个函数的int返回类型并没对函数的符号名有什么影响。
3.3 const形参对函数的符号名有影响吗?
3.3.1 第1组实验:
理论上const int a和int a是不同类型的变量,那const对函数对应的符号名有影响吗?
我们用下面的代码测试一下:
哦,func1和func2形参只是一个有const限定,一个木有。它们出现了重定义错误,说明它们对应的符号是一样的,看来const对符号名木有影响啊,加或不加都一样。真的是这样吗?我们再来测两组。
3.3.2 第2,3两组实验:
可以看出和前面一组实验不一样,这次的两组函数虽然参数只是一个有cosnt限定,一个没const限定,生成的符号名却都不一样,能通过编译。这说明了什么?(看到&和*没)
实际上仅当形参是引用或指针时,const形参才对符号名有影响。(实际上我也是在Primer书上看到的,这里只是验证一下,要不然我的脑壳可想不到)
注意如果形参本身是const指针不是这种情况(这和第一组实验类型)
3.3.3 第4,5组实验:
注意const int*和int *const的区别。实际上int & const不存在,因为一个引用绑定到一个对象后,不可能再绑定到另一个对象。所以int & const在语法中是不需要存在的。
背景知识补充:常量指针(const
int*或者int const*)与指针常量(int * const)。
常量指针是指不可通过指针给该指针指向的变量赋值(即不可以修改该变量),但是可以改变该指针的指向。定义函数时,如果不想在函数总修改所指向的参数,可以把形参声明问常量指针。C/C++标准库函数就是这么做的。
指针常量是指不可以改变指针的指向,但能通过指针给该指针指向的变量赋值(即可以修改该变量)。
4. 这也是extern “C”的由来
分析到了这里,我们已经验证为了支持函数重载C++编译器的函数符号命名机制和C语言是不一样的。实际上C++的符号命名机制也适合全局变量。
所以然,如果你在C++中直接调用C语言编译的函数,链接时会找不到符号,发送符号未定义错误(undefined
reference to之类的错误)。下面验证一下:
我们在文件cfunctest.h中声明了两个函数,并在8.c中实现了这两个函数,然后用C语言编译器编译8.c生成目标文件8.1.o。然后在8.cpp调用这个两个函数。先用8.cpp生成8.2.o。然后尝试将8.1.o和8.2.o链接。
我们来编译并链接一下
可以看出虽然8.c和8.cpp虽然可以各自编译成功,但是链接到一起时候却链接不到要调用的函数符号。
我们来看看这两个函数在8.1.o和8.2.o中的符号各是什么:
可以看到两个函数在8.1.o和8.2.o中的符号名是不同的。当然链接不上啦。如果查看8.c和8.cpp对应的汇编代码,也会发现生成的符号是不同的。
如果我硬是要在C++代码中调用用C编译器编译的函数,那该怎么办呢?这时候该extern “C”登场了。之所以会出现链接错误,是因为C++在调用函数时候,把函数符号改名了,而且这种改名机制和C编译器的符号命名机制是不同的。所以我们要告诉C++编译器,在调用某个用C编译的函数时,不要用C++的符号命名机制,而是用C语言的符号命名机制。这就是extern “C”的功能。
把8.cpp中#include “cfunctest.h”改成extern “C”{#include
“cfunctest.h”}再试一下就行了。
在cpp文件中加上extern “C”后,重新编译,发现生成的目标文件8.2.o中函数的符号名和前一张图中8.1.o中的符号名相同了。链接也无错误了。
实际上你也可以直接在cpp文件把对应函数调用处的汇编代码call _Z6cfunc1v改成call cfuncv1,call _Z6cfunc2v改成call cfunc2(反正保证两个目标文件中的函数符号一样就行)再用这个汇编代码编译,也可以正确链接的。
至于C++重载函数的匹配规律,有那么一点点复杂。看书就好。我这里只做这些分析了。
5.参考资料:《C++ Primer中文版》第四版 第7.8节:重载函数
善良超哥哥的吐血之作,转载请注明出处:http://blog.csdn.net/candcplusplus/article/details/12746975