蓝森林首页 | 返回主页 | 本站地图 | 站内搜索 | 联系信箱 |
 您目前的位置:首页 > 自由软件 > 技术交流 > 应用编程


    

蓝森林 http://www.lslnet.com 2006年6月6日 10:18


C++探索(virtual机制)(一部分)

有以下一个单一继承体系:
[code]class point2d {
public:
        point2d(float x = 0.0, float y = 0.0)
                : _x(x), _y(y) {};
   virtual ~point2d() {};

        virtual float x() { return _x; };
        virtual float y() { return _y; };
        virtual float z() { return 0.0; };
private:
        float _x, _y;
};

class point3d : public point2d {
public:
point3d(float x = 0.0, float y = 0.0, float z = 0.0)
: point2d(x, y), _z(z) {};
~point3d() {};
       
        float z() { return _z; };
private:
        float _z;
};[/code]


(一)        单一继承下的Object内存布局
(1)point2d Object
        class point2d 有两个float data member 共8个bytes,为了支持virtual机制,还需要额别安插一个data member,就是vptr(virtual table pointer)。
所以,整个point2d Object应该有12个bytes。
        这些data member排列次序应该是:按data member的声明次序进行排列,而被编译额别生成的vptr应该被置于最后,不过,C++ standard对vptr的位置并没有规定,一般来说,都被置于最后。(好像VC++早前版本被置于最前)。
(2)point3d Object
        class point3d继承自point2d,因此,在point3d Object中除了包含整个point2d subobject的12个bytes之外,还有它自己的data member。所以,point3d Object 共有16个bytes。
(二)virtual table
(1)        virtual member function
为了支持执行期多态,virtual member function 被引入C++中,class point2d
含有三个virtual member function以支持多态。
有如下代码:
                [code]point2d* p2d = new point3d(1.1, 2.2, 3.3);[/code]
那么:[code]p2d->;z(); [/code]是如何在执行期被确定z()的实体?
        在point2d Object中被安插一个data member:vptr,它指向一个virtual table,在virtual table的每一个slot中安插了virtual function的实体位置。一般来说:virtual table的0偏移量是_type_info_class以支持RTTI(runtime type identification),而真正virtual function指针是从1开始计数。
        所以,上例中,在vtbl(point3d中的point2d subobject的vritual table)中,是这样排列的:
[code]0:type_info_for_point3d
1: point3d::~point3d() à 指向point3d的destructor
2: point2d::x()   指向x()实体
3: point2d::y()   指向y()实体
4: point3d::z()   指向z()实体(注意:此时指向point3d::z())
   形成一个表格形式。[/code]p2d->;z()被编译器转化为以下形式:
        z(p2d);
进而,转化为:
(*p2d->;vptr[4])(p2d);
经过这种模型,p2d能很好地执行正确的z()实体。



(1)        如何确定实体
[code]point2d *ptr = new point3d(1.1, 2.2, 3.3);
ptr->;z();[/code]
以base class指针ptr如何寻找出derived class正确的z()实体?
观察并分析由gcc 3.3.1编译出的代码(这是一段main()函数码):
[code]movl        $16, (%esp)               
call        _Znwj[/code]               
16是什么?这就是point3d Object的字节数,
说明point3d object 将点16 bytes。
这是什么? 这是C++操作符new 。分配空间
其实在C++的new的背后,是通过函数_new()的调用实现的。
而函数的内部实现,是通过熟悉的malloc()函数。
那么这一段小段当然就是产生point3d Object了。
[code]movl        %eax, %ebx                // this 指针。
movl        $0x40533333, %eax        // 浮点数(3.3)
movl        %eax, 12(%esp)                // 入栈
movl        $0x400ccccd, %eax                // 浮点数(2.2)
movl        %eax, 8(%esp)                       
movl        $0x3f8ccccd, %eax                // 浮点数(1.1)
movl        %eax, 4(%esp)
movl        %ebx, (%esp)                // 压入this 指针
call        _ZN7point3dC1Efff                // call point3d 的constructor 进行初始化[/code]                               
到此,Object如何生产有个大致的了解了吧。
[code]movl        %ebx, %eax
movl        %eax, -8(%ebp)                // -8(%ebp) = ptr指针[/code]
将上几段代码转化C伪代码如下:
[code]point3d *this = malloc(sizeof(point3d));
point3d(this, 1.1, 2.2, 3.3);                    // constructor()
point2d *ptr = (point2d *)this;[/code]

[code]movl        -8(%ebp), %eax
movl        (%eax), %edx                // 提领ptr指针
addl        $8, %edx                        // 找到 virtual table 中z()实体的索引位置
movl        -8(%ebp), %eax
movl        %eax, (%esp)                // 压入ptr指针(this)
movl        (%edx), %eax
call        *%eax                                // call virtual function z()实体[/code]       
我对以上代码有点疑问:
1、一般来说:vptr(virtual table pointer)是被置于object的最后。
     但是从以上的代码来看,gcc 3.3.1将vptr置于obejct内存中
     的最前面。这个特点从代码:movl (%eax), %edx可以看出。
     其它的C++编译器,可能会是类似这样:
[code]        movl –8(%ebp), %eax,
        addl $8, %eax                =>; 先找到vptr(未尾)
        movl (%eax), %edx                =>; 然后再提领 *vptr[/code] 2、 我对gcc3.3.1的virtual function table也有疑问,按计算
   z()实体索引也不应该是2,即:2 * 4 个bytes。
     一般来说:
[code]    type_info_for_class 在索引0
    point3d::~point3d()在索引1
    point2d::x()        在索引2
    point2d::y()        在索引3
    point3d::z()        在索引4[/code]
以上的virtual function table布局,有可能安插了thunk技术
利用thunk码跳转到z()实体。
要解开以上两个疑问,要从constructor 和virtual function table中找答案。

(2)        下面是point3d的constructor
[code]movl        16(%ebp), %eax          // 第3个参数(y = 2.2)
movl        %eax, 8(%esp)
movl        12(%ebp), %eax         // 第2个参数(x = 1.1)
movl        %eax, 4(%esp)
movl        8(%ebp), %eax        // 第1个参数(this 指针)
movl        %eax, (%esp)
call        _ZN7point2dC2Eff        // call point2d 的constructor[/code]
                               // 也就是 point2d(this, x, y);
point2d subobject的起始地址与point3d object的起始地址是一样的。
对比一下point3d class constructor的定义:
point3d(float x = 0.0, float y = 0.0, float z = 0.0)       
                : _point2d(x, y) {}
先调用base class constructor。

[code]movl        8(%ebp), %eax                // this
movl        $_ZTV7point3d+8, (%eax)        //设置vptr指针,被置于vtbl地址 [/code]
我推断这个$_ZTV7point3d+8 不是vtbl地址,就是vtbl+offset
而%eax存放point2d subobject地址,由此可见,vptr确实被置于前面
故直接提领ptr指针(this指针)就是vtbl地址,或是vtbl加上z()的偏移量。
但从词面上来看,应该是vtbl加上z()的偏移量。
$_ZTV7point3d应该才是真正是vritual function table地址。
$_ZTV7point3d+8应该是x()实体索引,那么前一段代码:
[color=red]addl $8, %edx         是真正索引到z()实体,8+8=4*4=16bytes。[/color]
证明了前面分析的z()应该在virtual function table的索引为4是正确的。
而前述virtual function 在virtual function table的布局也是正确的。

[code]movl        8(%ebp), %edx        // this
movl        20(%ebp), %eax        // 第4个参数(z = 3.3)
movl        %eax, 12(%edx)        // *(this + 3) = 3.3[/code]       
以上代码,在调用base的constructor,再设置point3d object自已的 data member 即:_z。
*(this+3)= 3.3 即:_z = 3.3

对point3d的constructor工作进行一下整理,如下:
point3d(x, y, z): point2d(x, y), _z(z) {}; 被转化为:
[code] point3d(this, x, y, z) {
        point2d::point2d(this, x, y);
        _vptr = _vtbl;
        _z = z;
};[/code]

(3)        看看point3d的virtual function table布局
[code]_ZTV7point3d:
        .long        0
        .long        _ZTI7point3d
        .long        _ZN7point2d1xEv
        .long        _ZN7point2d1yEv
        .long        _ZN7point3d1zEv
        .section        .gnu.linkonce.t._ZN7point2dC2Eff,"ax",@progbits[/code]

以上是point3d的vtbl,可以明显看出,virtual function 的排列次序。
唯一和我讲述的不同是,0索引并不是type_info_for_class(RTTI)。我推断在多重继承和虚拟继承才有,因为,它们在需要的时候都需要调整this指针,从而产生执行期判断class type信息。而单一继承下是不城调整this指针的。一切都很自然地发生执行期多态。

C++探索(virtual机制)(一部分)

(4) 看看point2d 的constructor
[code]_ZN7point2dC2Eff:
pushl        %ebp
movl        %esp, %ebp
movl        8(%ebp), %eax                // 第一个参数(this指针)
movl        $_ZTV7point2d+8, (%eax)        // *this = _vptr_for_point2d
movl        8(%ebp), %edx                // 第一个参数this
movl        12(%ebp), %eax                // 第二个参数 (x = 1.1)
movl        %eax, 4(%edx)                // *(this+1) = 1.1
movl        8(%ebp), %edx
movl        16(%ebp), %eax                // 第三个参数 (y = 2.2)
movl        %eax, 8(%edx)                // *(this+2) = 2.2;
popl        %ebp[/code]

由上面可以看出,point2d的subobject内存布局为:
          this:        _vptr           (point2d virtual table + 8 )
this + 4bytes:        float x         (data member x)
this + 8bytes:        float y         (data member y)

共12个bytes。
进一步证明gcc3.3.1的vptr指针是被置地最前面的。:)

C++探索(virtual机制)(一部分)

补充一点:

(1)上述代码将产生两个virtual table, 一个对应point2d class, 一个对应point3d class。
    以便于: point2d *p2d = new point2d;       //设置point2d vptr
             point3d *p3d = new point3d;       //设置point3d vptr

(2) 在point3d 的constructor中,先调用point2d 的 constructor来设置point2d 的 vptr,令其指向 _vtbl_point2d。
    再执行point3d 本身的constructor代码,设置point3d 的 vptr,令其指向_vtbl_point3d,从而覆盖了 point2d 的 vptr。因为 point2d 的 subobject 与 point3d object 的起始地址是一致的。

C++探索(virtual机制)(一部分)

哈哈~~
支持~~

C++探索(virtual机制)(一部分)

深奥。。小弟似懂非懂。。



Copyright © 1999-2000 LSLNET.COM. All rights reserved. 蓝森林网站 版权所有。 E-mail : webmaster@lslnet.com