the semantics of data

class object layout

//64位系统
class A{ };		//sizeof(A)为1
class B : virtual public A{ };	//sizeof(B)为8
class C : virtual public A{ };	//sizeof(C)为8
class D : public B, public C{ };	//sizeof(D)为16

//sizeof(A)为1是因为编译器会安插一个char,使得多个object会有不同的地址

  • 内存布局:

    image-20221016191124550

  • 造成B和C大小为8的原因如下:

    • 语言本身造成的额外负担。若derived class派生自virtual base class,则derived class中含有一个vbptr指针,此指针指向virtual base class subobject或一个相关表格vbtable,而vbtable存放virtual base class subobject地址或编译位置(offset)
      • 注:derived class中包含本身和base class组成了对象,而属于某个基类的对象就是base class subobject
    • 编译器对特殊情况的优化处理。virtual base class A subobject的1 bytes一般放于derived class的固定部分的末端,某些编译器会对empty virtual base class提供特殊支持
      • empty virtual base class不定义任何数据,提供一个virtual interface。某些编译器处理下,一个empty virtual base class被视为derived class object最开始的那一部分,并没有使用任何的额外空间。因为含有member,所以也没有必要安插char
    • Alignment padding的限制。聚合的结构体大小收alignment限制,使其在内存更有效率地被存取
  • nonstatic data members和virtual nonstatic data members都存与class object中,且没有强制定义其排列顺序;static data members存于global data segment,不影响class object大小

  • nonstatic data members在class object中同一个access level的内存排列顺序应和被声明的顺序相同,不受static data members影响

  • class object的同一个access section中members不一定非得连续排列,member的alignment和内部使用的data members可能会介于声明的members间;且多个access section中data members可以自由排序,不用考虑声明顺序

  • access sections的多少并不影响内存大小

    class A
    {
        public:
        ...
        private:
        	float x;
        	static int y;
        private:
        	float z;
        	static int i;
        private:
        	float j;
    }
    

the binding of a data member

  • 现有以下代码:
extern float x;

class A
{
    public:
    	A(float, float, float);
    	float X() const { return x; };
    
    private:
    	float x, y, z;
}
  • 放在现在,X()的返回值肯定是class内部那个,但在以前的编译器,此操作会返回extern那个。因此,这也就产生了两种防御性程序风格:

    • 将所有data member放于class声明最开始处

      class A
      {
          private:
          	float x, y, z;
          
          public:
          	//这样将保证class内部
          	float X() const { return x; };
      }
      
    • 将所有inline member functions,放于class外。inline函数实体,在整个class声明完全看见后,绑定操作才会进行

      class A
      {
          public:
          	A();
          
          private:
          	float x,y,z;
      };
      
      inline float A::X() const
      {
          return x;
      }
      
  • 请思考如下代码:

    typedef int length;
    
    class A
    {
        public:
        	//length被判定为int类型
        	//_val 判定为A::_val
        	void do1( length val ) { _val = val; };
        	length do1() { return _val; };
        
        private:
        	//这里length必须在"本class对它的第一个操作前"被看见.否则先前的判定操作不合法
        	typedef float length;
        	length _val;
    }
    
  • 对于member function的argument list来说,argument list中的名称会在它们第一次遭遇时被适当判断完成。因此,需要将nested type声明放于判断前

data member 的存取

​ 现有如下代码:

A a;
//x的存取成本?
a.x = 0.0;
A* ot = &A;
//通过指针的x的存取成本?
pt->x = 0.0
  • 用指针进行存取:若A为derived class且继承体系中含有virtual base class,且存取的member从virtual base class继承而来,和单一继承、多重继承这样的就有很大差距,因为这个存取操作需要延迟至执行器,经由一个额外的间接导引解决

static data members

  • class object里的static data member,对于class objects和其本身,都不会产生额外负担

  • 无论是复杂的继承关系还是单一的class object,static data member永远只有一个实例

  • static data member每次被取用时,编译器都会对其进行转化

    //a.i = 0;
    A::i = 0; 
    
    //pt->i = 0;
    A::i = 0;
    
  • 多个相同的classs都声明相同的static member,在data segment中这肯定会导致名称冲突,但编译器对其进行name-mangling,也就是暗中对每一个冲突的static data member编码,如此即可获得独一无二的识别代码

    • 不同的编译器有不同的name-mangling,但都包含两点:
      • 运用一个算法推导识别代码
      • 若编译系统必须和使用者交谈,这是识别代码可以被轻易地推导回原来的名称
  • 对于以上代码,虽然使用的member selection operators对static data member进行存取操作,但这只是图方便,实际上static data member并不在class object中,因此也并没有通过class object

若由A中的一函数调用static data member,会发生如下转化:

//do为A中的函数
do().i = 0;

//转化求值
(void) do();
A.i = 0;

若取static data member地址,也只会得到指向其类型的指针,并不会指向其class member

&A::i;

//转化
const int*

nonstatic data members

  • nonstatic data members存放在class object中,需经过explict或implicit class object进行存取,且进行存取操作时,编译器还需要把class object的起始地址加上data member的offset

    A A::do1( const A& pt )
    {
        x += pt.x;
        y += pt.y;
        z += pt.z;
    }
    
    //转化
    A A::do1( A* const this, const A& pt )
    {
        this->x += pt.x;
        this->y += pt.y;
        this->z += pt.z;
    }
    
    --------------------------------------------分割线--------------------------------------------------------
    
    a.y = 0.0;
    
    //起始地址+offset
    &a + (A::y - 1);
    
    • 这里的”-1″操作是因为指向data member的指针的offset总是被加上1,如此编译系统即可区分”指向data member的指针,用以指出class的第一个member”和”指向data member的指针,没有指向任何member”两种情况

      • 取一个nonstatic dat member的地址,会得到它在class中的offset;而取一个绑定在class object上的data member的地址,会得到他在内存中的真实地址
      class B
      {
          public:
          	virtual ~B();
          protected:
          	static B origin;
          	float x,y,z;
      }
      
      //&origin: 当前地址减去offset并加一
      float B::* p1 = &origin.y;
      
      //最终得到val:offset + 1
      float B::* p2 = &B::x;	//B::* 是指向B data member的指针
      
    • 因为offset的值于编译期即可得出,因此存取一个nonstatic data member其实效率和c struct member一样,派不派生也是如此

data member的继承

单一继承

  • 对于derived class object,编译器可以自由其derived class member 和 base class member的排列顺序,但大部分编译器中,base class members会先出现(以上virtual base class除外)

​ 现有以下代码:

class Point2d
{
    public:
    	float x() { return _x; }
    	float y() { return _y; }
    	
    	void operation+=( const Point2d& rhs )
        {
            _x += rhs.x();
            _y += rhs.y();
        }
    	...	//constructor
            
    private:
    	float _x, _y;
}

class Point3d
{
    public:
    	float z() { return _z; }
    
    	void operation+=( const Point3d& rhs )
        {
            Point2d::operator+=( rhs );
            _z += rhs.z();
        }
    	...//constructor
    
    private:
    	float _z;
}
  • 以上这种继承被称为具体继承(concrete inheritance),derived class继承base class的data member和 member function,将之局部化,但这种行为并不会增加空间和时间上的额外负担。没有virtual function时,布局其实和c struct一样

  • 对于具体继承,需要注意因alignment padding膨胀的空间

    //32位
    
    //A大小 4 + 1 + alignment 3
    class A
    {
        private:
        	int val;
        	char c1;
    }
    
    //B大小由8 + 1 + aligment3
    class B : public A
    {
    	private:
        	char c2;
    }
    
    //根据B的意思,C也就是16
    class C : public B
    {
        private:
        	char c3;
    }
    

    也许你会认为,这不是浪费很多空间吗,为什么不让derived class member直接填上base class aligment那一部分?

    image-20221018185652966

    如果是以上布局,又会产生一个问题:继承而得的members会被覆盖

    B* pb;
    A* pa1, pa2;	//可指向ABC
    
    pa1 = pb;
    
    //这将导致c2的值被覆盖掉
    *pa2 = *pa1;
    

多态(单一)继承

​ 现有如下代码:

class Point2d
{
    public:
    	float x() { return _x; }
    	float y() { return _y; }
    	
    	virtual void operation+=( const Point2d& rhs )
        {
            _x += rhs.x();
            _y += rhs.y();
        }
    	
    	virtual float z() { return 0.0; }
    	virtual void z(float) { }
    	...	//constructor
            
    private:
    	float _x, _y;
}

class Point3d
{
    public:
    	float z() { return _z; }
    
    	void operation+=( const Point2d& rhs )
        {
            Point2d::operator+=( rhs );
            _z += rhs.z();
        }
    	...//constructor
    
    private:
    	float _z;
}

//p1和p2可能为Point2d类型,也可能为Point3d类型
void do( Point2d& p1, Point2d& p2 )
{
    p1 += p2;
}
  • 支持多态继承会造成空间和时间上的负担:
    • virtual table,存放virtual functions地址和slots(支持runtime type identification)
    • 每个class object导入一个vptr
    • 优化constructor,在其中设定vptr的初值,使其指向class应对应的virtual table
    • 优化destructor,在其中抹去vptr
  • 对于编译器来说,vptr一般放于class object尾端,如此可以保留base class C对象布局,放在c中亦可使用

多重继承

  • 对于单一继承这种形式,base class object 和 derived class object都是从相同地址开始(例如先前实例中),因此将derived class object指定给base class的指针或引用,编译器不需要针对其修改地址,执行效率很高

​ 现有如下代码:

class Point2d
{
    public:
    ...	//含有virtual函数
        
    protected:
    float _x, _y;
}

class Point3d : public Point2d
{
    ...
    
    protected:
    	float _z;
}

class Vertex
{
    public:
    ... //含有virtual函数
        
    protected:
    	Vertex* next;
}

class Vertex3d : public point3d, public Vertex
{
    ...
    
    protected:
    	float mumble; 
}

Vertex3d v3d;
Vertex* pv;
Point2d* p2d;
Point3d* p3d;

pv = &v3d;
//内部转换 pv = (Vertex*)( ( (char*)&v3d ) + sizeof(Point3d) );

//无需转换
p2d = &v3d;
p3d = &v3d
    
Vertex3d* pv3d;
Vertex* pv;

//若想进行指针的指定操作,还需加个判断
pv = pv3d ? (Vertex*)((char*)pv3d) + sizeof( Point3d );		//pv3d可能为野指针

内存布局:

image-20221018201105749

  • c++并未要求多重derived class object中,base class objects有特定的排列顺序
  • 对于多重派生对象,例如Vertex3d,将地址指定给最左端base class(point3d)时,无需修改地址,因为两者起始地址相同;但往后的base class,需要修改地址,加上或减去介于其中的base class subobjects大小。若存取往后的base class data members,也并不需要付出额外成本,members的位置在编译期已固定,通过offset运算即可得出

虚拟继承

​ iostram library:

//对应如下左图
class ios {...};
class istream : public ios {...};
class ostream : public ios {...};
class iostream : public istream, public ostream {...};

//对应如下右图
class ios {...};
class istream : virtual public ios {...};
class ostream : virtual public ios {...};
class iostream : public istream, public ostream {...};

image-20221018203932844

​ 根据如上可知,虚拟继承可以解决存储多个同一base class的问题(ios),那么这是如何实现的呢?

  • class内若内含virtual base class subobjects,会被分割为两部分:一个不变区域和一个共享区域
    • 不变区域:含有固定的offset,不受影响,可以直接存取
    • 共享区域:也就是virtual base class subobjects,这一区域会受每次派生操作影响而变化,只可以被简介存取

​ 编译期实现策略:

class Point2d
{
    ...
    protected:
    	float _x, _y;
}

class Point3d : virtual public Point2d
{
    ...
    protected:
    	float _z;
}

class Vertex : virtual public Point2d
{
    ...
    protected:
    	Vertex* next;
}

class Vertex3d : public Vertex, public Point3d
{
    ...
    protected:
    	float mumble;
}
  • 一般的布局策略是先安排derived class不变部分,随后建立共享部分

  • 存取class的共享部分:在每一个derived class object中安插一些指针,每个指针指向一个virtual base class

image-20221018205728613

void Point3d::operator+=( const Point3d& rhs )
{
    _x += rhs._x;
    _y += rhs._y;
    _z += rhs._z;
}

//进行如下转换

__vbcPoint2d->_x += rhs.__vbcPoint2d->_x;
_z += rhs._z;
----------------------------------分割线-------------------------
    
Point2d* p2d = pv3d;
//进行如下转换
Point2d* p2d = pv3d ? pv3d->__vbcPoint2d : 0;

​ 然而,这种实现模型却存在两个缺点:

  • 每个对象针对每一个virtual base class含有一个指向其class的指针
  • 随着虚拟继承串链的变长,间接存取层次也会增加。(如三层虚拟派生,则有三次间接存取,也就是三个virtual base class指针)

​ 解决:

  • 对于第一个,引入virtual base class table,virtual base class指针放在table中,编译期会安插一个指针指向virtual base class table

  • 对于第二个,拷贝取得所有的nested virtual base class指针

  • virtual base class最有效的形式:一个抽象virtual base class,不含data member

对象成员和指向data member的指针效率

  • 对于对象成员,在编译期未优化时聚合、封装、继承方式在存取方面都有效率上的差异;优化后都是相同的,且封装并不会带来执行器的效率成本。其中聚合和封装、单一继承效率高,因为单一继承中members被连续存储在derived class中,且offset于编译期就计算出了;但虚拟继承的效率很低
  • 对于指向data member的指针,在编译期未优化时,通过指针间接存取效率相对于直接存取会更低,但优化后都是一样的;单一继承并不会降低效率,但虚拟继承中,因每一层都导入一个额外层次的间接性,因此效率较差
hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » the semantics of data