当前位置导航:炫浪网>>网络学院>>编程开发>>C++教程>>C++进阶与实例

C++基本概念在编译器中的实现

      对于C++对象模型,相信很多程序员都耳熟能详。 本文试图通过一个简单的例子演示一些C++基本概念在编译器中的实现,以期达到眼见为实的效果。

      1、对象空间和虚函数

      1.1 对象空间

      在我们为对象分配一块空间时,例如:

      CChild1 *pChild = new CChild1();

      这块空间里放着什么东西?

      在CChild1没有虚函数时,CChild1对象空间里依次放着其基类的非静态成员和其自身的非静态成员。没有任何非静态成员的对象,会有一个字节的占位符。

      如果CChild1有虚函数,VC6编译器会在对象空间的最前面加一个指针,这就是虚函数表指针(Vptr:Virtual function table pointer)。我们来看这么一段代码:

    class CMember1 {
    public:
    CMember1(){a=0x5678;printf("构造 CMember1\n");}
    ~CMember1(){printf("析构 CMember1\n");}
    int a;
    };

    class CParent1 {
    public:
    CParent1(){parent_data=0x1234;printf("构造 CParent1\n");}
    virtual ~CParent1(){printf("析构 CParent1\n");}
    virtual void test(){printf("调用CParent1::test()\n\n");}
    void real(){printf("调用CParent1::test()\n\n");}
    int parent_data;
    };

    class CChild1 : public CParent1 {
    public:
    CChild1(){printf("构造 CChild1\n");}
    virtual ~CChild1(){printf("析构 CChild1\n");}
    virtual void test(){printf("调用CChild1::test()\n\n");}
    void real(){printf("调用CChild1::test()\n\n");}
    CMember1 member;
    static int b;
    };

      CChild1对象的大小是多少?以下是演示程序的打印输出:

    ---->派生类对象
    对象地址 0x00370FE0
    对象大小 12
    对象内容
    00370FE0: 00410104 00001234 00005678
    vptr内容
    00410104: 004016a0 00401640 00401f70

      CChild1对象的大小是12个字节,包括:Vptr、基类成员变量parent_data、派生类成员变量member。Vptr指向的虚函数表(VTable)就是虚函数地址组成的数组。

      1.2 Vptr和VTable

      如果我们用VC自带的dumpbin反汇编Debug版的输出程序:

    dumpbin /disasm test_vc6.exe>a.txt

      可以在a.txt中找到:

    ?test@CChild1@@UAEXXZ:
    00401640: 55 push ebp...
    ??_ECChild1@@UAEPAXI@Z:
    004016A0: 55 push ebp

      可见VTable中的两个地址分别指向CChild1的析构函数和CChild1的成员函数test。这两个函数是CChild1的虚函数。如果打印两个CChild1对象的内容,可以发现它们Vptr是相同的,即每个有虚函数的类有一个VTable,这个类的所有对象的Vptr都指向这个VTable。

      这里的函数名是不是有点奇怪,附录二简略介绍了C++的Name Mangling。

      1.3 静态成员变量

      在C++中,类的静态变量相当于增加了访问控制的全局变量,不占用对象空间。它们的地址在编译链接时就确定了。例如:如果我们在项目的Link设置中选择“Generate mapfile”,build后,就可以在生成的map文件中看到:

    0003:00002e18 ?b@CChild1@@2HA 00414e18 test1.obj

      从打印输出,我们可以看到CChild1::b的地址正是0x00414E18。其实类定义中的对变量b的声明仅是声明而已,如果我们没有在类定义外 (全局域) 定义这个变量,这个变量根本就不存在。

      1.4 调用虚函数

      通过在VC调试环境中设置断点,并切换到汇编显示模式,我们可以看到调用虚函数的汇编代码:

    16: pChild->test();
    (1) mov edx,dWord ptr [pChild]
    (2) mov eax,dword ptr [edx]
    (3) mov esi,esp
    (4) mov ecx,dword ptr [pChild]
    (5) call dword ptr [eax+4]

      语句(1)将对象地址放到寄存器edx,语句(2)将对象地址处的Vptr装入寄存器eax,语句(5)跳转到Vptr指向的VTable第二项的地址,即成员函数test。

      语句(4)将对象地址放到寄存器ecx,这就是传入非静态成员函数的隐含this指针。非静态成员函数通过this指针访问非静态成员变量。

      1.5 虚函数和非虚函数

      在演示程序中,我们打印了成员函数地址:

    printf("CParent1::test地址 0x%08p\n", &CParent1::test);
    printf("CChild1::test地址 0x%08p\n", &CChild1::test);
    printf("CParent1::real地址 0x%08p\n", &CParent1::real);
    printf("CChild1::real地址 0x%08p\n", &CChild1::real);

      得到以下输出:

    CParent1::test地址 0x004018F0
    CChild1::test地址 0x004018F0
    CParent1::real地址 0x00401460
    CChild1::real地址 0x00401670

      两个非虚函数的地址很容易理解,在dumpbin的输出中可以找到它们:

    ?real@CParent1@@QAEXXZ: 00401460: 55 push ebp...
    ?real@CChild1@@QAEXXZ: 00401670: 55 push ebp

      为什么两个虚函数的“地址”是一样的?其实这里打印的是一段thunk代码的地址。通过查看dumpbin的输出,我们可以看到:

    _9@$B3AE:
    (6) mov eax,dword ptr [ecx]
    (7) jmp dword ptr [eax+4]

      如果我们在跳转到这段代码前将对象地址放到寄存器ecx,语句(6)就会将对象地址处的Vptr装入寄存器eax,语句(7)跳转到Vptr指向的VTable第二项的地址,即成员函数test。基类和派生类VTable的虚函数排列顺序是相同的,所以可以共用一段thunk代码。

      这段thunk代码的用途是通过函数指针调用虚函数。如果我们不取函数地址,编译器就不会产生这段代码。请注意不要将本节的thunk代码与VTable中虚函数地址混淆起来。Thunk代码根据传入的对象指针决定调用哪个函数,VTable中的虚函数地址才是真正的函数地址。

      1.6 指向虚函数的指针

      我们试验一下通过指针调用虚函数。非静态成员函数指针必须通过对象指针调用:

    typedef void (Parent::*PMem)();
    printf("\n---->通过函数指针调用\n");
    PMem pm = &Parent::test;
    printf("函数指针 0x%08p\n", pm);
    (pParent->*pm)();

      得到以下输出:

      ---->通过函数指针调用

      函数指针 0x004018F0
      调用CChild1::test()

      我们从VC调试环境中复制出这段汇编代码:

    13: (pParent->*pm)();
    (8) mov esi,esp
    (9) mov ecx,dword ptr [pParent]
    (10) call dword ptr [pm]

      语句(9)将对象指针放到寄存器ecx中,语句(10)调用函数指针指向的thunk代码,就是1.5节的语句(6)。下面会发生什么,前面已经说过了。

      1.7 多态的实现

      经过前面的分析,多态的实现应该是显而易见的。当用指向派生类对象的基类指针调用虚函数时,因为派生类对象的Vptr指向派生类的VTable,所以调用的当然是派生类的函数。

      通过函数指针调用虚函数同样要经过VTable确定虚函数地址,所以同样会发生多态,即调用当前对象VTable中的虚函数。


 

共2页 首页 上一页 1 2 下一页 尾页 跳转到
相关内容
赞助商链接