【c++笔记二】重载(overload)之一看你就懂

2015年1月22日    晴
    寒假第二天,程序猿第二天,减肥第二天(体重稳定 害羞)。
    今天学的是 重载(overload),c中没有c++开始使用的。话不多说,直奔主题!
——————————————————————————华丽的分割线——————————————————————————————

一.什么是重载?

    同一作用域中,函数名相同,参数列表不同的函数,就叫做重载。

    从定义中,我们可以发现函数重载的几个条件:

  1. 同一作用域
  2. 函数名相同
  3. 参数列表不同

    三个条件缺一不可!!!

    我们先来看个例子:求两个数中最大的数

#include <iostream>
using namespace std;
int getmax(int x,int y){
    cout<<"getmax(int,int)"<<endl;
    return x>y ? x : y;
}
double getmax(int x,double y){
    cout<<"getmax(int.double)"<<endl;
    return x>y ? x: y;
}
int main()
{
    getmax(1,2);
    getmax(1,2.3);
    return 0;
}
    看看输出的结果是什么样的:

    由上面的代码我们可以知道,两个getmax函数构成了重载,这在c语言中是不允许的。

    重载给我们带来了很大的方便,如果是在c语言中,我们要编写求两个数最大的数的值的话,两个int类型的要区一个函数名,两个double类型的又要取一个函数名。如果类型各种各样,你就得取成百上千的函数名,太麻烦了。

    c++就很好的做到了这一点,通过重载适配各种参数类型,给编程带来极大方便。

    我们来分析一下上面那段代码:

    getmax(int,int)和getmax(int,double)两个函数,都属于全局函数,所以满足条件一:同一作用域

    两个函数的名字都相同,都是getmax,满足条件二:函数名相同

    第一个getmax的参数列表是int和int,第二个getmax的参数列表是int和double,他们的参数列表第二个参数类型不同,满足第三个条件:参数列表不同

    由此可见,重载的三个条件是与关系,缺一不可!

    我相信细心的人会发现,这两个getmax函数的返回值类型不一样(一个是int,一个是doubl),这样也算重载吗?

    Of course!只要满足上述三个条件,管你什么返回值类型,都属于重载!


二.怎样才算参数列表不同?

    有人会说,getmax(int x,int y)和getmax(int a,int b)这两个函数构成了重载,因为一个函数的参数叫x和y,另一个叫a和b。

    我们说的参数列表不同,不是说形参名不同。

    具体的参数列表不同表现在三个方面:

  1. 参数类型不一样。
  2. 参数个数不一样。
  3. 参数顺序不一样。
    只有满足上述三个条件的一条或一条以上,才算作参数列表不同。
    同样的我们来用代码说话:
#include <iostream>
using namespace std;
int getmax(int x){
    cout<<"getmax(int)"<<endl;
    return x;
}
int getmax(int x,int y){
    cout<<"getmax(int,int)"<<endl;
    return x>y ? x : y;
}
double getmax(int x,double y){
    cout<<"getmax(int,double)"<<endl;
    return x>y ? x: y;
}
double getmax(double x,int y ){
    cout<<"getmax(double,int)"<<endl;
    return x>y ? x: y;
}
int main()
{
    getmax(1,2);
    getmax(1,2.3);
    getmax(1);
    getmax(1.23,5);
    return 0;
}

    我们把getmax函数写了四个,每个都不一样,但是都构成 了重载。
    第一个getmax只有一个参数(int),而第二个getmax有两个参数(int,int),满足条件二: 参数的个数不同
    第二个和第三个参数列表中的第二个参数类型不同,第一个是int y,第二个是double y。满足条件一: 参数的类型不同
    第三个和第四个参数虽然都是一个int一个double,但是int 和 double 的位置不一样。第三个函数的int在前,double在后;第四个函数的double在前,int在后。满足条件三: 参数的顺序不同。
    由此可见,参数列表不同的三个条件是或关系,满足之一即可!


三.重载的原理是什么?

    先给大家看一张图哦:

    重载的原理其实很简单,两个字: 换名
    c++的编译器会给每个函数名取不一样的名字,同样是foo函数,编译器分别给他们取名叫做_Z3foov()._Z3fooi,_Z3food。实际上他们就是三个不一样的函数了。
    而c编译器可不会这样,你给函数取什么名字就是什么名字,这就是为什么c中没有函数重载的概念!
    怎么来验证我说的对不对呢?我们来看源程序翻译成汇编后的汇编代码(一般编译器做不到,谁叫我们学unix 的,上gcc编译器!):

我们先来看c语言编写的函数源代码(overload.c):

gcc overload.c -S,生成汇编文件(overload.s),我们来看汇编文件里面有什么:

没学过汇编的第一眼看上去肯定会说,什么gui!我也没学过,我也看不懂。但我知道第5行,是foo子函数的入口;第22行是main函数的入口;第31行是调用(call)foo函数!
由此可见,在c编写的程序中,函数名是什么就是什么,编译器不会帮你修改的。
那我们现在再来看看c++写的程序会是什么样的。先看overload.cpp:

g++ overload.cpp -S生成汇编文件(overload.s):

这里就有点不一样了。第七行是foo函数的入口(已经被编译器换名为_Z3fooi);第24行是main函数的入口(main函数是不会改名的,所以 main函数没有重载!),33行是调用foo函数(call _Z3fooi).

    看了foo函数,同样用c和c++写,他们底层实现是定点区别显而易见。c++编译器,会将foo函数改名为_Z3fooi,而c编译器还是直接使用foo。
    之所以能够重载,是因为c++将同一个函数名换名成不同的函数名,实际上调用的是不同的函数。
    换名其实是有规律的:前缀"_Z"是固定的,之后的数字代表函数名有几个字符,比如foo为3个字符,所以是_Z3.
    之后跟上原来的函数名,最后加上参数的类型缩写(int 就是 i,double就是d,char就是c,void就是v,等等)。
    换名后的函数名的规律就是: _Z + 函数名的字符个数 + 原函数名 + 参数列表的缩写
    举几个例子: getmax(int x,double y)换名后就是_Z6getmaxid
                             getmax(double x,double y)换名后就是_Z6getmaxdd
    这样就很容易解释重载成立的三个条件了。如果你函数名不一样,_Zx后面的函数名就不一样;如果你参数列表是一样的,id和id就会一样,最后还是同一个函书名不能构成重载。

四.换名带来的问题

    万事都有两面性。换名自然可能出现某些问题,而这些问题一般发生在跨语法,跨编译器上的。
    什么意思呢?还是举例说明来的实在。
    在多文件编程中,我用c语言实现了某一个函数功能,做成一个库文件(以 .o 文件为例)供c++程序来调用这个函数。(不懂的自己去学怎么用vi写代码,怎么用gcc编译器哦)
getmax.c:
#include  <stdio.h>
int getmax(int x,int y){
    printf("getmax(int,int)\n");
    return x>y ? x : y;
}
    这个getmax.c文件很简单,只有一个函数(因为只是做库文件,就不需要什么主函数了)。
    gcc getmax.c -c 生成 getmax.o文件已备接下来的main.cpp文件调用哦。
main.cpp:
#include <iostream>
using namespace std;
int getmax(int x,int y);
int main()
{
    getmax(1,2);
    return 0;
}
    让我们来向做多文件编程时一样的调用getmax.o文件:g++ main.cpp getmax.o。结果编译器报错了:

    注意方框内的提示,getmax未定义!!!为什么呢?我明明在main.cpp的第三行先声明了外部的getmax函数以便接下来的使用,而且编译的时候也链接了getmax.o文件!
    如果我们同时使用的是c++或者c编写两个源程序,肯定运行不会出错。
    问题出就出在,getmax这个函数我是用c风格写出来的,而我却在c++的代码中调用了他!这就是我们上面说道的换名在跨语言带来的错误。(虽然c++兼容c,但其实这是两种语言)
    为什么呢?让我们回顾重载的原理—— 换名
    c编写的getmax,编译器不进行换名,它还叫做getmax。在c++中我们声明了getmax,但是c++编译器将它换名为_Z6getmaxii。你在调用的时候,实际上是在调用_Z6getmaxii,而这里根本没有_Z6getmaxii的定义,只有getmax,所以编译出错了!

    那么解决办法是什么呢?
    在c++的函数前加上 extern "C" 即可,c++编译器就会像c编译器那样对待函数,不对它进行换名,也就从根本上杜绝了重载!
修改后的main.cpp:
#include <iostream>
using namespace std;
extern "C" int getmax(int x,int y);
int main()
{
    getmax(1,2);
    return 0;
}

    一切都和我们想像的一样美好了!所以,各位一定要注意在跨编译器或者跨语言编写的函数之间相互调用时,换名可能带来的问题哦!


五.用函数指针调用重载函数

    学了c/c++的同学们,对函数指针一定不陌生。
    比如我定义了一个函数:
int max(int x){
    return x;
}
    那么我们定义函数指针来调用它就是这样的:
int (*pm) (int x) = max;
int a = (*pm)(3);
    关于函数指针我就不罗嗦了,反正学了的人都会觉得函数指针是个很好用的东西。
    其实重载函数的函数指针也是一样的,虽然大家函数名都一样,但是参数列表很好的区分了他们之间的不同。
    看代码:
#include <iostream>
using namespace std;
int getmax(int x){
    cout<<"getmax(int)"<<endl;
    return x;
}
int getmax(int x,int y){
    cout<<"getmax(int,int)"<<endl;
    return x>y?x:y;
}
int main()
{
    int (*p1) (int x) = getmax;
    int (*p2) (int x,int y) = getmax;
    (*p1)(1);
    (*p2)(1,2);
    return 0;
}

    函数指针p1和p2都是通过getmax赋值的。但是调用的结果完全不一样,因为这两个函数指针在定义的时候的参数列表就不一样。
   So,你应该知道怎么用重载函数的函数指针了吧?


六.重载的解析

    其实这一部分我也只是一知半解,还不是完全懂。希望各路大神指点一二!
    所谓的重载的解析呢,就是说,你在调用各种重载了的函数时,编译器怎么判断去用哪个函数呢?这就是重载解析要去解决的问题啦。
比如:
#include <iostream>
using namespace std;
void foo (char* p, int n) {
	cout << "foo(char*,int)" << endl;
}
void foo (const char* p, char c) {
	cout << "foo(const char*,char)" << endl;
}
void bar (char c) {
	cout << "bar(char)" << endl;
}
void bar (int n) {
	cout << "bar(int)" << endl;
}
void bar (long long l) {
	cout << "bar(long long)" << endl;
}
void hum (double f, ...) {
	cout << "hum(double,...)" << endl;
}
void hum (int n, void* p) {
	cout << "hum(int,void*)" << endl;
}
int main (void) {
	char* p;
	char c;
	foo (p, c);
	short s;
	bar (s); 
	double f;
	void* q;
	hum (f, q); 
	return 0;
}

    可能看到结果,你有很多疑问。我们一点一点来分析:
    1.foo(p,c)调用的为什么是foo(const char×,char)?
        p是char*型 的,明显和foo(char*,int)的第一个参数更加匹配。但是c是char,又明显和foo(const char×,char)的第二个参数匹配?那我到底用哪个?编译器有自己的一套评分细则,它觉得, 常量转换(把const转换成非const比)比 升级转换(把char变为int)更加好!所以,我要用第二个foo。
    2.bar(s)调用的为什么是bar(int)而不是 bar(double)呢?
        同1,编译器觉得的把short(2字节)转换成int(四字节)明显比转换成double(八字节)更加省事,也就是 升级转换>标准转换
    3.hum(f,q)为什么调用的是hum(int,void*)?
        同1,f虽然为double,跟hum(double f, ...)的参数一更加匹配,但是编译器认为 标准转换省略号匹配更加好。

    综上,我们有一个规则: 完全匹配>常量转换>升级转换>标准转换>自定义转换>省略号匹配



结束语:
    好了,今天的笔记就记到这里了。菜鸟写的笔记果然就是这样没有深度啊 委屈 委屈 委屈
    如果大家有什么疑问,欢迎留言!
    如有不对的地方,望大神不吝赐教!

再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见








评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值