《深度摸索C++工具模子》念书条记(3)
副标题#e#
在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; }
// ...
};
#p#副标题#e#
这个气势气魄是为了防备以下现象的产生:
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;
// ...
};
由于member function的argument list中的名称会在它们第一次遭遇时被适内地决策完成,因而,对付上述措施片断,length的范例在两 个member function中都被决策为global typedef,当后续再有length的nested typedef声明呈现时, C++ Standard就把稍早的绑定标示为犯科。
(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可以自由分列。(固然当前没有任何编译器会这么做)
#p#分页标题#e#
编译器还大概汇合成一 些内部利用的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.
#p#分页标题#e#
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;
};
对一个多重担任工具,将其地点指定给“第一个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的指针***
#p#分页标题#e#
假如你去取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);