C++成员变量内存模型

0X00不带继承类内存布局

类变量内存中有哪些内容

静态变量:静态变量被放在全局区的静态区中,并不在变量中。
函数(非类成员函数,成员函数):代码区

每一个类变量的内存布局中没有这个类的函数信息,只包含成员,虚函数表指针(vfptr),虚继承表指针(vtptr)(不同编译器对虚继承实现不一致,本篇用微软的cl编译器做实例)。

class A{

public:
    void print() {
        cout << d << endl;
    }
    int a;
};

类A的内存布局如下:
类A的内存描述
只有这个成员变量,并没有定义的函数信息。

成员的内存地址标准

一个类中的成员变量是如何布局的?
现在我们有一段代码,代码的如下。

class A{
public:
    int  a;
    char a1;
    char a2;
    char a3;
};

在C++的标准中规定后出现的成员变量应该在内存的更高位地址(这边注意没有规定连续),所以A中的成员变量应该从低地址->高地址顺序为:a->a1->a2->a3。下面这张图是通过vs编译器查看编译后的内存结构,但是只能说明是按一定顺序排列的,我们可以打印出地址查看是否后出现的元素在地址。
cl编译器编译后的内存结构

通过该代码直接打印出类A中元素的内存地址

    A a;
    cout << "a.a = "<< (int)(&a.a) << endl;
    cout << "a.a1 = " << (int)(&a.a1) << endl;
    cout << "a.a2 = " << (int)(&a.a2) << endl;
    cout << "a.a3 = " << (int)(&a.a3) << endl;

输出如下
内存地址

上面输出可以看出,类中的成员变量由出现顺序a->a3, 在内存地址由低到高中的顺序也是a->a3。
这个例子,为了说明成员的地址是根据出现的顺序由低到高这个标准。

什么时候内存不会连续

标准只规定了后面出现的成员变量地址更大(在编译器没有给你做优化的情况下),没有规定连续。
在有内存对齐详细介绍)的情况(内存对齐是因为某些平台不支持随意的读取内存,只能支持特定位置开始)类成员变量就不会有连续的内存地址。
当类A的定义如下图,这个时候会产生内存对齐。

#include <iostream>
#include "stdio.h"
using namespace std;

class A{
public:
    char a1;
    int  a;//产生内存对齐
    char a2;
    char a3;
};

int main() {
    A a;
    //切记输出顺序和变量先后顺序是一样的
    cout << "a.a1 = " << (int)(&a.a1) << endl;
    cout << "a.a = " << (int)(&a.a) << endl; 
    cout << "a.a2 = " << (int)(&a.a2) << endl;
    cout << "a.a3 = " << (int)(&a.a3) << endl;
}

a1变量虽然是char类型,但是距离a变量也有4个对应字节,编译器会在a1后插入3个字节,a3后有2个字节用于内存对齐
具有内存对齐的结构

经过内存对齐后,布局带有“alignment”填充字段。
内存对齐后的地址

有虚继承的时候也会导致内存不连续

0X01带继承的内存布局

没有虚继承和虚函数的继承情况

在只有单继承的情况下类的内存布局,根据下面的代码做分析

class A{
    int a;
};

class B : public A{
    int b;
};

B继承自A,编译后我们看下B的内存布局

B的内存布局
可以看出其实就是很简单的把A内存布局拷贝一份到B的起始位置,然后接下去放置B的成员变量。只有的单继承并不会添加别的东西。

多继承的情况也是类似,不会添加任何的东西,只是顺序的把父类的内存布局根据继承的先后顺序拷贝下来(在没有虚继承的情况!!!)。

class A{
    int a;
};


class B{
    int b;
};

class C : public B , public A {
    int c;
};

内存布局图如下

C类的内存布局

只带虚函数的继承

在c++我们经常会声明一个函数为虚函数,那么在有虚函数的时候是什么样子的呢?我们定义一个类A看下编译后的结果

class A{
public:
    //规定了一个虚函数
    virtual void func() {
    }
    int a;
};

这个类中除了成员还有一个虚函数,拥有虚函数的类中都有一个指向虚函数表的指针,用于在运行期确定调用的是哪一个函数。

类A内存布局

现在我们知道具有虚函数的类内存布局,那么加上继承是什么样呢?其实和没有虚函数一样,子类会把父类的内存空间布局完美的复制一份(在没有虚继承的情况下)。

下面这个类B的内存布局,B继承自A。

class A{
    //规定了一个虚函数
    virtual void func() {

    }
    int a;
};

class B : public A{
    virtual void func2() {

    };
    int b;
};

这个是经过编译后看到B的内存布局。
在这里插入图片描述
拥有一个虚函数表指针,还有子类的成员和自己的成员。自始至终都只有一个虚函数表指针,保存实际函数地址在于另外一个表中,如下图。
虚函数表
c++中并没有规定虚函数表的实现,不同的编译器对虚函数表也是有各自不同的实现方式。

带虚继承的函数

c++中虚继承主要是用于重复继承相同的父类。解决的问题是:在重复继承父类元素后,一个类中会有重复父类相同的拷贝。

我们有如下的代码,C中重复继承了A类,那么我们C中就会有两个C内存布局的拷贝。

class A{
    int a;
};4
class B : public A{
    int b;
};
class C : public B, public A{
    int c;
};

下面是编译后的C中的内存布局。
在这里插入图片描述
很容易看出来在地址为0的时候有a变量,在地址为8的时候有a变量,对使用者来说他只知道有一个a,这就导致了内存的浪费。如果我们用虚继承就可以解决掉这个问题。

当我们某个类(或者这个类的子类)有可能出现重复继承某个基类时,我们需要使用虚继承。
如下段代码:

class A{
    int a;
};
class B : virtual public A{
    int b;
};
class C : virtual public A{
    int c;
};
class D : public B, public C {
    int d;
};

编译后D的内存布局如下图,注意我们这边用的是vs的cl编译器编译后的结果,不同编译器对虚继承的实现也不一样
类D的内存布局
下面是虚函数表的内容
虚函数表
即使我们重复继承了对象A,但是在虚继承作用下还是只有一个类A的内存布局。在虚基类(使用了虚继承关键字的类)中有一个指针vbptr,这个指针指向一个虚继承表,表中记录着表距离类开始位置的偏移和公共变量距离vbptr的偏移(切记是相对于vbptr的偏移),比如B中距离类开始偏移为0,距离公共位置的偏移是20。当我们需要访问公共变量的时候,编译器就需要通过vbptr来寻找具体位置。

为什么虚继承要这样做呢?

为什么需要vbptr这种东西,类的成员变量在哪编译器应该知道的啊?其实vbptr和vfptr作用相似,子类指针类型赋值给一个父类的指针类型时才会展示出作用。
还是上面那一段代码中,其中类B的内存布局是
类B的内存布局
看下B中虚继承表的内容,第一个表示距离类开始的偏移,第二个值表示到公共变量的偏移
在这里插入图片描述
假设我们有一段代码,ptr1中保存的是D类的变量指针,ptr2保存的是B类的变量指针。

    D d;
    B *ptr1 = &d;
    B b;
    B *ptr2 = &b;

我们都用B类指针访问a类成员,在运行期间我们也不清楚这个指针指向的内存到底是什么类型,D类内存中和B类内存中需要偏移不同的值才能找到a变量。如果有虚继承表时我们先去查下偏移多少到a,B类中保存的是8,D类中保存的是20,这样就能准确的找到公共变量的位置了。

0X02附录

参考书籍:《深度探索C++对象模型2012版》
转载请声明来自:https://blog.youkuaiyun.com/lqq_419/article/details/83314932

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值