送给新手:关于函数覆盖与虚函数~~

Enter空格 2011-10-15 12:20:33
首先来明确一个概念,函数名即地址,也就是说函数名就是个指针。

编译阶段,编译器为每个函数的代码分配一个地址空间并编译函数代码到这个空间中,函数名就指向这个地址空间。

也即每个函数名都有自己唯一的代码空间。

同理,类的成员函数也是如此。

但是,有一点大家一定要记住,C++编译器编译CPP文件时,会根据"C++编译器的函数名修饰规则" 对函数名进行修饰。
(修饰规则大家自己去搜吧,我就不叙述了),前面讲到函数名称的作用是指向函数真实代码的指针。
知道了以上规则,那么我们对函数覆盖便不难理解了。

首先来看看百度百科中函数覆盖的中文描述是:
函数覆盖发生在父类与子类之间,其函数名、参数类型、返回值类型必须同父类中的相对应被覆盖的函数严格一致,
覆盖函数和被覆盖函数只有函数体不同,当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,
而不是父类中的被覆盖函数版本,这种机制就叫做函数覆盖。

我们来写一段函数覆盖的代码
class father
{
public:

void fun()
{cout<<"father's fun"<<endl;}

};

class son:public father
{
public:
void fun()
{cout<<"son's fun"<<endl;}
};

void main()
{
father Father,*pFather;
son Son,*pSon;
int i = sizeof(Father); //此行代码过后,i=1,此行代码无意义只是让大家知道普通成员函数不占用类的实例空间。

// Father.fun(); //此行注释与下行注释是正常的调用函数覆盖相信大家都能理解,所以不在解释
// Sun.fun();
pSon = (son*)&Father; //子类指针指向父类实例是危险的,此例中并没涉及到任何越界,并且为了展示区别
//所以才这样使用,但大家要明白,这样做是危险的。
pFather = (father*)&Son;
pSon->fun(); //函数调用执行了father's fun
pFather->fun(); //函数调用执行了son's fun
}

此时有些人可能就不能理解为什么会出现这种调用结果了,那么大家是否还记得我上面曾提到的"C++编译器的函数名修饰规则"?
我们来根据"C++编译器的函数名修饰规则"再来想想原因。
根据规则,编译器把父类father中的fun函数名编译为"?fun@father@@QAEZ",子类son中的fun函数名编译为"?fun@son@@QAEZ"。
当pSon->fun();调用时,编译器会把pSon所存地址值的类型转化成当前指针类型,而pSon的当前类型为son(这句话不多余,
因为类型是可以随意转换的),
所以表达式"pSon->fun()"全部展开以后得到的函数名称即"?fun@father@@QAEZ"
同理表达式"pFather->fun()"全部展开以后得到的函数名称即"?fun@son@@QAEZ",
既然得出了函数名,那么也就可以根据函数名称,跳转到真实的函数代码实体位置了。


根据以上分析,我觉得"函数覆盖(英文名不知到叫什么只能用中文了)"这个词汇真的容易把人带入歧途,
我的语文又不好,所以还希望哪位语文好的兄弟,来重新翻译下"函数覆盖"这个词汇。
呃,真费劲啊,函数覆盖算是讲完,不知道大家有每有看懂,如果还看不懂的话,我是真没招了。
下面在来讲个虚函数吧。



有了前面的基础,大家应该对函数有了充分的了解。
那么问大家一个问题,为什么一个类实力化后普通成员函数不影响实例的大小?
呵呵,如果一个新手能回答出来,那我这篇东西就不算白写。
正确答案嘛,是因为不需要它来影响实例大小,因为编译器会根据"C++编译器的函数名修饰规则"与"表达式的地址类型",
自动的把成员函数展开成完整的函数名,也就找到了函数的真实地址,所以普通成员函数是不影响实力大小的。

我再来问大家一个问题,你们认为虚函数需不需要影响类的实例大小?
哈哈,这次的答案是需要。
这次我们来写一段虚函数的代码瞧瞧 因为只有在实例内部添加一个指针,才能够完成例如,

class father
{
public:
virtual void fun()
{cout<<"father's fun"<<endl;}

};

class son:public father
{
public:
void fun()
{cout<<"son's fun"<<endl;}
};

void main()
{
father Father,*pFather;
son Son,*pSon;
int i = sizeof(Father); //此行代码过后,i=4,此行代码无意义只是让大家知道虚函数占用类的实例空间。

Father.fun(); //函数执行结果 "father's fun"
Son.fun(); //函数执行结果 "son's fun" 这句简单点说,就是很多人说的动态绑定,我们下面会具体分析。

pSon = (son*)&Father; //再次强调,子类指针指向父类实例是危险的,一旦越界操作,就会引发异常。
pFather = (father*)&Son;
pSon->fun(); //函数执行结果 "father's fun"
pFather->fun(); //函数执行结果 "son's fun" 这两句也是动态绑定,相信还是有不少人不理解,下面具体分析。
}

恩,大家需要调式一下上面的程序,对Father添加监视,你会发现Father实例中莫名其妙的多了一个vftable类型指针对象vfptr。
是了,虚函数的实现靠的就是这个东西了。
IED帮我们在我们的实例外部实例化了一个vftable对象(我知道这句话很绕,但是我不知道该怎么更好的解释了),
同时为我们的实例Father添加了一个指向vftable对象的指针vfptr。
我们继续把监视中的指针vfptr展开,可以看到一个叫[0]的函数指针(别问我这名为啥长成这样,我也没搞清楚),
哈哈,找到了,这里存储了一个地址,这个地址就是一个函数真实地址(如果用的VS2010编译器,你会直观的看到这个地址
所对应的是father::fun这个函数)。
然后,我再来明确一个虚函数规则,就是当你的实例调用虚函数时,最终调用的就是这个vftable类型的成员[0]所存储地址。
那么好了,我们知道子类是完全继承父类的,所以那个vftable类型指针对象vfptr也同时被继承了下来。
IDE同样为我们实例化一个vftable对象,让vfptr来指向这个vftable对象。
而如果我们的子类重写了虚函数,那么IDE在实例化vftable对象时,就会把[0]这个指针重写为新的子类中那个虚函数地址。
如果我们的子类没有重写这个虚函数,那么IDE就会找到距离这个子类关系最近的一个实现了虚函数的父类,
把这个父类中的虚函数地址,写入到子类的[0]中。
这样子类在调用虚函数的时候,就可以实现动态绑定了。
另外父类指针指向子类实例时,因为有了vfptr指针占位,所以当父类指针调用虚函数时,寻址到的vfptr是子类实例的。
而子类的vfptr指向子类自己的vftable对象,所以父类最终调用的会是子类对象的中[0],所以[0]中存的是哪个函数地址。
父类指针最终调用的就会是哪个函数了。


呼~~~~长处一口气,终于算是写完了,看不看的懂就看你的造化了,其他所谓重载等,我就不将了,如果你能看懂此文
那些东西自然就好理解了(也是我懒,也很意外怎么坚持写完这贴)。
本次讲解最终解释全归CSDN会员mymixing所有。
看过此贴所引发的任何后遗症,本人盖不负责。

另外如果有哪位高手强忍着看完此贴,如果有遗漏或者错误,还请指出,我也好加深学习。










...全文
2740 100 打赏 收藏 转发到动态 举报
写回复
用AI写文章
100 条回复
切换为时间正序
请发表友善的回复…
发表回复
赵4老师 2014-07-09
  • 打赏
  • 举报
回复
《深度探索C++对象模型》 《C++反汇编与逆向分析技术揭秘》
liu254773606 2014-07-09
  • 打赏
  • 举报
回复
引用 94 楼 jianwen0529 的回复:
[quote=引用 93 楼 jianwen0529 的回复:] [quote=引用 91 楼 liu254773606 的回复:] [quote=引用 18 楼 supermegaboy 的回复:] [Quote=引用 11 楼 mymixing 的回复:] 马上要出门了,等了20多分钟终于还是没有等来各位星星所指出的错误原因。 不得不说,对CSDN确实很失望。 如果是在看雪发表一篇技术文(当然不是我发表,没那水平)。 如果文章有误,那些高手是会指出错误的理由,并交流改进方法的。 而那些新手一般也都会回文表示一下。 但在这里,我努力写出的一篇经验之谈,得到的貌似只有诸如 "看了第一行就。。。看不下去了。"这样的回答 实在让人失望。 我只…… [/Quote] 不好意思,刚才外出了。 在这里我指出你文章中比较严重的错误,无所谓的细节就不说了: 一、不能说函数名就是个地址,也不能说函数名就是个指针。看样子你应该不知道从函数到指针的转换规则。指针仅仅是函数名在表达式中的行为,指针仅仅是函数名在表达式中的有条件转换结果,但非其本质。函数名的本质是什么?很简单,就是函数实体的名字,是函数实体的抽象。 二、你举的百度百科中的函数覆盖的例子是错误的,那个例子所展现的现象不是覆盖,而是隐藏,子类函数隐藏了父类函数,只有存在虚函数才存在覆盖,覆盖的英文名称叫override,而隐藏叫hide。而且,关于覆盖的条件的描述也是存在错误的,覆盖并不要求连返回值类型都严格一致,覆盖允许返回值协变。 三、关于虚函数的实现,虚函数的实现并没有标准化,表驱动方式仅是实现的方法之一(虽然大多数实现都采用这种方式),类对象模型的实现也可以完全不使用表驱动方式去实现,因此,你在文中所提的这个问题: “我再来问大家一个问题,你们认为虚函数需不需要影响类的实例大小? 哈哈,这次的答案是需要” 正确答案是不需要,vtbl并不是要求的,vtbl仅是充分条件,不是必要条件。
第2点肯定是写错了的,覆盖的条件是“三同”, 所谓三同就是同函数名称,参数类型、个数相同,返回值也相同。除此之外的都可以称为隐藏。 第三点持怀疑,如果没有虚表,何来的多态性一说。反过来说就一定有虚表,有了虚表肯定影响类的实例大小[/quote] 覆盖的条件是“三同”, 所谓三同就是同函数名称,参数类型、个数相同,返回值也相同 返回值不作为参考吧?!你也不看看现在你说了四个,哪来的三同?![/quote] 况且还有个条件基类必须为virtual函数。[/quote] 脑子热了一下,不好意思,纠正下,三同指函数名相同,参数类型相同,参数个数相同
liu254773606 2014-06-27
  • 打赏
  • 举报
回复
引用 18 楼 supermegaboy 的回复:
[Quote=引用 11 楼 mymixing 的回复:] 马上要出门了,等了20多分钟终于还是没有等来各位星星所指出的错误原因。 不得不说,对CSDN确实很失望。 如果是在看雪发表一篇技术文(当然不是我发表,没那水平)。 如果文章有误,那些高手是会指出错误的理由,并交流改进方法的。 而那些新手一般也都会回文表示一下。 但在这里,我努力写出的一篇经验之谈,得到的貌似只有诸如 "看了第一行就。。。看不下去了。"这样的回答 实在让人失望。 我只…… [/Quote] 不好意思,刚才外出了。 在这里我指出你文章中比较严重的错误,无所谓的细节就不说了: 一、不能说函数名就是个地址,也不能说函数名就是个指针。看样子你应该不知道从函数到指针的转换规则。指针仅仅是函数名在表达式中的行为,指针仅仅是函数名在表达式中的有条件转换结果,但非其本质。函数名的本质是什么?很简单,就是函数实体的名字,是函数实体的抽象。 二、你举的百度百科中的函数覆盖的例子是错误的,那个例子所展现的现象不是覆盖,而是隐藏,子类函数隐藏了父类函数,只有存在虚函数才存在覆盖,覆盖的英文名称叫override,而隐藏叫hide。而且,关于覆盖的条件的描述也是存在错误的,覆盖并不要求连返回值类型都严格一致,覆盖允许返回值协变。 三、关于虚函数的实现,虚函数的实现并没有标准化,表驱动方式仅是实现的方法之一(虽然大多数实现都采用这种方式),类对象模型的实现也可以完全不使用表驱动方式去实现,因此,你在文中所提的这个问题: “我再来问大家一个问题,你们认为虚函数需不需要影响类的实例大小? 哈哈,这次的答案是需要” 正确答案是不需要,vtbl并不是要求的,vtbl仅是充分条件,不是必要条件。
第2点肯定是写错了的,覆盖的条件是“三同”, 所谓三同就是同函数名称,参数类型、个数相同,返回值也相同。除此之外的都可以称为隐藏。 第三点持怀疑,如果没有虚表,何来的多态性一说。反过来说就一定有虚表,有了虚表肯定影响类的实例大小
lm_whales 2014-06-27
  • 打赏
  • 举报
回复
关于析构函数: delete 父类的指针的时候如果父类析构函数,不是虚析构函数 --------虚析构函数。。会被子类自动继承为,虚函数 即有虚析构函数的类,子类析构函数自动为虚函数。--------- 那么,只能delete 指针本身类型的对象 ---------即子类的析构函数不会执行只会执行父类的析构函数。 如果父类有虚析构函数,那么delete父类指针 的时候, 实际调用的是子类的析构函数父类的析构函数,会在子类析构函数调用过程中 -----------直观看到的“子类析构函数代码”,执行结束-------------, 被调用。 除非发生异常,任何情况下,子类析构的时候,就会同时在最后阶段,析构相应的父类对象。 也就是说,父类的析构函数一定会调用。 除非 1)对象的构造没有完成----例如,构造函数发生异常----- 2)或者以后某个阶段,包括对象析构过程中,发生异常,或者直接 exit等等 。。。代码跑掉了,没有完全执行。。
上上之杰 2014-06-27
  • 打赏
  • 举报
回复
看不懂标记一下
wangen12345 2014-06-27
  • 打赏
  • 举报
回复
引用 1 楼 supermegaboy 的回复:
后面的还没看,看第一行就已经错了。
错了吗,你会不会C++
幻夢之葉 2014-06-27
  • 打赏
  • 举报
回复
引用 93 楼 jianwen0529 的回复:
[quote=引用 91 楼 liu254773606 的回复:] [quote=引用 18 楼 supermegaboy 的回复:] [Quote=引用 11 楼 mymixing 的回复:] 马上要出门了,等了20多分钟终于还是没有等来各位星星所指出的错误原因。 不得不说,对CSDN确实很失望。 如果是在看雪发表一篇技术文(当然不是我发表,没那水平)。 如果文章有误,那些高手是会指出错误的理由,并交流改进方法的。 而那些新手一般也都会回文表示一下。 但在这里,我努力写出的一篇经验之谈,得到的貌似只有诸如 "看了第一行就。。。看不下去了。"这样的回答 实在让人失望。 我只…… [/Quote] 不好意思,刚才外出了。 在这里我指出你文章中比较严重的错误,无所谓的细节就不说了: 一、不能说函数名就是个地址,也不能说函数名就是个指针。看样子你应该不知道从函数到指针的转换规则。指针仅仅是函数名在表达式中的行为,指针仅仅是函数名在表达式中的有条件转换结果,但非其本质。函数名的本质是什么?很简单,就是函数实体的名字,是函数实体的抽象。 二、你举的百度百科中的函数覆盖的例子是错误的,那个例子所展现的现象不是覆盖,而是隐藏,子类函数隐藏了父类函数,只有存在虚函数才存在覆盖,覆盖的英文名称叫override,而隐藏叫hide。而且,关于覆盖的条件的描述也是存在错误的,覆盖并不要求连返回值类型都严格一致,覆盖允许返回值协变。 三、关于虚函数的实现,虚函数的实现并没有标准化,表驱动方式仅是实现的方法之一(虽然大多数实现都采用这种方式),类对象模型的实现也可以完全不使用表驱动方式去实现,因此,你在文中所提的这个问题: “我再来问大家一个问题,你们认为虚函数需不需要影响类的实例大小? 哈哈,这次的答案是需要” 正确答案是不需要,vtbl并不是要求的,vtbl仅是充分条件,不是必要条件。
第2点肯定是写错了的,覆盖的条件是“三同”, 所谓三同就是同函数名称,参数类型、个数相同,返回值也相同。除此之外的都可以称为隐藏。 第三点持怀疑,如果没有虚表,何来的多态性一说。反过来说就一定有虚表,有了虚表肯定影响类的实例大小[/quote] 覆盖的条件是“三同”, 所谓三同就是同函数名称,参数类型、个数相同,返回值也相同 返回值不作为参考吧?!你也不看看现在你说了四个,哪来的三同?![/quote] 况且还有个条件基类必须为virtual函数。
幻夢之葉 2014-06-27
  • 打赏
  • 举报
回复
引用 91 楼 liu254773606 的回复:
[quote=引用 18 楼 supermegaboy 的回复:] [Quote=引用 11 楼 mymixing 的回复:] 马上要出门了,等了20多分钟终于还是没有等来各位星星所指出的错误原因。 不得不说,对CSDN确实很失望。 如果是在看雪发表一篇技术文(当然不是我发表,没那水平)。 如果文章有误,那些高手是会指出错误的理由,并交流改进方法的。 而那些新手一般也都会回文表示一下。 但在这里,我努力写出的一篇经验之谈,得到的貌似只有诸如 "看了第一行就。。。看不下去了。"这样的回答 实在让人失望。 我只…… [/Quote] 不好意思,刚才外出了。 在这里我指出你文章中比较严重的错误,无所谓的细节就不说了: 一、不能说函数名就是个地址,也不能说函数名就是个指针。看样子你应该不知道从函数到指针的转换规则。指针仅仅是函数名在表达式中的行为,指针仅仅是函数名在表达式中的有条件转换结果,但非其本质。函数名的本质是什么?很简单,就是函数实体的名字,是函数实体的抽象。 二、你举的百度百科中的函数覆盖的例子是错误的,那个例子所展现的现象不是覆盖,而是隐藏,子类函数隐藏了父类函数,只有存在虚函数才存在覆盖,覆盖的英文名称叫override,而隐藏叫hide。而且,关于覆盖的条件的描述也是存在错误的,覆盖并不要求连返回值类型都严格一致,覆盖允许返回值协变。 三、关于虚函数的实现,虚函数的实现并没有标准化,表驱动方式仅是实现的方法之一(虽然大多数实现都采用这种方式),类对象模型的实现也可以完全不使用表驱动方式去实现,因此,你在文中所提的这个问题: “我再来问大家一个问题,你们认为虚函数需不需要影响类的实例大小? 哈哈,这次的答案是需要” 正确答案是不需要,vtbl并不是要求的,vtbl仅是充分条件,不是必要条件。
第2点肯定是写错了的,覆盖的条件是“三同”, 所谓三同就是同函数名称,参数类型、个数相同,返回值也相同。除此之外的都可以称为隐藏。 第三点持怀疑,如果没有虚表,何来的多态性一说。反过来说就一定有虚表,有了虚表肯定影响类的实例大小[/quote] 覆盖的条件是“三同”, 所谓三同就是同函数名称,参数类型、个数相同,返回值也相同 返回值不作为参考吧?!你也不看看现在你说了四个,哪来的三同?!
幻夢之葉 2014-06-27
  • 打赏
  • 举报
回复
引用 34 楼 mymixing 的回复:
我说了,理论的书我看的很少,如果有些词汇我说错了,用错了地方。 那我抱歉。 但是这些东西,是我在实践中直观的理解出来的。 首先要谢谢supermegaboy前辈在18楼的回复。 不过还是要请问下。 1,我把函数名理解成指针,对以后编程的学习与理解,是否会有什么阻碍? 我目前的认知中,还没感觉到。 2,呵呵这些概念型的东西,我真的差的不少,从来没去记过这样的东西。。抱歉了。 3,那虚函数实现规则是谁来定的呢?,我理解虚函数也不是从书本上理解的,是在调试中理解的。 所以,总的来说,开篇的时候,忘记说只适用于VC系列环境了。。 此贴的目的是让人新手了解我说的这些东西,其代码实现时,是怎么的过程。 此帖中有人说我误人子弟,您这样认为么?
建议你还是得补下理论,因为可以让你在很多不解的问题上豁然开朗。 总结而且分享心得是值得鼓励的,但是更全面的了解某个概念也是必须的,那样子才能说得更让人信服不是么?!
GrimRaider 2011-10-31
  • 打赏
  • 举报
回复
看完虚函数的那段恍然大悟呀。
camelisi 2011-10-21
  • 打赏
  • 举报
回复
LZ的想法是好的,有这种动机的帖子不多
大鲸鱼有韵味 2011-10-21
  • 打赏
  • 举报
回复
把函数名理解成地址也没什么问题吧,我把函数名当成编译器用来标识地址的。

想办法把错误改了吧, 不然没看评论就悲剧了
pSon->fun(); //函数调用执行了father's fun
pFather->fun(); //函数调用执行了son's fun


inside of the c++ object
从内存空间的角度讲的很详细的。

gotope 2011-10-19
  • 打赏
  • 举报
回复
不错了能总结,更有高手指正,更好
zoel175185 2011-10-18
  • 打赏
  • 举报
回复
其实有3个概念 重载 覆盖 隐藏...........高质量c++编程指南里有提到
ri_aje 2011-10-18
  • 打赏
  • 举报
回复
[Quote=引用 1 楼 supermegaboy 的回复:]

后面的还没看,看第一行就已经错了。
[/Quote]
我看了第一行,就在想同一个问题,往下没仔细看,直接翻了一下就到你这楼了。
dazhi316 2011-10-18
  • 打赏
  • 举报
回复
楼上说的对 结果确实错了啊
yisikaipu 2011-10-18
  • 打赏
  • 举报
回复
更正楼上:带来的困惑多于便利
yisikaipu 2011-10-18
  • 打赏
  • 举报
回复
[Quote=引用 67 楼 tanchunwu 的回复:]
我的理解p和Hello等价,类似于数组和指针
函数名,在这里不就是函数入口地址吗?
[/Quote]

#18和#35楼已经说得很清楚,把函数名等同于函数地址,带来的便利多于困惑

数组arr[],在必要时,arr转换为指向arr[0]的指针,而&arr转换为指向arr[]的指针,前者是指向元素的指针,后者是指向数组的指针。可见数组的语法毫不含糊。
	int arr[3];
int *p_to_element=arr;
//int *p_to_arr=&arr; // 错误
int (*p_to_arr)[3]=&arr;


而函数func(),在必要时,func转换为函数地址,&func也是转换为函数地址,*func还是转换为函数地址,于是,**func,***func,...,***N个星***func仍然还是转换为函数地址,如果语法上允许写出&(&(&func))这样的东西,也应该还是转换为函数地址。可见,这里的func,&func,*func,********func,纯粹是茴字的不同的写法,或者虽然不是一个字,但这些字互为异体字。
void func()
{
cout <<"func()" <<endl;
}

int main()
{
void (*p1)();
void (*p2)();
void (*p3)();
void (*p4)();
void (*p5)();

p1=&func;
p2=func;
p3=*func;
p4=**func;
p5=***func;

p1();
p2();
p3();
p4();
p5();

(*p1)();
(*p2)();
(*p3)();
(*p4)();
(*p5)();

(**p1)();
(**p2)();
(**p3)();
(**p4)();
(**p5)();

(***p1)();
(***p2)();
(***p3)();
(***p4)();
(***p5)();

return 0;
}


所以,不管是数组名还是函数名,可以说适当的时候会转换为某种地址,但不要说就等同于某种地址
chos2006 2011-10-18
  • 打赏
  • 举报
回复
接分啦
ldd909 2011-10-18
  • 打赏
  • 举报
回复
pSon->fun(); //函数调用执行了father's fun
pFather->fun(); //函数调用执行了son's fun
错了吧?
应该是:
pSon->fun(); //函数调用执行了son's fun
pFather->fun(); //函数调用执行了father's fun
加载更多回复(79)

65,210

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧