> 文章列表 > C++函数重载问题

C++函数重载问题

C++函数重载问题

1.函数重概念

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了。

函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。

2.函数重载判断

函数重载有三种情况:

  • 函数顺序不同
  • 函数类型不同
  • 函数参数个数不同

记住:如果只有函数返回值不同,不是函数重载;返回值不同,参数也不同的时候,可以作为函数重载。

看代码:

//函数重载
int Add(int x, int y)
{return x + y;
}
float Add(float x, float y)
{return x + y;
}
double Add(double x, double y)
{return x + y;
}
int main()
{int x = 10;int y = 10;int a= Add(1, 1);float b = Add(1.1, 1.2);double c = Add(1.111111112111111, 2.22222222222);cout << a << endl;cout << b << endl;cout << c << endl;}

上面三个函数,虽然函数名相同,但参数不同,所以为不同函数,这是函数参数不同。

int Add(int x, int y)
{return x + y;
}
int Add(int x, int y, int z)
{return x + y + z;
}int main()
{int x = 10;int y = 10;int a1= Add(1, 1);int a2 = Add(1, 2, 1);cout << a1 << endl;cout << a2 << endl;/*float b = Add(1.1, 1.2);double c = Add(1.111111112111111, 2.22222222222);*///cout << c << endl;}

这个是函数参数个数不同,导致的函数重载。

double Add(int x, double y)
{return x + y;
}
double Add(double x, int y)
{return x + y;
}int main()
{int x = 10;int y = 10;double a1= Add(1, 1.212111111);double a2 = Add(1.1111111111,1);cout << a1 << endl;cout << a2 << endl;/*float b = Add(1.1, 1.2);double c = Add(1.111111112111111, 2.22222222222);*///cout << c << endl;
}

这是函数参数顺序不同导致的函数重载。

3.C++支持函数重载的原理

为什么C++支持函数重载,而C语言不支持函数重载呢?
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接

C++函数重载底层原理是基于编译器的 name mangling 机制。

编译器需要为C++中的所有函数,在符号表中生成唯一的标识符,来区分不同的函数。而对于同名不同参的函数,编译器在进行name mangling操作时,会通过函数名和其参数类型生成唯一标识符,来支持函数重载。

注意:name mangling 后得到的函数标识符与返回值类型是无关的,因此函数重载与返回值类型无关。

比如,下面的几个同名函数Add

int    Add(int i)           { return 0;     }
float  Add(int i, float f)  { return i + f; }
double Add(int i, double d) { return i+d;   }

在经过编译中的name mangling操作后,得到的符号表中和Add有关的如下:

$ g++ main.cc -o main.o && objdump -t main.o
main.o:     file format elf64-x86-64SYMBOL TABLE:
0000000000001157 g     F .text  000000000000001c              _Z3Addid
000000000000113b g     F .text  000000000000001c              _Z3Addcif
0000000000001129 g     F .text  0000000000000012              _Z3Addci
0000000000001173 g     F .text  0000000000000016              main
...

其中, 前缀 _z 是GCC的规定,3 是函数名Add的字符个数,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 Add(int val) {return 0; }

经过name mangling后得到的标识符是Add

$ gcc name.c -o name.o && objdump  -t name.o | grep Add
0000000000001129 g     F .text  0000000000000012              Add

这就导致了C编译器不支持函数重载。