C++实现函数重载的技术手段是函数符号改名,所以我们可以通过分析编译器的函数符号改名机制来验证C++函数重载规则。
(1)如何查看编译器改名后的函数名
在VS编译器下,我们可以从符号表中看编译器如何改名的。符号表是在.map文件里,在vs里默认不显示符号表文件。要想显示出来,这样设置:属性—>配置属性—>链接器—–>调试—>生成映射文件—>选择是。
//.cpp
int test() {
return 0;
}
int main() {
}
// 编译后,在exe文件下打开.map文件可以看到:
0002:00000710 ?test@@YAHX@Z 00411710 f test.obj
0002:00000740 _main 00411740 f test.obj
(tips:从这里还可以看到main函数之前前后还做了啥,这里就不展开了)
从上面可以看到test()被改名成?test@@YAHX@Z(这里要注意,这里是VS下的改名规则,不同平台可能不一样,还有当然对.cpp进行编译,如果对.c进行编译,会生成如下:)
// .c
0002:00000670 _main 00411670 f test.obj
0002:000006a0 _test 004116a0 f test.obj
// 可以看到C编译的时候,只在函数名前加_,所以不能重载。
现在来解释**?test@@YAHX@Z**的含义:
- ? 表示名称开始,后面是函数名
- @@YA 表示参数表开始,后面紧跟返回值和参数列表。这里的YA是__cdecl,而对于__stdcall是@@YG
- H表示返回值,X表示函数参数。(H表示int, X表示void,N表示double,M表示float, F表示short等
- Z表示结束
(2)重载的好处
-
C中没有重载,所以要对不同的函数取不同的名字,可能会造成名称污染
-
类的构造函数跟类名相同,如果没有重载机制,实例化不同对象比较麻烦
-
操作符重载,比如string +
(3)重载的决定因素
从上面改名规则可以看到,编译器根据函数的名称、返回值类型、参数类型、个数来进行重命名。
但是:函数返回值并不是重载的因素, 其原因在于函数返回值可以被忽略。
对于参数个数不同、类型不同一般很好理解,这里对有个特殊:
- 常函数const可以作为重载条件,因为常对象只能调用常函数
- 重载的概念要在同一个作用域内
- 参数为const* 和 const &形式是可以成为重载因素的,而单单const参数是无法形成重载决定因素的。
// const
int test(int i) // ?test@@YAHH@Z
int test(const int i) // ?test@@YAHH@Z
// const*
int test(int* i) // ?test@@YAHPAH@Z
int test(const int* i) // ?test@@YAHPBH@Z
int test(int* const i) // ?test@@YAHQAH@Z
// const&
int test(int& i) // ?test@@YAHAAH@Z
int test(const int& i) // ?test@@YAHABH@Z
(4) 重载优先级
-
第一步:创建候选函数列表,其中包含有与被调函数名称相同的函数与模板函数。
-
第二步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数。
-
第三步:确定是否有最佳可行的函数。如果有,则使用,重载要求最佳可行函数至少有一个参数的优先级是大于其他函数的,否则会产生二义性。
-
确定最佳函数,只考虑其特征标,而不考虑返回类型。确定最佳函数,匹配特征标要依次经过以下判断(1)完全匹配(常规函数优于模板;允许无关紧要的转换)
(2)提升匹配(如char和short自动转换为int)
(3)标准转换(int转换为char,long转换为double)
(4)用户自定义的转换(如类声明中定义的转换)
(5)允许无关紧要的转换,这些转换包括引用,指针与实体之间,数组与指针之间,函数与函数指针之间,const与非const等等。
int test(float i) { return 0; } int test(double i) { return 0; } int main() { test(1); } // error,这里产生二义性
-
(5) extern “C”
如果用C语言编译器编程生成1.c和目标文件1.o,然后再cpp中调用2.c中的函数,cpp生成2.cpp和2.o,则1.o和2.o链接时会发生错误。虽然1.c和2.cpp可以各自编译成功,但是链接一起的时候需要调用函数符号,而1.c中的函数符号不同,如果要用extern “C”。告诉编译器调用C中函数时,不要用C++的符号命名机制,而是要用C语言命名机制。
#ifdef __cplusplus
extern "C" {
#endif
int test() {
return 0;
}
#ifdef __cplusplus
}
#endif
// 然后test就不会进行改名,在.map文件里是这样的
0002:000006a0 _test 004116a0 f test.obj
参考资料
(1)https://blog.youkuaiyun.com/gogogo_sky/article/details/72807123
(2)https://www.cnblogs.com/invisible2/p/6204492.html