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