面试题 | 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编译器不支持函数重载。
看完本期,相信你能对函数重载有了更为深刻且本质的认知,如果觉得写的不错就点个赞吧。
#面试题目#