面试题 | 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表示第一个函数的参数类型intf是第二个函数的参数类型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编译器不支持函数重载。

看完本期,相信你能对函数重载有了更为深刻且本质的认知,如果觉得写的不错就点个赞吧。

#面试题目#
全部评论

相关推荐

11-09 11:01
济南大学 Java
Java抽象带篮子:外卖项目真得美化一下,可以看看我的详细的外卖话术帖子
点赞 评论 收藏
分享
想去夏威夷的小哥哥在度假:5和6才是重点
点赞 评论 收藏
分享
7 6 评论
分享
牛客网
牛客企业服务