欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

我眼中的虚函数

程序员文章站 2022-11-08 13:07:22
同事问我有关虚函数的问题,我张了张嘴,不自觉的冒出 继承,多态,后绑定, 这些词,脑子里反复的问自己是这样吗,这些名称能解释清楚什么是虚函 数吗。这不是一个简单的问题,显然短短几个专业术语是解释不清楚的。问了问google,看了好多篇关于虚函数的帖子,都不是我眼中的虚函数解释,不是说他们说得不对,而 ......

同事问我有关虚函数的问题,我张了张嘴,不自觉的冒出 继承,多态,后绑定, 这些词,脑子里反复的问自己是这样吗,这些名称能解释清楚什么是虚函

数吗。这不是一个简单的问题,显然短短几个专业术语是解释不清楚的。问了问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