《深度探索C++对象模型》读书笔记(3) 数据成员语意学

转自codepub,只做为本人学习使用,如果侵犯了您的权力,请立即通知我删除,谢谢。

 

在visual C++ 6.0中测试如下代码:

  在visual C++ 6.0中测试如下代码:

 #include "iostream"
using namespace std;

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

int main()
{
    cout<<"sizeof(X): "<<sizeof(X)<<endl;
    cout<<"sizeof(Y): "<<sizeof(Y)<<endl;
    cout<<"sizeof(Z): "<<sizeof(Z)<<endl;
    cout<<"sizeof(A): "<<sizeof(A)<<endl;

    return 0;
}

   得出的结果也许会令你毫无头绪

 sizeof(X): 1
sizeof(Y): 4
sizeof(Z): 4
sizeof(A): 8

   下面一一阐释原因:

   (1)对于一个class X这样的空的class,由于需要使得这个class的两个objects得以在内存中配置独一无二的地址,故编译器会在其中安插进一个char.因而class X的大小为1.

   (2)由于class Y虚拟继承于class X,而在derived class中,会包含指向visual base class subobject的指针(4 bytes),而由于需要区分这个class的不同对象,因而virtual base class X subobject的1 bytes也出现在class Y中(1 bytes),此外由于Alignment的限制,class Y必须填补3bytes(3 bytes),这样一来,class Y的大小为8.

   需要注意的是,由于Empty virtual base class已经成为C++ OO设计的一个特有术语,它提供一个virtual interface,没有定义任何数据。visual C++ 6.0的编译器将一个empty virtual base class视为derived class object最开头的一部分,因而省去了其后的1 bytes,自然也不存在后面Alignment的问题,故实际的执行结果为4.

   (3)不管它在class继承体系中出现了多少次,一个virtual base class subobject只会在derived class中存在一份实体。因此,class A的大小有以下几点决定:(a)被大家共享的唯一一个class X实体(1 byte);(b)Base class Y的大小,减去“因virtual base class X而配置”的大小,结果是4 bytes.Base class Z的算法亦同。(8bytes)(c)classs A的alignment数量,前述总和为9 bytes,需要填补3 bytes,结果是12 bytes.

   考虑到visual C++ 6.0对empty virtual base class所做的处理,class X实体的那1 byte将被拿掉,于是额外的3 bytes填补额也不必了,故实际的执行结果为8.

   不管是自身class的还是继承于virtual或nonvirtual base class的nonstatic data members,其都是直接存放在每个class object之中的。至于static data members,则被放置在程序的一个global data segment中,不会影响个别的class object的大小,并永远只存在一份实体。

   ***Data Member的绑定***

   早期C++的两种防御性程序设计风格的由来:

   (1)把所有的data members放在class声明起头处,以确保正确的绑定:

 class Point3d
{
// 在class声明起头处先放置所有的data member
float x,y,z;
public:
float X() const { return x; }
// ...
};

   这个风格是为了防止以下现象的发生:

 typedef int length;

class Point3d
{
public:
// length被决议为global
// _val被决议为Point3d::_val
void mumble(length val) { _val = val; }
length mumble() ...{ return _val; }
// ...

private:
// length必须在“本class对它的第一个参考操作”之前被看见
// 这样的声明将使先前的参考操作不合法
typedef float length;
length _val;
// ...
};

 

   (2)把所有的inline functions,不管大小都放在class声明之外:

 class Point3d
{
public:
// 把所有的inline都移到class之外
Point3d();
float X() const;
void X(float) const;
// ...
};

inline float Point3d::X() const
{
return x;
}

   这个风格的大意就是“一个inline函数实体,在整个class声明未被完全看见之前,是不会被评估求值的”,即便用户将inline函数也在class声明中,对该member function的分析也会到整个class声明都出现了才开始。

  ***Data Member的布局***

   同一个access section中的nonstatic data member在class object中的排列顺序和其被声明的顺序一致,而多个access sections中的data members可以自由排列。(虽然当前没有任何编译器会这么做)

   编译器还可能会合成一些内部使用的data members(例如vptr,编译器会把它安插在每一个“内含virtual function之class”的object内),以支持整个对象模型。

   ***Data Member的存取***

   (1)Static Data Members

   需要注意以下几点:

   (a)每一个static data member只有一个实体,存放在程序的data segment之中,每次程序取用static member,就会被内部转化为对该唯一的extern实体的直接参考操作。

 Point3d origin, *pt = &origin;

// origin.chunkSize = 250;
Point::chunkSize = 250;

// pt->chunkSize = 250;
Point3d::chunkSize = 250;

   (b)若取一个static data member的地址,会得到一个指向其数据类型的指针,而不是一个指向其class member的指针,因为static member并不内含在一个class object之中。

   &Point3d::chunkSize会获得类型如下的内存地址:const int*

   (c)如果有两个classes,每一个都声明了一个static member freeList,那么编译器会采用name-mangling对每一个static data member编码,以获得一个独一无二的程序识别代码。

   (2)Nonstatic Data Members以两种方法存取x坐标,像这样:

 origin.x = 0.0;
pt->x = 0.0;

   “从origin存取”和“从pt存取”有什么重大的差异吗?

   答案是“当Point3d是一个derived class,而在其继承结构中有一个virtual base class,并且并存取的member(如本例的x)是一个从该virtual base class继承而来的member时,就会有重大的差异”。这时候我们不能够说pt必然指向哪一种 class type(因此我们也就不知道编译期间这个member真正的offset位置),所以这个存取操作必须延迟到执行期,经由一个额外的简洁导引,才能够解决。但如果使用origin,就不会有这些问题,其类型无疑是Point3d class,而即使它继承自virtual base class,members的offset位置也在编译时期就固定了。

   ***继承与Data Member***

   (1)只要继承不要多态(Inheritance without Polymorphism)

   让我们从一个具体的class开始:

 class Concrete{
public:
// ...
private:
int val;
char c1;
char c2;
char c3;
};

   每一个Concrete class object的大小都是8 bytes,细分如下:(a)val占用4 bytes;(b)c1、c2、c3各占用1 byte;(c)alignment需要1 byte.

   现在假设,经过某些分析之后,我们决定采用一个更逻辑的表达方式,把Concrete分裂为三层结构:

 class Concrete {
private:
int val;
char bit1;
};

class Concrete2 : public Concrete1 {
private:
char bit2;
};

class Concrete3 : public Concrete2 {
private:
char bit3;
};

   现在Concrete3 object的大小为16 bytes,细分如下:(a)Concrete1内含两个members:val和bit1,加起来是5 bytes,再填补3 bytes,故一个Concrete1 object实际用掉8 bytes;(b)需要注意的是,Concrete2的bit2实际上是被放在填补空间之后的,于是一个Concrete2 object的大小变成12 bytes;(c)依次类推,一个Concrete3 object的大小为16 bytes.

  为什么不采用那样的布局(int占用4 bytes,bit1、bit2、bit3各占用1 byte,填补1 byte)?

   下面举一个简单的例子:

 Concrete2 *pc2;
Concrete1 *pc1_1, *pc1_2;

pc1_1 = pc2;  // 令pc1_1指向Concrete2对象

// derived class subobject被覆盖掉
// 于是其bit2 member现在有了一个并非预期的数值
*pc1_2 = *pc1_1;

   pc1_1实际指向一个Concrete2 object,而复制内容限定在其Concrete subobject,如果将derived class members和Concrete1 subobject捆绑在一起,去除填补空间,上述语意就无法保留了。在pc1_1将其Concrete1 subobject的内容复制给pc1_2时,同时将其bit2的值也复制给了pc1_1.

   (2)加上多态(Adding Polymorphism)

   为了以多态的方式处理2d或3d坐标点,我们需要在继承关系中提供virtual function接口。改动过的class声明如下:

 class Point2d {
public:
Point2d(float x = 0.0, float y = 0.0) : _x(x),_y(y) {};
virtual float z() ...{ return 0.0; }  // 2d坐标点的z为0.0是合理的
virtual void operator+=(const Point2d& rhs) {
_x += rhs.x();
_y += rhs.y();
}
protected:
float _x,_y;
};

   virtual function给Point2d带来的额外负担:

   (a)导入一个和Point2d有关的virtual table,用来存放它声明的每一个virtual function的地址;

   (b)在每一个class object中导入一个vptr;(c)加强constructor和destructor,使它们能设置和抹消vptr.

 class Point3d : public Point2d {
public:
Point3d(float x = 0.0, float y = 0.0,float z = 0.0) : Point2d(x,y),_z(z) {};
float z() { return _z; }
void z(float newZ) { _z = newZ; }
void operator+=(const Point2d& rhs) {  //注意参数是Point2d&,而非Point3d&
Point2d::operator+=(rhs);
_z += rhs.z();
}
protected:
float _z;
};

   自此,你就可以把operator+=运用到一个Point3d对象和一个Point2d对象身上了。

   (3)多重继承(Multiple Inheritance)

   请看以下的多重继承关系:

 class Point2d {
public:
// ...  // 拥有virtual接口
protected:
float _x,_y;
};

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

class Vertex {
public:
// ...  // 拥有virtual接口
protected:
Vertex *next;
};

class Vertex3d : public Point3d,public Vertex {
public:
// ...
protected:
float mumble;
在visual C++ 6.0中测试如下代码:

  对一个多重继承对象,将其地址指定给“第一个base class的指针”,情况将和单一继承时相同,因为二者都指向相同的起始地址,需付出的成本只有地址的指定操作而已。至于第二个或后继的base class的地址指定操作,则需要将地址修改过,加上(或减去,如果downcast的话)介于中间的base class subobjects的大小。

 Vertex3d v3d;
Vertex3d *pv3d;
Vertex *pv;

pv = &v3d;
// 上一行需要内部转化为
pv = (Vertex*)((char*)&v3d) + sizeof(Point3d));

pv = pv3d;
// 上一行需要内部转化为
pv = pv3d ? (Vertex*)((char*)pv3d) + sizeof(Point3d)) : 0;  // 防止可能的0值

   (4)虚拟继承(Virtual Inheritance)

   class如果内含一个或多个virtual base class subobject,将被分隔为两部分:一个不变局部和一个共享局部。不变局部中的数据,不管后继如何衍化,总是拥有固定的offset,所以这一部分数据可以被直接存取。至于共享局部,所表现的就是virtual base class subobject.这一部分的数据,其位置会因为每次的派生操作而变化,所以它们只可以被间接存取。

   以下均以下述程序片段为例:

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

   间接存取主要有以下三种主流策略:

   (a)在每一个derived class object中安插一些指针,每个指针指向一个virtual base class.要存取继承得来的virtual base class members,可以使用相关指针间接完成。

   由于虚拟继承串链得加长,导致间接存取层次的增加。然而理想上我们希望有固定的存取时间,不因为虚拟衍化的深度而改变。具体的做法是经由拷贝操作取得所有的nested virtual base class指针,放到derived class object之中。

 // 在该策略下,这个程序片段会被转换为
void Point3d::operator+=(const Point3d& rhs)
{
_vbcPoint2d->_x += rhs._vbcPoint2d->_x;
_vbcPoint2d->_y += rhs._vbcPoint2d->_y;
_z += rhs._z;
}

   (b)在(a)的基础上,为了解决每一个对象必须针对每一个virtual base class背负一个额外的指针的问题,Micorsoft编译器引入所谓的virtual base class table.

   每一个class object如果有一个或多个virtual base classes,就会由编译器安插一个指针,指向virtual base class table.这样一来,就可以保证class object有固定的负担,不因为其virtual base classes的数目而有所变化。

   (c)在(a)的基础上,同样为了解决(b)中面临的问题,Foundation项目采取的做法是在virtual function table中放置virtual base class的offset.

   新近的Sun编译器采取这样的索引方法,若为正值,就索引到virtual functions,若为负值,则索引到virtual base class offsets.

 // 在该策略下,这个程序片段会被转换为
void Point3d::operator+=(const Point3d& rhs)
{
(this + _vptr_Point3d[-1])->_x += (&rhs + rhs._vptr_Point3d[-1])->_x;
(this + _vptr_Point3d[-1])->_y += (&rhs + rhs._vptr_Point3d[-1])->_y;
_z += rhs._z;
}

   小结:一般而言,virtual base class最有效的一种运用方式就是:一个抽象的virtual base class,没有任何data members.

  ***对象成员的效率***

   如果没有把优化开关打开就很难猜测一个程序的效率表现,因为程序代码潜在性地受到专家所谓的与特定编译器有关的奇行怪癖。由于members被连续储存于derived class object中,并且其offset在编译时期就已知了,故单一继承不会影响效率。对于多重继承,这一点应该也是相同的。虚拟继承的效率令人失望。

   ***指向Data Members的指针***

   如果你去取class中某个data member的地址时,得到的都是data member在class object中的实际偏移量加1.为什么要这么做呢?主要是为了区分一个“没有指向任何data member”的指针和一个指向“的第一个data member”的指针。

   考虑这样的例子:

 float Point3d::*p1 = 0;
float Point3d::*p2 = &Point3d::x;
// Point3d::*的意思是:“指向Point3d data member”的指针类型

if( p1 == p2) {
cout<<" p1 & p2 contain the same value ";
cout<<" they must address the same member "<<endl;
}

   为了区分p1和p2每一个真正的member offset值都被加上1.因此,无论编译器或使用者都必须记住,在真正使用该值以指出一个member之前,请先减掉1.

   正确区分& Point3d::z和&origin.z:取一个nonstatic data member的地址将会得到它在class中的offset,取一个绑定于真正class object身上的data member的地址将会得到该member在内存中的真正地址。

   在多重继承之下,若要将第二个(或后继)base class的指针和一个与derived class object绑定之member结合起来那么将会因为需要加入offset值而变得相当复杂。

 struct Base1 { int val1; };
struct Base2 { int val2; };
struct Derived : Base1, Base2 { ... };

void func1(int Derived::*dmp, Derived *pd)
{
// 期望第一个参数得到的是一个“指向derived class之member”的指针
// 如果传来的却是一个“指向base class之member”的指针,会怎样呢
pd->*dmp;
}

void func2(Derived *pd)
{
// bmp将成为1
int Base2::*bmp = &Base2::val2;
// bmp == 1
// 但是在Derived中,val2 == 5
func1(bmp,pd);
}


   也就是说pd->*dmp将存取到Base1::val1,为解决这个问题,当bmp被作为func1()的第一个参数时,它的值必须因介入的Base1 class的大小而调整:

 // 内部转换,防止bmp == 0
func1(bmp ? bmp + sizeof(Base1) : 0, pd);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值