以题论道----关于虚函数的一些解读

                                    peakflys原创作品,转载请注明源作者和源链接!
    virtual function是很多公司面试题的重点考察内容,虽然对于C++而言这是一个老生常谈的话题了,但是工作中我发现还是有很多人理解的不透彻。
    先看下面的一个例子:
/**
 *\brief virtual function test case
 *\author peakflys
 *\date Sun Dec  1 14:52:47 CST 2013
 */
#include <iostream>
using namespace std;
class Base
{
public:
    virtual void print(const int a = 10) {cout<<"Base: "<<a<<endl;}
};
class Derive : public Base
{
public:
    virtual void print(const int a = 100) {cout<<"Derive: "<<a<<endl;}
};
int main()
{
    Base *pb = new Derive;
    pb->print();
    Base& rb = *pb;
    rb.print();
    Derive d;
    d.print();
    Base *pbb = &d; 
    pbb->print();
    Base& rbb = d;
    rbb.print();
    Base b;
    b.print();
    Derive *pd = (Derive*)&b;
    pd->print();
    Derive& rd = *(Derive*)&b;
    rd.print();
    delete pb; 
    return 0;
}
你认为运行后的结果是什么呢?
下面是在我机器上的运行结果(Linux dev 2.6.32,gcc (GCC) 4.8.1)
Derive: 10
Derive: 10
Derive: 100
Derive: 10
Derive: 10
Base: 10
Base: 100
Base: 100
上面例子主要考察的内容有四块:虚函数的执行、引用和指针的关系、函数调用过程、类型强转后的行为。如果你能答对所有的结果,下面的内容可以略过。
下面我们来一一回顾一下所涉及到的这四块内容。
1、虚函数的运行机理:
虚函数是C++实现多态性的必要手段,它在运行时刻才决定具体该调用哪个函数。对于虚函数的完整细节实现标准并未给出,但是大多数编译器厂商,包括GCC、VS的常见实现都是在含有虚函数的类对象起始地址增加一个虚表指针,虚表指针指向的数组空间称之为虚表,这个数组包含了类对象的所有虚函数地址。详细内容大家可以参看《Inside The C++ Object Model》的Function语义学(注:这本书里有部分结论和例子运行同现在主流编译器的实现有出入)。
2、引用的行为
在常见的编译器中,引用一般都是通过指针来实现的,它同指针的区别就是它比指针有更多的约束,使用上有更多的限制。
3、虚函数的调用过程:
虚函数的调用过程通常是以下三个步骤:
①、参数压栈
②、从虚表指针指向的虚表中找出函数的地址
③、调用函数。
这些操作都是在编译时期就确定的,所不同的是运行时刻对象不同,其对应的虚表中函数地址自然也就是运行时真实对象的函数,这也就是虚函数实现的本质。
而这个过程中,参数的入栈是对象无关的,而且是在编译时期就确定下来的。所以上面例子中所有指针和引用所调用函数的参数,都是指针和引用本身类型对应的函数默认参数,同运行时刻他们真实指向的对象内存无关。
4、类型强转后的行为
通常的类型强转是告诉编译器必须按照指定结构的内存布局来解析对应内存,正如上例中”Derive *pd = (Derive*)&b; “ ,编译器就会把b对应的内存来当做Derive的内存布局来解析,但是内存里的内容不变,所以虚函数运行正常。
注:这种行为很危险,如果使用的内存布局并不适合真实内存,很可能造成访问越界等问题,所以要格外小心强转操作的使用!对于例子中的downcasting行为,建议使用C++提供的dynamic_cast来转换。
为了大家更好的理解上面的内容,特附上使用指针和引用分别调用虚函数过程的gcc汇编代码和注释:
    Base *pb = new Derive;
  400b49:   bf 08 00 00 00          mov    $0x8,%edi
  400b4e:   e8 6d fe ff ff          callq  4009c0 <_Znwm@plt>
  400b53:   48 89 c3                mov    %rax,%rbx
  400b56:   48 89 df                mov    %rbx,%rdi
  400b59:   e8 f4 01 00 00          callq  400d52 <_ZN6DeriveC1Ev>   //以上均为Derive对象的构造
   400b5e:   48 89 5d e8             mov    %rbx,-0x18(%rbp)             //pb指针的赋值
    pb->print();
  400b62:   48 8b 45 e8             mov    -0x18(%rbp),%rax               //pb指针指向的内存的首地址,即Derive对象的起始地址,亦即虚表指针的地址
  400b66:   48 8b 00                mov    (%rax),%rax                        //取虚表地址
  400b69:   48 8b 00                mov    (%rax),%rax                        //取虚表中的第一项内容(因Derive和Base只有一个虚函数),即print函数地址
  400b6c:   48 8b 55 e8             mov    -0x18(%rbp),%rdx               //this指针传入rdx
  400b70:   be 0a 00 00 00          mov    $0xa,%esi                         //参数10入栈(可见在编译时期就已经确定了)
  400b75:   48 89 d7                mov    %rdx,%rdi                            //this指针借rdx传给rdi
  400b78:   ff d0                   callq  *%rax                                     //调用虚函数(通过真实对象的虚表来确定的真正被调函数)
    Base& rb = *pb;
  400b7a:   48 8b 45 e8             mov    -0x18(%rbp),%rax
  400b7e:   48 89 45 e0             mov    %rax,-0x20(%rbp)
    rb.print();
  400b82:   48 8b 45 e0             mov    -0x20(%rbp),%rax
  400b86:   48 8b 00                mov    (%rax),%rax
  400b89:   48 8b 00                mov    (%rax),%rax
  400b8c:   48 8b 55 e0             mov    -0x20(%rbp),%rdx
  400b90:   be 0a 00 00 00          mov    $0xa,%esi
  400b95:   48 89 d7                mov    %rdx,%rdi
  400b98:   ff d0                   callq  *%rax                                       //以上为通过引用调用虚函数的过程,可见同指针调用的实现完全相同,注释略
通过上面的分析,相信大家应该都能轻松的明白上面例子的运行结果,此处不再一一解读。
                                                         --by peakflys 15:57:06 Sunday, December 01, 2013

posted on 2013-12-01 16:08 peakflys 阅读(2922) 评论(7)  编辑 收藏 引用 所属分类: C++

评论

# re: 以题论道----关于虚函数的一些解读 2013-12-01 19:05 Richard Wei

effective C++ item 38  回复  更多评论   

# re: 以题论道----关于虚函数的一些解读 2013-12-02 09:32 peakflys

仅作例子讲解@Richard Wei
  回复  更多评论   

# re: 以题论道----关于虚函数的一些解读 2013-12-03 11:21 zdd

最后一个类型转换,这样也可以。不知道两者有何区别。
Derive& rd = (Derive&)b;
rd.print();  回复  更多评论   

# re: 以题论道----关于虚函数的一些解读 2013-12-04 09:47 NWAO

".....上面例子中所有指针和引用所调用函数的参数,都是指针和引用本身类型对应的函数默认参数,同运行时刻他们真实指向的对象内存无关。"

這句話加紅一下就更好了, 很重要的,同學們可以參考Effective C++ ITEM 37.  回复  更多评论   

# re: 以题论道----关于虚函数的一些解读 2013-12-04 23:45 vsgoster

感谢,很受用~特别是默认参数的入栈,从没考虑过这个问题。  回复  更多评论   

# re: 以题论道----关于虚函数的一些解读[未登录] 2013-12-05 14:24 peakflys

@zdd 两者没有本质区别。
*(Derive*)&b 先取地址,强转地址类型,然后再取内容,同汇编实现基本是一一对应起来的;
(Derive&)b是使用C式的强转直接把内容转成Derive的引用,编译器帮你翻译成的汇编代码实现应该和上面的是一样的。  回复  更多评论   

# re: 以题论道----关于虚函数的一些解读 2013-12-12 00:00 Hacksign

博主的例子,有一个坑,具有默认参数的virtual函数执行静态绑定。所以,如果去掉参数,即virtual void print(void),那么第一个print调用应该执行derive版本的print。  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


<2013年12月>
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234

导航

统计

公告

人不淡定的时候,就爱表现出来,敲代码如此,偶尔的灵感亦如此……

常用链接

留言簿(4)

随笔分类

随笔档案

文章档案

搜索

最新评论

阅读排行榜

评论排行榜