C++对象模型之详述C++对象的内存布局

本文深入探讨了C++对象模型中关于对象内存布局的问题,涉及单一继承、多重继承、重复继承、单一虚拟继承和钻石型虚拟继承等多种情况。文章分析了继承对对象内存分布的影响,特别是虚函数表的变化和运行时类型识别。通过实例展示了如何遍历和调用虚函数表中的函数,揭示了虚函数调用的动态绑定原理。
摘要由CSDN通过智能技术生成
C++对象模型之简述C++对象的内存布局一文中,详细分析了各种成员变量和成员函数对一个类(没有任何继承的)对象的内存分布的影响,及详细讲解了如何遍历对象的内存,包括虚函数表。如果你在阅读本文之前,还没有看过C++对象模型之简述C++对象的内存布局一文,建议先阅读一下。而本文主要讨论继承对于对象的内存分布的影响,包括:继承后类的对象的成员的布局、继承对于虚函数表的影响、virtual函数机制如何实现、运行时类型识别等。由于在C++中继承的关系比较复杂,所以本文会讨论如下的继承情况:
1)单一继承
2)多重继承
3)重复继承
4)单一虚拟继承
5)钻石型虚拟继承

此外,当一个类作为一个基类时,它的析构函数应该是virtual函数,这样下面的代码才能正确地运行
Base *p = new Derived;
...
delete p;
在本文的例子,为了验证虚函数表的内容,会遍历并调用虚函数表中的所有函数。但是当析构函数为virtual时,在遍历的过程中就会调用到对象的析构函数,从而对对象进行析构的操作,导致接下来的调用出错。但是本文的目的是分析和验证C++对象的内存布局,而不是设计一个软件,析构函数为非virtual函数,并不会影响我们的分析和理解,因为virtual析构函数与其他的virtual函数是一样的,只是做的事不一样。所以在本文中的例子中,析构函数均不为virtual,特此说明一下。

同时为了调用的方便,所有的virtual的函数原型均为:返回值为void,参数也为void。

注:以下的例子中的测试环境为:32位Ubuntu 14.04 g++ 4.8.2,若在不同的环境中进行测试,结果可能有不同。

1、根据指向虚函数表的指针(vptr)遍历虚函数表
由于在访问对象的内存时,都要遍历虚函数表来确定虚函数表中的内容,所以对这部分的功能抽象出来,写成一个函数,如下:
void visitVtbl(int **vtbl, int count)
{
    cout << vtbl << endl;
    cout << "\t[-1]: " << (long)vtbl[-1] << endl;

    typedef void (*FuncPtr)();
    for (int i = 0; vtbl[i] && i < count; ++i)
    {
        cout << "\t[" << i << "]: " << vtbl[i] << " -> ";
        FuncPtr func = (FuncPtr)vtbl[i];
        func();
    }
}

代码解释:
参数vtbl为虚函数表的第一个元素的地址,也就是对象中的vptr的值。参数count指的是该虚函数表中虚函数的数量。由于虚函数表中保存的信息并不全是虚函数的地址,也不是所有的虚函数表中都以NULL表示虚函数表中的函数地址已经到了尽头。所以为了让测试程序更好地运行,所以加上这一参数。

虚函数表保存的是函数的指针,若把虚函数表当作一个数组,则要指向该数组需要一个双指针,即参数中的int **vtbl,获取函数指针的值,即获取数组中元素的值,可以通过vtbl[i]来获得。

虚函数表中还保存着对象的类型信息,通常为了便于查找对象的类型信息,使用虚函数表中的索引(下标)为-1的位置保存该类对应的类型信息对象(即类std::type_info的对象)的地址,即保存在第一个虚函数的地址之前。

2、单一继承
类的具体代码如下:
class Base
{
    public:
        Base()
        {
            mBase1 = 101;
            mBase2 = 102;
        }
        virtual void func1()
        {
            cout << "Base::func1()" << endl;
        }
        virtual void func2()
        {
            cout << "Base::func2()" << endl;
        }
    private:
        int mBase1;
        int mBase2;
};

class Derived : public Base
{
    public:
        Derived():
            Base()
        {
            mDerived1 = 1001;
            mDerived2 = 1002;
        }
        virtual void func2()
        {
            cout << "Derived::func2()" << endl;
        }
        virtual void func3()
        {
            cout << "Derived::func3()" << endl;
        }
    private:
        int mDerived1;
        int mDerived2;
};

使用如下的代码进行测试:
int main()
{
    Derived d;
    char *p = (char*)&d;
    visitVtbl((int**)*(int**)p, 3);
    p += sizeof(int**);

    cout << *(int*)p << endl;
    p += sizeof(int);

    cout << *(int*)p << endl;
    p += sizeof(int);

    cout << *(int*)p << endl;
    p += sizeof(int);

    cout << *(int*)p << endl;

    return 0;
}

代码解释:
在测试代码中,最难明白的就是以下语句中的参数:
visitVtbl((int**)*(int**)p, 3);
char指针p指向了对象中的vptr,由于vptr也是一个指针,所以p应该是一个双指针,对其解引用(*p)可以获得vptr的值。然而在同一个系统中,无论是什么类型的指针,其占用的内存大小都是相同的(一般在32位系统中为4字节,64位系统中为8字节),所以可以通过以下语句获取vptr的值:
  • 20
    点赞
  • 81
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值