同事问我有关虚函数的问题,我张了张嘴,不自觉的冒出 继承,多态,后绑定, 这些词,脑子里反复的问自己是这样吗,这些名称能解释清楚什么是虚函
数吗。这不是一个简单的问题,显然短短几个专业术语是解释不清楚的。问了问google,看了好多篇关于虚函数的帖子,都不是我眼中的虚函数解释,不是说他们说得不对,而是觉得如果向一个不怎么了解虚函数的人解释,总觉少了些什么,特别是向 c 程序员解释,我觉得会让他越发的糊涂,到最后只能找个比较全的例子,照着代码敲上一遍,知道虚函数怎么实现,怎么访问。但是我想说的是什么是虚函数,至少我没看到让我觉得满意的解释。
1.1 函数签名
──────
在C++中调用C的函数需要用extern "C" 来修饰函数名,这样保证C++编译器在连接的时候能正确的找到对应的C函数。为什么?C与C++编译器在编译函数签名的办法上有些不同,比如函数: void func(){…},我们叫这个函数就叫func,但是C 编译器不这样叫,他有自己的一套比如叫_func(下划线函数名),而C++也有自己的一套比如叫?func@4。可以这样理解:一个苹果,程序员叫它苹果,C就叫树上的苹果,C++叫2两重的苹果,extern 的作用就是让C++使用C的叫法,为什么要这样做呢,因为函数func 已经被C用C的叫法编译过了,名字
已经被改成C叫法写进编译文件里了,也就是说在编译后的文件里已经没有一个叫func名称的函数了。基于以上解释,可以理解为什么C 没有函数重载,因为苹果目前只有树上才长,而C++可以叫3两重 4 两重…的苹果,虽然我们都叫苹果,但是C只有一个叫法,而C++有很多中叫法所以它有函数重载,具体点的解释就是函数func(int),func(float) 在c里面只有_func一种叫法,如果超出一个就会重名了。而c++可以这样来区别?func@int, ?func@float,所以他可以重载,其实所有的函数都是唯一的,只是我们只看函数的名字,而忽略了函数的参数而已,完整的函数命名包含了所有的这些信息。
1.2 多态是什么
───────
书上的解释很清楚了,简单点来说就是调用同一个函数,随着调用对象的不同,而导致最终调用的函数不同。复杂点来说,我相信进化论,我们的手是最完美的进化产物,如果用继承的观点来解释就是,我们的手可以有鱼鳍的功能划水,可以有猴子手的功能攀爬,可以有人手的功能扒饭,根据调用的对象不同而使用不同的功能。继承树是这样,鱼>猴子>人,到我们人这层,手的功能继承了前两个的函数,划水和攀爬,进化出新的功能扒饭。如果用代码解释可以参考下面
class Fish{
virtual void hand(Water w){…}
};
class Monkey:public Fish{
virtual void hand(Water w){…}
void hand(Tree t){…}
};
class Human:public Monkey{
void hand(Rice r){…}
};
鱼和猴子划水的姿势显然是不同的,所以划水的"手"需要被重新定义,我用virtual 来修饰鱼的hand,然后在猴子类中再重新定义让它有自己的一套。而人和猴子的划水姿势估计差不多,所以在不需要重新实现。想象一下,同样是函数"hand"他会根据调用的东西不同(水,树,米饭)而使用不同的实现是不是很神奇,但是具体神奇在那里呢或者说编译器是怎么做到的,来我接着说。
1.3 为什么会有接口这个东西
─────────────
可以这样理解接口,它是通往外面的通道,手段,方法…具体点来说就像银行的柜员,不管你是存款,贷款,取现等等直接找他就搞定多方便。其实函数就是接口,你可能听过什么接口类,不过就是一个专门提供接口函数的类。你可能注意到银行会有很多个柜员,但是怎么快速的找到自己想要的柜员呢?这里有个小瑕疵不过可以忽略,我们可以假设一下,假设每个柜员只负责办理一种业务你怎么能快速找到你想要的柜员。这个时候柜员机出现了,想想你去银行是不是需要那身份证去挂个号,什么个人业务,综合业务,企业业等等,出来一张字条,是不是告诉你需要在哪个窗口等待,柜员机出来之前,好像可以问问大堂经理,或者保安对吧。我觉得柜员机才是银行真正的对外接口,柜员是各个业务的子接口,接着再抽象一下银行是个类,柜员机是个对外接口(函数)而柜员就是虚函数,为什么是虚函数,而不是正常函数,或者是重载函数。解释一下,其实银行这个类也是可以继承的,比如现在有什么类似私人银行,基金银行,或者其他什么银行,里面的柜员所负责的业务肯定又些许不同,而在调用者(客户)又不能看出他们有啥不一样,
正常函数不能覆盖(覆盖了,继承过来的业务会有影响),只能重载(会让人看出不同)所以虚函数是最佳叫法。好了,所需要的元素都出現了,银行类,柜员机(接口),柜员(虚函数),具体过程就是,不管客户是什么人,到银行,找到柜员机,告诉办理什么业务,然后柜员机告诉具体柜员,最后到指定的柜员(虚函数)前办理业务。如果用代码表示可以这样
class Bank{
public: int guiyuanji(){返回柜员号};
virtual void guiyuan1(){};
virtual void guiyuan2(){};
virtual void guiyuan3(){};
};
试想把柜员机想象成一张表,里面放着业务和对应的柜员编号,你是编译器,然后有客户来办理业务,你会怎么处理回到上面一节,编译器是怎么实现的。虚函数会被放经一张表,然后会有一个柜员机(虚标指针)指向这个表,当客户端代码调用业务的时候,编译器会根据业务给客户一个编号代具体的表虚函数
vptr={存款:guiyuan1,取钱:guiyuan2,贷款:guiyuan3};
如这个代码
pBank->guiyuanji(存款,数量);
编译器会这样来处理,
pBank->vptr[存款](数量);
现在来解决多态,既然是多态,肯定涉及到继承,想象一下有个私人银行,他既办理普通一样的业务,比如天热吹个空调,天冷吹个空调,脚痛歇个脚之类,还负责办理私人的存款业务就是很特殊的那种(具体我也不清楚)反正跟普通银行不一样就对了。看代码
class PrivateBank:public Bank{
virtual void guiyuan1(){私人存款业务}
};
柜员机(虚表) vptr={存款:guiyuan1,取钱:guiyuan2,贷款:guiyuan3};
怎么跟上面一样 , 呃..对不起上面有些问题,这时需要将上一节的东西考虑进来,每个函数都是独一无二的,所以这个表要根据C++命名规则来,
vptr={存款:PrivateBank_guiyuan1,取钱:Bank_guiyuan2,贷款:Bank_guiyuan3};
上面估计就是vptr={存款:Bank_guiyuan1,取钱:Bank_guiyuan2,贷款:Bank_guiyuan3};
看出来了没,改了哪个函数,这张表就改变哪个这样编译器的处理就跟上面一样了你可能会问,多态在哪里呢?稍稍思考1秒,想象一下,你们家大少爷打算把一笔钱存进私人银行,然后给你一个银行地址让你这个佣人去存,你可能知道有哪些银行,也可能知道有哪些存款业务,可在你家少爷给你一个地址告诉你去存钱的时候,你是不知道这个是什么银行,什么业务的。只有到了银行,人家才会给你说这是xx业务。这就是所谓的后绑定,后在哪里? 我觉得把,你要是常常去银行肯定知道先拿号 在排队,然后再填表办业务。但此时的你知道又能怎么样,还不是要到了银行才知道自己在办理什么业务,这就是后。总结一下,虚函数就是可以重写的函数,不是重载哦,重写就是实现一个新的函数然后替换掉对应的虚表位置,编译器通过虚表指针查询到正真的函数执行调用,后绑定就是在真正函数调用的时候才会调用虚表中的函数,后还可以这样理解,一般的函数调用是编译器知道函数的地址,而虚函数调用的时候有个查表的操作,查完表才能调用,这不就后了嘛
参考
c++对象模型
https://www.cnblogs.com/raichen/p/5744300.html