谈谈对象大小——从字节对齐到对象模型

Posted leijiangtao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了谈谈对象大小——从字节对齐到对象模型相关的知识,希望对你有一定的参考价值。

谈谈对象大小——从字节对齐到对象模型


一. 前言
        这篇文章主要介绍以下从c的结构体变量到c++的类对象中编译器对内存分配做的事情。总而言之,言而总之,这篇文章就是讲述对于一个变量(对象)它的内存布局是怎么样子的。
        为了方便描述,我们按照以下几个层次来讲述:
                1.c中struct的字节对齐
                2.从struct到class的过渡
                3.单继承对对象内存模型的影响
                4.虚函数对对象内存布局的影响
                5.多继承对对象内存模型的影响
                6.虚基类对对象内存布局的影响


二. c中struct的字节对齐
        在谈字节对齐之前,我们先思考一下下面这个结构体大小?

struct node
{
};

        没错,就是一个空结构体,按道理来说,如果没有数据应该内存大小为0,然而事实上并不是这样。它有一个隐藏的1B大小,那是呗编译器安插进去的一个char,这使得这个struct对应的对象在内存中有独一无二的地址。
        下面就来了解字节对齐:


        许多计算机系统对基本数据类型变量的地址做了约束,这样做是为了提高bus(总线)对数据存取的效率。比如,我们如果对一个int类型数据进行读写操作;如果int变量地址不是4的倍数,那么数据就会被分割在多个块中,那么cpu读写该变量就要进行多次访存。所以,字节对齐是操作系统对于性能提高的一种策略。
        不同的系统中这些策略都是有区别的,例如:
                Linux系统:        对于2字节数据类型变量地址必须是2的倍数,其余数据类型变量地址必须是4的倍数。
                Windows系统:  对于大小为k字节的数据类型变量必须是k的倍数。
        而上述中提到的地址必须是x的倍数,换句话说也就是按照k对齐。我们可以更改对齐规则么?可以!不过一般我们不这么做,除非有特殊需求,比如thunk技术。
        先看下面这段代码:


struct node
{
    char mem1;   //sizeof(char) = 1
    int  mem2;   //sizeof(int)  = 4
    char mem3;
};
struct node a;

        sizeof(a)的值应该是多少呢? 5?   12!
        如果从未了解过字节对齐的朋友可能对这个答案感到很不可思议,因为这比本应该占的内存(5字节)的两倍还多。
        但是事实上,按照上述所说的字节对齐规则来说,得到12这个答案其实并不意外。我们先来看来对齐规则:

                1.对于基本数据类型变量按照其字节数大小k对齐。
                2.对于结构体类型变量按照其成员中最大对齐量对齐。            ———保证结构体第一个变量满足对齐规则
                3.对于结构体类型变量其大小要为其成员最大对齐量的整数倍   ———保证结构体数组中每个元素满足对齐规则。

        所以按照上述规则,a的内存布局应该是这样的:
      技术图片

 



        首先,mem1逻辑地址为0,满足按照1字节对齐;
—–规则2
        这时候分配mem2,因为mem2数据类型为int大小为4个字节,所以需要在中间插入3个字节的间隙,所以mem2的逻辑地址为4;
—–规则1
        这时候分配mem3,当前逻辑地址为8,满足按照1字节对齐,所以直接分配给mem3
—–规则1
        此时逻辑地址已经分配了9字节,然而不满足成员最大对齐量(int对齐量为4字节)的整数倍,所以还需要在最后插入3字节间隙。
—–规则3


        可能有的朋友还是对规则3的缘由不是很清楚,为什么还需要最后插入3个字节。如果我们申请一个结构体数组 : struct node arr[2];

        假如我们最后不进行3个字节的间隙插入,那么很显然,即使arr[0]元素的每个成员满足对齐规则,然而数组要保证元素地址连续,那么必定导致arr[1]的第一个成员一定不满足对齐规则。


        如何修改字节对齐的规则呢?请看下面这段代码:

#pragma pack(push,1)  //使结构体按1字节方式对齐,将原来的对齐规则压栈
struct node
{
    char mem1;
    int  mem2;
};
#pragma pack(pop)     //恢复原来的对齐规则

//sizeof(node) = 5 



三. 从struct到class的过渡
        首先,需要注意的一点是,在c++中,我们可以使用struct替换class,因为本质上这两个关键字除了default access section不一样以外,其他都是都无差别。
        所以这里说,从struct到class的过渡的实质,只是在这里,我把struct认为一个数据集合体,没有private data,也没有member function等等,虽然struct可以代替class实现private data、member function、继承等等。
        后面所谈到的继承派生等等都只是指class。


四.单继承对对象内存模型的影响
        首先先来看下这段代码:
//带透明通道颜色信息的顶点结构
class ColorAVertex{
public:
    int _x,_y,_z;
    unsigned char _r,_g,_b,_a;   //颜色rgba三通道值
};

        嗯,经过第二段和第三段的介绍,我们应该很容易推出 sizeof(ColorAVertex) = 16。
        不过,经过分析之后,决定将其分裂成三层结构:


class Vertex{
    int _x,_y,_z;
};
class ColorVertex:public Vertex{
public:
    unsigned char _r,_g,_b;
};
class ColorAVertex:public ColorVertex{
public:
    unsigned char _a;
};

        从设计的角度来看,可能这个结构相对更加合理,但是从实现的角度来说,我们发现一个事情:sizeof(ColorAVertex) = 20
        嗯,要对这个问题追根溯源,我们就需要了解单继承对对象模型产生的影响。我们可以观察上述两种情况对应产生的对象模型:
        技术图片

        可以看出来,在第二种情况,内存布局中间多了一段间隙。而这段间隙的来源就是ColorVertex类要满足规则3.而在ColorAVertex继承ColorVertex的后,_a并不会接着_b之后,而是需要保持那段间隔。因为继承派生是is a 关系,所以在ColorAVertex的内存布局中不能改变ColorVertex的布局结构。不过在其他某些编译器种,并不是这样实现的。
                VC++2015 : sizeof(ColorAVertex) = 20
                GUN GCC   : sizeof(ColorAVertex) = 16
        除此之外,还有一些member类型需要注意:static member 和 member function( 不包括 virtual function ) 。这些类型的成员是不会引起对象内存布局的变化的。这些数据成员并不是存在每个对象的内存模型之中。


          我们再来看下面这种情况:


struct A
{
};
struct B:A
{
};

          在上面我们了解到如果member data为空,那么编译器会安插一个char,那么在这段代码中B的大小是2还是1呢?
          事实上,当B继承A之后,因为A已经安插了一个char了,B就不为空了,所以说sizeof(B) = 1。


五.虚函数对对象内存布局的影响
        在上一段末尾中提到,member function不影响对象内存布局,但是把virtual function排除开外了。因为virtual function作为c++运行时多态的核心,而为了支持这一特性,在对象内部也添加了相应的一些信息。
        例如:下面这段代码:


class A
{
public:
    int a;
    virtual void say(){  printf("A::say()
"); }
};
class B:public A
{
public:
    int b;
    void say(){ printf("B::say()
"); }
};
int main()
{
    A* p = ....;
    p->say();
    return 0;
}

        在以上继承体系中,编译阶段并不能知道A* p真正的类型,而编译阶段就需要进行函数绑定,而真正的函数只有在运行阶段才知道。所以,对象内部必须要存储相应的动态类型信息,这样才能实现动态绑定。而这样的信息就是vptr,也就是虚表指针,指向一个虚函数表。而一个类的虚函数表只有一个,也就是一个类的不同对象vptr值是相同的。
        所以说,拥有虚函数的对象,会多4个字节用于存放vptr虚表指针,而存放位置由编译器决定,一般来说不是对象首部,就是尾部。不过大多数编译器都是放在对象首部,也就是说&vptr=&obj。
        在单继承体系中,如果一个类的父类中已经有vptr了,那么在子类的虚函数只有可能是新添或者重写。而这些操作都是先复制父类的虚表,然后在其上修改。注意:在单继承体系中,对象内存模型中vptr只有一个,也就是后面的操作都是在vptr对应的虚表上覆盖或者新添。

      技术图片

 



六.多继承对对象内存模型的影响
        在多继承体系中,对象的很多东西就会变得十分复杂,这里我们撇开这些复杂的特殊性不谈。只是简单谈谈在对象内存模型中的影响。
        首先看下面这段代码:


class Top{
public:
    int _top;
};
class Left: public Top{
public:
    int _left;
};
class Right: public Top{
public:
    int _right;
};
class Bottom: public Left,public Right{
public:
    int _bottom;
};
    技术图片

        在继承过程中,由于Bottom继承了Left和Right,导致基类Top在Bottom中有两份实体。所以,Bottom的内存布局如下:
          技术图片

        当然,有的时候,这样的重复并不是我们所希望的我,所以对于多继承中,这样的重复继承情况,c++提出了虚拟继承这样的概念来解决。


七.虚基类对对象内存布局的影响

        嗯,如果要花篇幅去深究虚基类里面的种种细节,可能再写几千字也不为过。不过在这里我只是简单的介绍,如果类的继承体系中出现了虚基类会对内存布局产生的影响。

        虚基类的内存布局难点是在于,既要保证多个继承源的情况下,虚基类只能有一个。并且子类的内存布局中不能改变父类的内存布局模式。这里就不探讨解决方案的相关的。直接给出常规编译器的解决方案之一吧(VC++2015)。如果一个类B虚拟继承自类A。相当于类B组合了一个A对象,并且含有一个指针指向A对象。而基于B对象读取A类的相关data或者function都是通过这个指针。所以说,在子类中,A对象只有一个,其他父类中如果虚拟继承了类A,只不过是把这个指针指向了这个A对象,而这些操作都是子类的构造函数完成的。

        观察如下代码:

class Top {
public:
    int _top;
};
class Left : public virtual Top {
public:
    int _left;
};
class Right : public virtual Top {
public:
    int _right;
};
class Bottom : public Left, public Right {
public:
    int _bottom;
};

        对于该继承体系,Bottom的对象内存布局应该如下:

    技术图片

 


 


        如果加上了虚函数呢?

class Top {
public:
    int _top;
    virtual int top(){ return _top; }
};
class Left : public virtual Top {
public:
    int _left;
    virtual int left(){ return _left; }
};
class Right : public virtual Top {
public:
    int _right;
    virtual int right(){ return _right; }
};
class Bottom : public Left, public Right {
public:
    int _bottom;
    virtual int bottom(){ return _bottom; }
};

        对于该继承体系,Bottom的对象内存布局应该如下:

      技术图片

 

        对于不同的编译器,对于虚拟继承的内存布局处理方式可能有区别,这里只是按照VC++2015编译器的解决方式来进行说明。

        当然,本文章对内存模型的布局原因等没有过多着墨介绍。一是本人能力有限,怕言辞有误。二是的确本身c++标准对内存布局并没有很严格的规定,在各个编译器中有的细节设计也有不同之处。所以这里只是作为简单了解。如果想更进一步了解可以查看<Inside the C++ Object Model>一书。

 

谈谈对象大小——从字节对齐到对象模型原创YzlCoder 发布于2017-02-02 22:39:24 阅读数 616  收藏展开
一. 前言        这篇文章主要介绍以下从c的结构体变量到c++的类对象中编译器对内存分配做的事情。总而言之,言而总之,这篇文章就是讲述对于一个变量(对象)它的内存布局是怎么样子的。        为了方便描述,我们按照以下几个层次来讲述:                1.c中struct的字节对齐                2.从struct到class的过渡                3.单继承对对象内存模型的影响                4.虚函数对对象内存布局的影响                5.多继承对对象内存模型的影响                6.虚基类对对象内存布局的影响

二. c中struct的字节对齐        在谈字节对齐之前,我们先思考一下下面这个结构体大小?
struct node{};123        没错,就是一个空结构体,按道理来说,如果没有数据应该内存大小为0,然而事实上并不是这样。它有一个隐藏的1B大小,那是呗编译器安插进去的一个char,这使得这个struct对应的对象在内存中有独一无二的地址。        下面就来了解字节对齐:

        许多计算机系统对基本数据类型变量的地址做了约束,这样做是为了提高bus(总线)对数据存取的效率。比如,我们如果对一个int类型数据进行读写操作;如果int变量地址不是4的倍数,那么数据就会被分割在多个块中,那么cpu读写该变量就要进行多次访存。所以,字节对齐是操作系统对于性能提高的一种策略。        不同的系统中这些策略都是有区别的,例如:                Linux系统:        对于2字节数据类型变量地址必须是2的倍数,其余数据类型变量地址必须是4的倍数。                Windows系统:  对于大小为k字节的数据类型变量必须是k的倍数。        而上述中提到的地址必须是x的倍数,换句话说也就是按照k对齐。我们可以更改对齐规则么?可以!不过一般我们不这么做,除非有特殊需求,比如thunk技术。        先看下面这段代码:

struct node{    char mem1;   //sizeof(char) = 1    int  mem2;   //sizeof(int)  = 4    char mem3;};struct node a;1234567        sizeof(a)的值应该是多少呢? 5?   12!        如果从未了解过字节对齐的朋友可能对这个答案感到很不可思议,因为这比本应该占的内存(5字节)的两倍还多。        但是事实上,按照上述所说的字节对齐规则来说,得到12这个答案其实并不意外。我们先来看来对齐规则:
                1.对于基本数据类型变量按照其字节数大小k对齐。                2.对于结构体类型变量按照其成员中最大对齐量对齐。            ———保证结构体第一个变量满足对齐规则                3.对于结构体类型变量其大小要为其成员最大对齐量的整数倍   ———保证结构体数组中每个元素满足对齐规则。
        所以按照上述规则,a的内存布局应该是这样的:


        首先,mem1逻辑地址为0,满足按照1字节对齐;—–规则2        这时候分配mem2,因为mem2数据类型为int大小为4个字节,所以需要在中间插入3个字节的间隙,所以mem2的逻辑地址为4;—–规则1        这时候分配mem3,当前逻辑地址为8,满足按照1字节对齐,所以直接分配给mem3—–规则1        此时逻辑地址已经分配了9字节,然而不满足成员最大对齐量(int对齐量为4字节)的整数倍,所以还需要在最后插入3字节间隙。—–规则3

        可能有的朋友还是对规则3的缘由不是很清楚,为什么还需要最后插入3个字节。如果我们申请一个结构体数组 : struct node arr[2];
        假如我们最后不进行3个字节的间隙插入,那么很显然,即使arr[0]元素的每个成员满足对齐规则,然而数组要保证元素地址连续,那么必定导致arr[1]的第一个成员一定不满足对齐规则。

        如何修改字节对齐的规则呢?请看下面这段代码:
#pragma pack(push,1)  //使结构体按1字节方式对齐,将原来的对齐规则压栈struct node{    char mem1;    int  mem2;};#pragma pack(pop)     //恢复原来的对齐规则
//sizeof(node) = 5 123456789

三. 从struct到class的过渡        首先,需要注意的一点是,在c++中,我们可以使用struct替换class,因为本质上这两个关键字除了default access section不一样以外,其他都是都无差别。        所以这里说,从struct到class的过渡的实质,只是在这里,我把struct认为一个数据集合体,没有private data,也没有member function等等,虽然struct可以代替class实现private data、member function、继承等等。        后面所谈到的继承派生等等都只是指class。

四.单继承对对象内存模型的影响        首先先来看下这段代码://带透明通道颜色信息的顶点结构class ColorAVertex{public:    int _x,_y,_z;    unsigned char _r,_g,_b,_a;   //颜色rgba三通道值};123456        嗯,经过第二段和第三段的介绍,我们应该很容易推出 sizeof(ColorAVertex) = 16。        不过,经过分析之后,决定将其分裂成三层结构:

class Vertex{    int _x,_y,_z;};class ColorVertex:public Vertex{public:    unsigned char _r,_g,_b;};class ColorAVertex:public ColorVertex{public:    unsigned char _a;};1234567891011        从设计的角度来看,可能这个结构相对更加合理,但是从实现的角度来说,我们发现一个事情:sizeof(ColorAVertex) = 20        嗯,要对这个问题追根溯源,我们就需要了解单继承对对象模型产生的影响。我们可以观察上述两种情况对应产生的对象模型:
        可以看出来,在第二种情况,内存布局中间多了一段间隙。而这段间隙的来源就是ColorVertex类要满足规则3.而在ColorAVertex继承ColorVertex的后,_a并不会接着_b之后,而是需要保持那段间隔。因为继承派生是is a 关系,所以在ColorAVertex的内存布局中不能改变ColorVertex的布局结构。不过在其他某些编译器种,并不是这样实现的。                VC++2015 : sizeof(ColorAVertex) = 20                GUN GCC   : sizeof(ColorAVertex) = 16        除此之外,还有一些member类型需要注意:static member 和 member function( 不包括 virtual function ) 。这些类型的成员是不会引起对象内存布局的变化的。这些数据成员并不是存在每个对象的内存模型之中。

          我们再来看下面这种情况:

struct A{};struct B:A{};123456          在上面我们了解到如果member data为空,那么编译器会安插一个char,那么在这段代码中B的大小是2还是1呢?          事实上,当B继承A之后,因为A已经安插了一个char了,B就不为空了,所以说sizeof(B) = 1。

五.虚函数对对象内存布局的影响        在上一段末尾中提到,member function不影响对象内存布局,但是把virtual function排除开外了。因为virtual function作为c++运行时多态的核心,而为了支持这一特性,在对象内部也添加了相应的一些信息。        例如:下面这段代码:

class A{public:    int a;    virtual void say(){  printf("A::say() "); }};class B:public A{public:    int b;    void say(){ printf("B::say() "); }};int main(){    A* p = ....;    p->say();    return 0;}123456789101112131415161718        在以上继承体系中,编译阶段并不能知道A* p真正的类型,而编译阶段就需要进行函数绑定,而真正的函数只有在运行阶段才知道。所以,对象内部必须要存储相应的动态类型信息,这样才能实现动态绑定。而这样的信息就是vptr,也就是虚表指针,指向一个虚函数表。而一个类的虚函数表只有一个,也就是一个类的不同对象vptr值是相同的。        所以说,拥有虚函数的对象,会多4个字节用于存放vptr虚表指针,而存放位置由编译器决定,一般来说不是对象首部,就是尾部。不过大多数编译器都是放在对象首部,也就是说&vptr=&obj。        在单继承体系中,如果一个类的父类中已经有vptr了,那么在子类的虚函数只有可能是新添或者重写。而这些操作都是先复制父类的虚表,然后在其上修改。注意:在单继承体系中,对象内存模型中vptr只有一个,也就是后面的操作都是在vptr对应的虚表上覆盖或者新添。



六.多继承对对象内存模型的影响        在多继承体系中,对象的很多东西就会变得十分复杂,这里我们撇开这些复杂的特殊性不谈。只是简单谈谈在对象内存模型中的影响。        首先看下面这段代码:

class Top{public:    int _top;};class Left: public Top{public:    int _left;};class Right: public Top{public:    int _right;};class Bottom: public Left,public Right{public:    int _bottom;};12345678910111213141516
        在继承过程中,由于Bottom继承了Left和Right,导致基类Top在Bottom中有两份实体。所以,Bottom的内存布局如下:
        当然,有的时候,这样的重复并不是我们所希望的我,所以对于多继承中,这样的重复继承情况,c++提出了虚拟继承这样的概念来解决。

七.虚基类对对象内存布局的影响
        嗯,如果要花篇幅去深究虚基类里面的种种细节,可能再写几千字也不为过。不过在这里我只是简单的介绍,如果类的继承体系中出现了虚基类会对内存布局产生的影响。
        虚基类的内存布局难点是在于,既要保证多个继承源的情况下,虚基类只能有一个。并且子类的内存布局中不能改变父类的内存布局模式。这里就不探讨解决方案的相关的。直接给出常规编译器的解决方案之一吧(VC++2015)。如果一个类B虚拟继承自类A。相当于类B组合了一个A对象,并且含有一个指针指向A对象。而基于B对象读取A类的相关data或者function都是通过这个指针。所以说,在子类中,A对象只有一个,其他父类中如果虚拟继承了类A,只不过是把这个指针指向了这个A对象,而这些操作都是子类的构造函数完成的。
        观察如下代码:
class Top {public:    int _top;};class Left : public virtual Top {public:    int _left;};class Right : public virtual Top {public:    int _right;};class Bottom : public Left, public Right {public:    int _bottom;};12345678910111213141516        对于该继承体系,Bottom的对象内存布局应该如下:

        如果加上了虚函数呢?
class Top {public:    int _top;    virtual int top(){ return _top; }};class Left : public virtual Top {public:    int _left;    virtual int left(){ return _left; }};class Right : public virtual Top {public:    int _right;    virtual int right(){ return _right; }};class Bottom : public Left, public Right {public:    int _bottom;    virtual int bottom(){ return _bottom; }};1234567891011121314151617181920        对于该继承体系,Bottom的对象内存布局应该如下:


        对于不同的编译器,对于虚拟继承的内存布局处理方式可能有区别,这里只是按照VC++2015编译器的解决方式来进行说明。
        当然,本文章对内存模型的布局原因等没有过多着墨介绍。一是本人能力有限,怕言辞有误。二是的确本身c++标准对内存布局并没有很严格的规定,在各个编译器中有的细节设计也有不同之处。所以这里只是作为简单了解。如果想更进一步了解可以查看<Inside the C++ Object Model>一书。

点赞 2收藏分享
YzlCoder发布了284 篇原创文章 · 获赞 383 · 访问量 59万+他的留言板关注C/C++—— 内存字节对齐规则阅读数 600
内存对齐规则博文来自: Be The Best!


————————————————版权声明:本文为CSDN博主「YzlCoder」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/y1196645376/article/details/54835979

以上是关于谈谈对象大小——从字节对齐到对象模型的主要内容,如果未能解决你的问题,请参考以下文章

怎么确定Java对象的大小

对象模型分析

理解单词对齐

C++从青铜到王者第二篇:C++类和对象(上篇)

将对象列表从片段传递到 recyclerView 适配器

记一次对java对象在内存中的分析