面试题 | C++的函数重载返回值类型有关吗,是怎么实现的?
前一期【走近vtpr、vtbl,揭秘动态多态】讲解了基于虚函数实现的动态多态,并且深入剖析了动态绑定在编译期、执行期各自完成的任务。本期继续讲解基于函数重载实现的静态多态。
函数重载
重载(overload),允许多个同名函数,而这些函数的参数列表不同,具体的参数类型,在编译期间就能确定。
name mangling
C++函数重载底层原理是基于编译器的 name mangling
机制。
编译器需要为C++中的所有函数,在符号表中生成唯一的标识符,来区分不同的函数。而对于同名不同参的函数,编译器在进行name mangling
操作时,会通过函数名和其参数类型生成唯一标识符,来支持函数重载。
注意:name mangling
后得到的函数标识符与返回值类型是无关的,因此函数重载与返回值类型无关。
比如,下面的几个同名函数func
:
int func(int i) { return 0; } float func(int i, float f) { return i + f; } double func(int i, double d) { return i+d; }
在经过编译中的name mangling
操作后,得到的符号表中和func
有关的如下:
$ g++ main.cc -o main.o && objdump -t main.o main.o: file format elf64-x86-64 SYMBOL TABLE: 0000000000001157 g F .text 000000000000001c _Z4funcid 000000000000113b g F .text 000000000000001c _Z4funcif 0000000000001129 g F .text 0000000000000012 _Z4funci 0000000000001173 g F .text 0000000000000016 main ...
其中, 前缀 _z
是GCC的规定,4
是函数名func
的字符个数,i
表示第一个函数的参数类型int
,f
是第二个函数的参数类型float
,而d
表示参数类型是double
。经过 name mangling
后可以发现,函数重载与返回值类型无关,仅与函数名和函数参数类型相关。
类成员函数重载
看完上面的讲解,你心中可能仍有疑问,返回值类型真的不能表征函数重载吗?
如果不能,那为啥在std::vector
中,对于begin()
函数,既返回了iteraotr
类型,也返回了const iterator
类型,编译器还没有报错,而且这种现象在STL的容器中几乎随处可见。
// from stl_vector.h iterator begin() noexcept { return iterator(this->_M_impl._M_start); } /** Returns a read-only (constant) iterator that points to the * first element in the vector. Iteration is done in ordinary * element order. */ const_iterator begin() const noexcept { return const_iterator(this->_M_impl._M_start); }
在回答这个问题之前啊,先讲解下编译器是怎么对类的成员函数进行转换、编译的。
如下demo,类Number
有成员函数add
,这个成员函数经过编译器转换后是什么样子?
class Number { public: Number(int val=0):val_{0} {}; int val() {return val_;} const int val() const {return val_;} void setVal(int val) { val_ = val;} void add(const Number& rhs) { val_ += rhs.val_; } private: int val_{0}; };
实际上,编译器会将所有的成员函数转换为C-Style的函数。为了实现这一操作,就需要将在add
函数的第一个参数前插入this
指针:
// _ZN6Number3addERKS_ 是经过 name mangling 后的唯一标识符 void _ZN6Number3addERKS_(Number* this, const Numer* rhs) { this->val_ += rhs.val_; }
同理,setval
函数也会被转换为:
// _ZN6Number6setValEi 是经过 name mangling 后的唯一标识符 void _ZN6Number6setValEi(Number* this, int val) { this->val_ = val; }
对于setval
函数,其中,_ZN
是固定前缀,6Number
表示Number
有6个字符,3add
表示add
有3个字符,E
我理解为Extral
(额外的意思,即this指针,需要额外插入),i
则表示参数类型int
。
对于add
函数经过 name mangling
后,R
表示引用(Reference ),K是啥单词缩写不清楚。
简而言之:
- 每个成员函数,都会在第一个参数前插入一个this指针,将成员函数转换为非成员函数;
- 每个成员函数,经过
name mangling
转换后,都会生成唯一的标识符,并且这个标识符只与函数名与函数参数类型有关。
此时,你就能理解,为什么Number
类中,val()
函数能有两个重载:val()
函数能重载不是依赖于返回值类型不同,仍然是依赖于参数类型不同,因为在经过编译器插入this指针后,他们会变成:
// _ZN6Number3valEv 、_ZNK6Number3valEv 是 val 经过 name mangling 后的唯一标识符 _ZN6Number3valEv(Number* this); _ZNK6Number3valEv(const Number* this);
Number
的各个成员函数经过 name mangling
后的结果:
0000000000001248 w F .text 0000000000000027 _ZN6Number3addERKS_ // add 000000000000122c w F .text 000000000000001b _ZN6Number6setValEi // setval 0000000000001218 w F .text 0000000000000014 _ZN6Number3valEv // val 00000000000011fc w F .text 000000000000001c _ZN6NumberC1Ei // construct 00000000000011fc w F .text 000000000000001c _ZN6NumberC2Ei
注意:不必过于关注name mangling
本身,只需要知道name mangling
这个机制是一套命名规则,为每个函数生成唯一的标识符即可,不必研究规则本身,是怎么命名的,每个单词是啥缩写,大致了解即可,不同的编译器规则都可能不同。
相信,到此,你应该明白了函数重载怎么回事:只依赖于函数名及其参数类型,与返回值类型无关!!!
by the way
最后,顺便提下,为什么C编译器不支持函数重载?或者说,在C++环境中调用C的代码并且要求编译器按照C的编译风格来编译这部分代码时,要加上"extern C"
?
就是因为C编译器的name mangling
规则与C++的不同:C语言的命名规则仅依赖于函数名,和函数参数类型无关。比如:
int func(int val) {return 0; }
经过name mangling
后得到的标识符是func
:
$ gcc name.c -o name.o && objdump -t name.o | grep func 0000000000001129 g F .text 0000000000000012 func
这就导致了C编译器不支持函数重载。
看完本期,相信你能对函数重载有了更为深刻且本质的认知,如果觉得写的不错就点个赞吧。
#面试题目#