西城

指尖代码,手上年华

联系 聚合 管理
  20 Posts :: 0 Stories :: 62 Comments :: 0 Trackbacks
这两天看了两篇关于C++虚函数表的文章,一篇是陈浩的,http://blog.csdn.net/haoel/article/details/1948051
还有一篇是http://www.cnblogs.com/livingintruth/archive/2012/08/03/2620599.html。照着第二篇里的例子,
在我的电脑上测试了一下,发现有不少自己不懂的地方,原作者也没有讲的太清楚,所以自己查了不少资料,将一些所得
与大家分享。
示例的代码(有部分改动):

#include <cstdio>
#include <iostream>
#include <typeinfo>
using namespace std;
class Point
{
public:
    Point()
    {
        cout<<"Point constructor"<<endl;
    }
  
    virtual void func_hs()
    {
        cout<<"Point::func_hs"<<endl;
        printf("the address of this --func_hs:%p\n",&Point::func_hs);
    }
    virtual void  func_zzy()
    {
        cout<<"Point::func_zzy"<<endl;
        printf("the address of this --func_zzy:%p\n",&Point::func_zzy);
    }

    static void print()
    {
        //相对地址,在虚表指针之后.0x4 0x8 0xc --------->point to member
        printf("&Point::x=%p\n&Point::y=%p\n&Point::z=%p\n",
               &Point::x,&Point::y,&Point::z);
    }

    void printThis()
    {
        //float *
        printf("&this->x=%p\n&this->y=%p\n&this->z=%p\n",
               &this->x,&this->y,&this->z);
    }

    void printVt()
    {
        printf("the address of object,this:%p\nthe address of vt:%p\n",
               this,(void*)*(int*)this);
    }
    void callVtFuncs(int num=2)
    {
        cout<<endl<<endl;
        
        typedef void (*Funp)(void);
        
        
        for(int i=0;i<num;i++)
        {
            Funp funp=(Funp)*((int*)*(int*)this+i);
            printf("%p\n",((int*)*(int*)this+i));
            printf("Point::callVtFuncs=>address of this fun:%p\n",funp);
            if(i==2||i==3)
            {
                continue;
            }
            funp();
        }
    }

    void printVirtualFunAddress()
    {
        cout<<endl<<endl;
           printf("func_hs:%p\nfunc_zzy:%p\nfunc_zzzy:%p\n",
              &Point::func_hs,&Point::func_zzy,
              &Point::func_zzzy);
        printf("%p\n",&Point::func_zzzy);
        
    }
    virtual ~Point()
    {
        // printf("%p\n",&Point::~Point);
        cout<<"Point destructor"<<endl;
    }
    virtual void  func_zzzy()
    {
        cout<<"Point::func_zzzy"<<endl;
        printf("the address of this --func_zzzy:%p\n",&Point::func_zzzy);
    }

protected:
    float x,y,z;
};

    
int main(int argc, char *argv[])
{
    Point point;
    Point::print();
    point.printThis();
    point.printVt();
    point.callVtFuncs(5);
    point.printVirtualFunAddress();
    printf("sizeof func:%u\n",sizeof(&main));
    printf("%p\n",&main);
    
    printf("sizeof memfunc:%u\n",sizeof(&Point::printVirtualFunAddress));
    printf("%p\n",&Point::printVirtualFunAddress);
    
    printf("%p\n",&Point::func_zzzy);

    printf("sizeof virtmemfunc:%u\n",sizeof(&Point::func_zzzy));
    cout<<typeid(point).name()<<endl;
    
    return 0;
}


Point类里面包括virtual desctructor共有4个virtual functions.
输出结果如下:

Point constructor
&Point::x=0x4
&Point::y=0x8
&Point::z=0xc
&this->x=0xbffff624
&this->y=0xbffff628
&this->z=0xbffff62c
the address of object,this:0xbffff620
the address of vt:0x8048fc0


0x8048fc0
Point::callVtFuncs=>address of this fun:0x8048a12
Point::func_hs
the address of this --func_hs:0x1
0x8048fc4
Point::callVtFuncs=>address of this fun:0x8048a64
Point::func_zzy
the address of this --func_zzy:0x5
0x8048fc8
Point::callVtFuncs=>address of this fun:0x8048c8e
0x8048fcc
Point::callVtFuncs=>address of this fun:0x8048cda
0x8048fd0
Point::callVtFuncs=>address of this fun:0x8048cf8
Point::func_zzzy
the address of this --func_zzzy:0x11


func_hs:0x1
func_zzy:(nil)
func_zzzy:0x5
0x11
sizeof func:4
0x804880c
sizeof memfunc:8
0x8048bdc
0x11
sizeof virtmemfunc:8

data member那一块比较简单,没有太大的问题。主要是virtual table这块比较复杂 。第一个小问题就是
顺序的问题。实验证明在vtable中虚函数指针的顺序是按照声明时的顺序放置的。不管virtual desctructor
在哪个位置,都会占据两个slot,即有两个virtual desctructor.至于为什么会有两个,具体我也不是太清楚,
后面在讨论。
我第一个有一个有疑问的地方就是关于typeinfo的问题。记得《Inside the c++ object model》中,Lippman
提到的对象模型中,将typeinfo放置在vtable的第一个slot中,而根据上面例子来看,第一个slot就是所声明
的第一个virtual function.那么typeinfo存在何处?

vtable 的首地址是0x8048fc0,而从0x8048fb0开始的内存地址信息如下:

(gdb) x/16a 0x8048fb0
0x8048fb0:      0x75253a63      0xa     0x0     0x8048fdc <_ZTI5Point>
0x8048fc0 <_ZTV5Point+8>:       0x8048a12 <Point::func_hs()>    0x8048a64 <Point::func_zzy()>   0x8048c8e <Point::~Point()>   0x8048cda <Point::~Point()>
0x8048fd0 <_ZTV5Point+24>:      0x8048cf8 <Point::func_zzzy()>  0x696f5035      0x746e  0x804a4c8 <_ZTVN10__cxxabiv117__class_type_infoE@@CXXABI_1.3+8>
0x8048fe0 <_ZTI5Point+4>:       0x8048fd4 <_ZTS5Point>  0x3b031b01      0x98    0x12

可以看出,在vtable之前也有关于Point类的信息,0x8048fbc处的值为
0x8048fdc <_ZTI5Point>,而0x8048fdc处的值为:0x804a4c8 <_ZTVN10__cxxabiv117__class_type_infoE@@CXXABI_1.3+8,可以猜测这是与类型有关的。


输出中我在typeid(point)那一行设了断点,进入函数之后可以看到:

(gdb) s
std::type_info::name (this=0x8048fdc)
    at /usr/lib/gcc/i686-pc-linux-gnu/4.7.1/../../../../include/c++/4.7.1/typeinfo:102
102         { return __name[0] == '*' ? __name + 1 : __name; }
(gdb) n
5Point

point的类型信息为5Point,而type_info::name传入的参数信息就是0x8048fdc.再看用
-fdump-class-hierarchy输出的关于Point的类型信息:


Vtable for Point
Point::_ZTV5Point: 7u entries
0     (int (*)())0
4     (int (*)())(& _ZTI5Point)
8     (int (*)())Point::func_hs
12    (int (*)())Point::func_zzy
16    (int (*)())Point::~Point
20    (int (*)())Point::~Point
24    (int (*)())Point::func_zzzy

Class Point
   size=16 align=4
   base size=16 base align=4
Point (0xb60019a0) 0
    vptr=((& Point::_ZTV5Point) + 8u)


这样的结果已经相当清楚。在g++的实现中,真正的typeinfo信息放置在vtable之后,其位置是通过vtable之前的地址内
所包含的信息所指定。在整个关于Point类的这些信息里,起始位置为&Point::_ZTV5Point,其值为0x0,之后是关于类型
信息,然后才是vtable的入口点。即 vptr=((& Point::_ZTV5Point) + 8u)。point类的this指针通过类型转化后所解
引用得到的值即使vptr.至于size=16很好理解,1个vptr+3个float.


另一个诡异的信息就是point to member的输出,单个输出是,3个virtual function分别是0x1 0x5 0x11,而在
printVirtualFunAddress中所输出的信息却很诡异,分别是0x1,nil,0x5,这就是为什么要在main函数中加入测试
函数指针大小的原因,普通函数是4个字节,而成员函数的确是8个字节,不管是不是virtual function.所以在printf
参数入栈的时候,要push两次,将第二次push的结果当作了第二个参数的值,以此后推。所以会有上面的结果。

最后就是那两个desctructor的问题,在上面通过函数指针调用函数的时候要将他们都略过去。如果将virtual去掉,
那么vtable中就没有desctructor。
下面是objdump出来的结果:objdump -d a.out|grep PointD

 8048932:    e8 51 03 00 00           call   8048c88 <_ZN5PointD1Ev>
 8048944:    e8 3f 03 00 00           call   8048c88 <_ZN5PointD1Ev>
08048c88 <_ZN5PointD1Ev>:
 8048cc5:    74 0b                    je     8048cd2 <_ZN5PointD1Ev+0x4a>
08048cd4 <_ZN5PointD0Ev>:
 8048ce0:    e8 a3 ff ff ff           call   8048c88 <_ZN5PointD1Ev>


两个函数中,main调用的是<_ZN5PointD1Ev>,这就是我们上面声明的那个virtual desctrucotr.而且在内存布局中,
<_ZN5PointD1Ev>的位置比较靠前,其在vtable中的位置也应在<_ZN5PointD0Ev>之前,而<_ZN5PointD0Ev>

08048cd4 <_ZN5PointD0Ev>:
 8048cd4:    55                       push   %ebp
 8048cd5:    89 e5                    mov    %esp,%ebp
 8048cd7:    83 ec 18                 sub    $0x18,%esp
 8048cda:    8b 45 08                 mov    0x8(%ebp),%eax
 8048cdd:    89 04 24                 mov    %eax,(%esp)
 8048ce0:    e8 a3 ff ff ff           call   8048c88 <_ZN5PointD1Ev>
 8048ce5:    8b 45 08                 mov    0x8(%ebp),%eax
 8048ce8:    89 04 24                 mov    %eax,(%esp)
 8048ceb:    e8 80 f9 ff ff           call   8048670 <_ZdlPv@plt>
 8048cf0:    c9                       leave  

的工作好像也要调用<_ZN5PointD1Ev>,这个应该是编译器生成的,再往深处我也不甚清楚了。



posted on 2012-08-10 11:17 西城 阅读(2237) 评论(2)  编辑 收藏 引用 所属分类: C/C++

Feedback

# re: 关于C++ 虚函数表的一些问题(g++) 2012-08-29 19:20 izualzhy
static void print()
{
//相对地址,在虚表指针之后.0x4 0x8 0xc --------->point to member
printf("&Point::x=%p\n&Point::y=%p\n&Point::z=%p\n",
&Point::x,&Point::y,&Point::z);
}

静态函数使用非静态成员,为什么可以编译通过啊?  回复  更多评论
  

# re: 关于C++ 虚函数表的一些问题(g++) 2012-08-30 19:16 西城
@izualzhy
我又看了一下,确实是可以的。感觉&Point::x这种东西是与具体对象无关的,每个对象都一样,所以我觉得从行为上来说应该是与静态成员一样。  回复  更多评论
  


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