一 “无继承”情况下的对象构造
考虑下面程序片段:
1 2 3 4 5 6 7 8 9 10 11 |
|
1 把Point类写成c程序,c++标准说这是一种所谓的Plain OI Data。
typedef struct
{
float
x,y,z;
}Point;
如果我们以C++来编译这段代码,观念上,编译器会为Point 声明一个trivial default
constructor、一个trival destructor、 一个trivial copy constructor,以及一个trivial copy
assignment operator。但实际上,编译器会分析这个声明,并为它贴上Plain OI
Data标签。
1>
L1中,观念上Point的trivial default
constructor和trival
destructor都会产生并被调用,然而,事实上那些trivial
members要不是没有被定义,就是没有被调用,程序的行为一如它在c中的表现一样。
2> L5中的Point object
local,同样也是既没有被构造也没有被析构。如果local没有先经初始化,可能会称为一个潜在的程序漏洞-万一第一次使用就需要其初始值。
3> L6被转化为: Point
*heap=__new(sizeof(Point)); 并没有default constructor施行与new运算符所传回的Point
object身上。
4>
L7中,观念上该操作会触发trivial
copy assignment operator进行拷贝搬运操作。然而实际上,该对象被看作一个Plain
OI Data,所以赋值操作将只是像C那样的纯粹位搬移操作。
5>
L9中的操作被转化为:__delete(heap); destructor要不是没有产生就是没有调用。
6>
L10中,函数以传值的方式将local当作返回值传回,这在观念上会触发trivial
copy constructor,不过实际上return操作只是简单的位拷贝操作,因为对象是一个Plain
OI Data。
2
抽象数据类型
class
Point{
public:
Point(float
x=0.0,float y=0.0,float z=0.0):_x(x),_y(y),_z(z){}
//no copy constructor,copy operator or
destructor defined ...
private:
float
_x,_y,_z;
};
我们并没有为Point定义一个copy cosntructor或copy operator,因为默认的位语意(default
bitwise semantics)已经足够。我们也不需要一个destructor,因为程序默认的内存管理方法也已经足够。
1> L1中,有了default
constructor作用与其上,由于global被定义在全局范围中,其初始化操作延迟到程序激活时才开始。
2> L5中,会被加上default
Point constructor的inline expansion;如下:
Point local;
Point local;
local._x=0.0;local._y=0.0;local._z=0.0;
3> L6配置heap Point object:
Point *heap=new Point; //现在被附加上一个对default
Point constructor的有条件调用。
Point *heap=__new(sizeof(Point));
if(Hadp!=0)
heap->Point::Point();
然后才被编译器进行inline
expansion操作,至于heap指针指向local object:
4> L7中: *heap=local;则保持着简单的位拷贝操作。
6> L10中,以传值方式传回local object,情况也是一样,保持着简单的位拷贝操作。
5> L9中的删除操作,并不会导致destructor被调用,仍采用默认的内存管理方法,因为我们并没有明确地提供一个destructor函数实体。
观念上,我们Point class有一个相关的default
copy constructor、copy operator和destructor,然而都是无关痛痒的(trivial),而且编译器实际上根本没有产生他们。
3 为继承做准备
class
Point{
public:
Point(float
x=0.0,float y=0.0):_x(x),_y(y){}
//no destructor,copy cosntructor,or copy
operator defined...
virtual float
z();
protected:
float
_x,_y;
};
virtual函数引入带来的影响:
① 促使每一个Point
object拥有一个virtual table pointer,这个指针提供给我们virtual
接口的弹性,代价每一个object需要额外一个word的空间。
② 我们所定义的constructor被附加一些代码,以便初始化vptr,这些信息必须被附加在任何base class
cosntructors的调用之后,但必须在任何使用者(程序员)供应的码之前。
③ 合成一个copy constructor和一个copy assignment
operator,而且其操作不再是trivial。用在一个Point object被初始化或以一个derived class
object赋值,正确处理vptr指针。
1>
L1的gloabl初始化操作,L1的local初始化操作,L6的heap初始化操作以及L9的heap删除操作,都还和2中的一样。
2> L9中的赋值操作,很可能触发copy assignment
operator的合成,及其调用操作的一个inline expansion。
3> 最戏剧性的冲击发生在以传值方式传回local的那一行,由于copy
constructor的出现,foobar()很可能被转化为下面这样:
//用以支持copy constructor
Point foobar(Point &__result)
{
Point local;
local.Point::Point(0.0,0.0);
//heap的部分没变
__result.Point::Point(local);
//copy constructor的应用
//local对象的destructor将在这里发生
//local.Point::~Point();
return ;
}
如果支持NRV优化,这个函数还会进一步转化为:
//以支持NRV优化
Point foobar(Point &__result)
{
__result.Point::Point(0.0,0.0);
//heap的部分没变
return ;
}
重要注意或提示:
一般而言,如果你的设计之中有有许多函数都要以传值方式传回一个local
class object。那么提供改一个copy
constructor就比较合理-深知即使default memberwise语义已经足够,它的出现会触发NRV。然而,就想上面的例子一样,NRV优化后将不再需要调用copy
constructor。
二 继承体系下的对象构造
当我们顶一个object如下:
T object;
时,如果T有一个constructor(不论是有user提供或是编译器合成),它会被调用。
constructor可能内含大量的隐藏码,因为编译器会扩充每一个constructor,扩充程度视class
T的继承体系而定,一般而言编译器所做的扩充大致如下:
1 所有virtual base class
constructors必须调用,从左到右,从最深到最浅。(正确处理虚基类对象的偏移量offset)
2 所有上一层base class cosntructors必须被调用,以base class的声明顺序为顺序。
3 如果class object有vptr,他们必须设定初值,指向适当的virtual tables。
4 记录在member initialization list中的data
members初始化操作会放进constructor函数本身,并以members的声明顺序为顺序。
5 如果有一个member 并没有出现在member initialization list之中,但它有一个default
constructor,那么default constructor必须被调用。
虚拟继承
存在虚拟继承时,必须保证虚基类子对象的初始化必须有最底层的派生类完成,否则可能会出现多次初始化。方法:
1> 增加一个用以指示virtual base class constructor应不应该调用的参数。
2>
把每一个constructor分类为二,一个针对完整的object,另一个针对subobject。"完整object"版无条件地调用虚基类构造函数;"subobject”版则步调用虚基类构造函数。
vptr初始化语义学
vptr应该在base class constructors调用之后,但是在程序员供应的码或是"member
initialization list"中所列的members初始化操作之前。编译器保证这一点。
这样解决了“在class中限制一组virtual function名单
”的问题,如果每一个constructor都一直的等待到base class constructors执行完之后才设置其对象的vptr,那么每次都能够调用正确的virtual
function实体。
constructor的执行算法通常如下:
1> 在派生类构造函数中,“所有基类构造函数”及“上一层基类“的构造函数会被调用。
2> 上述完成之后,对象的vptr被初始化,指向相关的虚函数表。
3> 如果有成员初始化列表的话,将在构造函数体内扩展开来,这必须在vptr设定之后才惊醒,以免一个虚函数调用。
4> 调用类成员对象的默认构造函数,如果有的话。
5> 最后,执行程序员所提供的码。
但其实不是每个基类构造函数都必须初始化vptr的。vptr必须设定的两种情况:
1> 当一个完整的对象被构造出来时。
2> 当一个subobject constructor调用了一个虚函数是(不论是直接调用或间接调用)
三 对象的赋值(copy assignment opertor)语义学
我们设计一个类,并以一个类对象指定给另一个类对象时,我们有三个选择:
1 什么都不做,因此得以实施默认行为(member copy or bitwise copy)。
如果我们不对Point类供应一个赋值操作符,而光是依赖默认的memberwise
copy,编译器会产生出一个实体吗?
这个答案和copy constructor的情况一样。实际上不会,因为此类已经有了bitwise
copy语义了,所以implicit copy
assignment operator视为无用的,也根本不会合成出来。
2 明确地拒绝一个class object指定给另一个class object。(将copy
assignment operator声明为private,并且不提供定义即可)
3 提供一个explicit copy assignment
operator。只有默认行为所导致的语义不安全或不正确是,我们才设计一个赋值操作符。
一个类对默认的赋值操作符,在一下情况不会表现出bitwise copy 语义
1> 当class内含一个member object,而其class有一个copy assignment opertor时。
2> 当一个class的base class 有一个copy assignment opertor时。
3> 当一个class声明了任何virtual functions(我们一定不能拷贝右端class
object的vptr地址,因为它可能是一个派生类对象)。
4> 当class 继承自一个virtual base class(不论base class 有没有copy assignment
opertor)时。
c++标准上说copy
assignment opertor并不表示bitwise copy semantics是nontrivial。实际上,只有nontivial
instances才会合成出来。
建议尽可能不要允许一个virtual base
class的拷贝操作,甚至提供一个比较奇怪的建议:不要在任何virtual base class 中声明数据。 应为copy
assignment opertor没有什么好的方法避免virtual base class 在派生层次中重复拷贝现象。
?四
解构(析构)语义学
如果class没有定义destructor,那么只有在class内带的member
object(或是class 自己的base class)拥有destructor的情况下,编译器才会自动合成出来一个来。否则,destructor会被视为不需要,也就不会合成(当然不会调用了),或者说是trivial的。
我们应该根据“需要”而不是“感觉”来提供destructor,更不应该因为不确定是否需要一个destructor,于是就提供它。
为了决定class是否需要一个程序层面的destructor(或是constructor),我们应该想一下class
object的声明在哪了结束(或开始)?需要什么操作才能宝座对象的完整。这是我们写程序应该了解的,也是constructor和destructor什么时候起作用的关键。
一个有程序员定义的destructor被扩展的方式类是constructor被扩展的方式,但顺序相反:
1 destructor的函数本身首先被执行。
2 如果class拥有member class objects,而后者拥有destructor,那么会以其声明顺序的相反顺序被调用。
3 如果内带一个vptr,则现在重新设定,指向适当之base class的virtual
table。(并总是设置,只用量中情况下设置,和构造函数相似)
4 如果有任何直接的nonvirtual base classes拥有destructor,他会以声明顺序相反的顺序调用。
5 如果有任何virtual base
class拥有destructor,而当前讨论的这个class是最尾端的class,那么他们会以其原来的构造顺序的相反顺序被调用。