C++类内存布局(虚函数与虚继承)

C++类内存布局

单继承
class Base{
    int x, y;
};


class Derive: public Base{
    int z;
};

可以看到,在Derive类中,Base类的成员被放在了前面。

多继承
class Base1{
    int x, y;
};

class Base2{
    int a, b;
};

class Derive: public Base1, public Base2{
    int z;
};

对于多继承而言,一般是按照声明的顺序从左到右排放。在上面的例子中,Base1放在Base2前面。

单继承+虚基类
class Base{
    int x, y;
};

class Derive: virtual public Base{
    int z;
};
class Base1{
    int x, y;
};

class Base2: virtual public Base1{
    int a, b;
};

class Derive: virtual public Base2{
    int z;
};

在一个派生类中保留间接基类的多份同名成员,虽然可以不同的成员变量中存放不同的数据,但这在大多数情况下都是多余的,因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。

为了解决多继承时的名称冲突和数据冗余问题,C++提出了虚继承机制,使得在派生类中只保留一份间接基类的成员。

为了做到这一点,虚继承了基类的派生类中必须包含一个指向虚基类的指针(或者偏移量),因为随着虚继承的不断加深,虚基类存放的位置在派生类中是不断变化的,必须在运行时才能确定。这个偏移量和虚函数地址一样都是存放在虚表中,所以派生类中会有一个指向虚表的指针vptr。

事实上vptr指向的并非是虚表的开始位置,而是虚表中虚函数地址存放的起始位置,虚基类偏移量、头部偏移量、RTTI(类类型信息)存放在虚函数之前。这一点是需要注意的。

在上面两个图中还有一个细节是值得关注的,那就是pad填充字节。这是由于内存对齐规则的限制而导致的,关于C++的内存对齐规则,这里简单介绍:

  • GCC对齐系数可通过#pragma pack(n)来指定(32系统默认为4,64位系统默认为8)
  • 给定对齐系数和结构体中最长数据类型中长度较小的值,称为有效对齐值。
  • 结构体的一个成员的偏移量为0,之后的每个成员相对于结构体首部的偏移量都是成员大小与有效对齐值中较大的那个的整数倍,如有需要编译器会在成员之间加上填充字节。
  • 结构体的总大小为有效对齐值的整数倍,如有需要编译器会在最后一个成员之后加上填充字节。
单继承+虚函数
class Base{
    int x, y;
    virtual int fun(){
        return x+y;
    }
};

class Derive: public Base{
    int z;
    virtual int fun(){
        return z;
    }
};
class Base{
    int x, y;
    virtual int fun(){
        return x+y;
    }
};

class Base2: public Base{
    int a, b;
    virtual int fun(){
        return a+b;
    }
};

class Derive: public Base2{
    int z;
    virtual int fun(){
        return z;
    }
};

C++虚函数机制是实现多态的关键。而C++标准中对于虚函数的实现机制没有统一的标准,各个编译器厂家的实现略有不同。这里讲的是GCC的实现。

为了实现虚函数机制,需要将虚函数地址存放在虚表中。

单继承+虚函数+虚基类
class Base{
    int x, y;
    virtual int fun(){
        return x+y;
    }
};

class Derive: virtual public Base{
    int z;
    virtual int fun(){
        return z;
    }
};
多继承+虚函数
class Base1{
public:
    int x, y;
    virtual int fun(){
        return x+y;
    }
};

class Base2{
public:
    int a, b;
    virtual int foo(){
        return a-b;
    }
};

class Derive: public Base1, public Base2{
public:
    int z;
    virtual int fun(){
        return z;
    }
    virtual int foo(){
        return z;
    }
};

Derive* ptr = new Derive;
Base1 *p1 = ptr;
/*虚拟C++码
Base1 *p1 = ptr;
*/
Base2 *p2 = ptr;
/*虚拟C++码
Base2 *p2 = ptr ? (Base2 *)((char*)ptr) + sizeof(Base1) */
多继承+虚基类
class Base{
    int x, y;
};
class Derive1: virtual public Base{
    int a;
};

class Derive2: virtual public Base{
    int b;
};

class Derive: public Derive1, public Derive2{
    int z;
};
class Base{
    int x, y;
};
class Derive1: virtual public Base{
    int a;
};

class Derive2: virtual public Base{
    int b;
};

class Derive: virtual public Derive1, virtual public Derive2{
    int z;
};
Derive *ptr = new Derive;
Base *p = ptr;
/*虚拟C++码
Base *p = ptr ? ptr + _vptr_Dervie[-3] : 0;
*/

这是虚继承最常用的从场景,为了避免在Derive类中保存两份Base的数据,以及避免命名冲突。Derive类想要存取虚基类的数据,首先需要从虚表中找出虚基类的偏移量,在根据变量在虚基类中的偏移量得到成员变量的地址。

多继承+虚函数+虚基类
class Base{
    int x, y;
};
class Derive1: virtual public Base{
    int a;
    virtual int fun(){
        return a;
    }
};

class Derive2: virtual public Base{
    int b;
    virtual int foo(){
        return b;
    }
};

class Derive: virtual public Derive1, virtual public Derive2{
    int z;
    virtual int fun(){
        return z;
    }
    virtual int foo(){
        return z;
    }
};
小结

类中只要包含虚函数或者虚基类,必然需要虚表。对于有虚函数的类,虚表中需存放虚函数的地址,其位置在指向虚表的指针指向位置的后面;对于有虚基类的类,虚表中需要存放虚基类的相对便宜量,其位置在指向虚表指针指向位置的前面。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值