《深度探索c++对象模型》第三章:数据成员

空虚基类与派生类占用的内存

class X{
   };
class Y:public virtual X{
   };
class Z:public virtual X{
   };
class A:public Y,public Z{
   };

在这里插入图片描述
  上述类X,Y,Z,A中都没有显式定义的数据,只表示了它们之间的继承关系,它们的大小都不为0:

书中给出的大小 VS2017测试的大小
sizeof X:1 sizeof X:1
sizeof Y:8 sizeof Y:4
sizeof Z:8 sizeof Z:4
sizeof A:12 sizeof A:8

  这跟编译器的实现有关,下面阐述原因(先解释书中的):
  一个空的类如X,实际并不是空的,它有一个隐晦的1 byte,是编译器安插进去的一个char。这样使得类X的两个对象都有一个char的成员,使得这两个对象在内存中的地址不一样,用来区别这两个对象。
  Y和Z的大小由三部分组成:
1、指向虚基类X的指针(编译器为了兼容,32位和64位系统基本都是4 bytes)
2、虚基类X subobject的1个char也出现在了Y,Z里,因为Y,Z都为空
3、数据对齐填充(alignment)。Y,Z目前是5 bytes,在大部分机器上,群聚的结构体大小会受到alignment的限制,使得它们能够更有效率地在内存中被存取。通常是需要填充到4 bytes的倍数,所以5 bytes会被填充3 bytes,最终为8 bytes
在这里插入图片描述
  A的大小并不是简单的sizeof Y + sizeof Z,如果这样的话,A里面不就存了两份X吗?虚基类在每个派生类中只会有一份实体,class A的组成:
1、被大家共享的唯一一个class X实体,大小为1bytes
2、基类Y的大小减去“因虚基类X而配置”的大小,结果是4 bytes,同理Z也是4 bytes,总共8 bytes
3、class A自己的大小:0 byte
4、数据对齐填充(alignment),目前是9 bytes,经过填充后变为12 bytes

  编译器的另一种策略(VS2017,VC++)把Y,Z中对空的虚基类视为成员,既然有了成员,Y,Z就不为空,也就不需要额外安插1 char,少了这个1 char,也少了数据对齐填充(alignment)的3 bytes,所以Y,Z为4 bytes。同理,类A中class X的实体1 byte被拿掉,于是数据对齐填充(alignment)的3 bytes也不需要了,所以A为8 bytes。
在这里插入图片描述
  c++对象模型尽量以空间优化和存取速度优化的考虑来表现non-static data member,并且保持和C语言的struct兼容。它把数据直接存放在每一个class object之中,对于继承而来的non-static data member(不管是virtual或non-virtual base class)也是如此。
  对于static的数据成员,则被放置在程序的一个全局数据段里(global data segement),它们并不属于某个对象,它们属于整个类。不管产生多少个对象,static的数据成员只有一份实体(即使没有任何object,static的数据成员也已经存在),但是一个template class的static数据成员的行为稍有不同,在7.1节有讨论。
  每一个对象必须要有足够的空间容纳它所有的non-static数据成员,此外还要加上:
1、由编译器自动加上的额外数据成员,用来支持某些语言特性(主要是各种virtual特性,比如虚函数表指针vptr,还有上面提到过的指向虚基类的指针等)
2、数据对齐填充(alignment)的需要

3.1数据成员(Data Member)的绑定

//某个foo.h头文件
extern float x;	//x在别处被定义,此处被引用。这里x是声明不是定义,定义是要分配存储空间的。

//Point3d.h文件
class Point3d{
   
	public:
		Point3d(float,float,float);
		//问题:被回传和设定的x是哪一个x呢?
		float X()const {
   return x;}
		void X(float new_x)const {
    x=new_x; }
		//...
	private:
		float x,y,z;
};

  Point3d::X()返回的是类内部的x。
  一个类对成员的分析,是直到整个class的声明都出现了才开始的(例如上述对Point3d::X()的分析会延迟至class声明的右大括号出现才开始)。因此,在一个inline member function躯体之内的一个data member绑定操作,会在整个class声明完成之后才发生。

3.2数据成员的布局

class Point3d{
   
	public:
		//...
	private:
		float x;
		static List<Point3d*> *freeList;
		float y;
		static const int chunkSize=250;
		float z;
};

  non-static数据成员在对象中的排列顺序和其被声明的顺序一样,任何中间介入的static数据成员例如freeList和chunkSize都不会被放进对象的布局之中。在上述例子中,每一个Point3d的对象由3个float组成,次序是x,y,z,static数据成员存放在程序的数据段中,属于整个类,不属于某个对象。
  c++ Standard要求,在同一个acess section(也就是private,public,protected等区段)中,数据成员的排列只需要满足“较晚出现的数据成员在对象中有较高的地址”即可,即在同一个acess section中,次序按照声明的次序,但是不一定连续,可能因为边界调整(alignment)或者编译器自动合成的一些内部使用的数据成员如虚函数表指针vptr介入这些数据成员到中间。(传统的编译器会把vptr放到所有明确声明的数据成员最后,当然也有编译器放在对象的最前端)
  那不同的acess section的情况呢?
  c++ Standard允许将多个acess section中的数据成员自由排列,不必在乎他们在类中出现的顺序。

class Point3d{
   
	public:
		//...
	private:	//一个acess section
		float x;
		static List<Point3d*> *freeList;
	private:	//另一个acess section
		float y;
		static const int chunkSize=250;
	private		//另一个acess section
		float z;
};

  上述类的对象的大小和组成和3.2节开头的那个一样,只是不同acess section之间的成员顺序由编译器的实现决定,例如y可以放在x前面,z可以放在y前面。但是当前大多数编译器会把多个acess section按照声明的次序合成成一个acess section。

3.3 数据成员的存取

  我自己的理解:所谓存,就是member=xxx的形式,取,就是xxx=member的形式。

static成员

  前面讲过,每一个static成员只存在一个实体,存放在程序的data segment数据段中,被视为一个global变量(只在class生命范围内可见)
  当通过对象调用static成员时,会进行转化

Point3d origin,*pt=&origin;
//chunkSize是一个static成员,
//origin.chunkSize=250;	//这样调用,内部转化为:
Point3d::chunkSize=250;	
//pt->chunkSize=250;	//这样调用,内部转化为:
Point3d::chunkSize=250;	

//foobar().chunkSize=250;	这样调用,内部转化为:
(void)foobar();
Point3d::chunkSize=250;

下面是我在VS2017测试的代码:

#include "pch.h"
#include <iostream>
using namespace std;
class test {
   
	public:
		static const int t1=1;	//t1是const,在类内初始化
		static int t2;
};
int test::t2 = 0;	//t2非const,在类外初始化
int main() {
   
	test t;
	cout << t.t1 <<" "<<t.t2<< endl;	//合法
	cout <<test::t1 <<endl;
	cout << test::t2 << endl;
}

  若获取一个static成员的地址,会得到一个指向其数据类型的指针,而不是指向其类成员的指针,因为static成员并不内含在一个类对象中。例如:
  &Point3d::chunkSize会得到一个const int*的地址而不是Point3d::*类型的地址(指向类对象成员的指针)。
  如果一个程序里定义了两个类,两个类都声明了一个static成员,且两个static成员同名,那么都存放在程序的数据段中时会引起同名冲突,编译器会暗中对每一个static成员编码,得到一个独一无二的程序识别代码,这种手法叫name-mangling,主要做两件事:
  1、一种算法,推导出独一无二的名称
  2、推导出的名称能够还原(万一编译系统(或环境工具)必须与使用者交谈,那么那些独一无二的名称可以轻易被推导回原来的名称)

non-static成员

  non-static成员存在于每一个对象中,必须通过显示的或者隐式的对象才能对non-static成员进行存取。只要程序员在成员函数里直接处理non-static成员,隐式的对象就会出现,例如:

Point3d::translate(const Point3d &pt){
   
	x+=pt.x
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值