Inside the C++ Object Model学习笔记[Chap5]

来源:互联网 发布:设置桌面软件 编辑:程序博客网 时间:2024/05/17 04:14

5 构造、析构与拷贝语意学

5.0 引言

可以定义和调用一个纯虚函数,但是只能被静态地调用,不能经由虚拟机制调用,如:

class AbstractBase {

       public:

              virtual void interface() const = 0;

};

inline void AbstractBase::interface() const { …… }

class ConcretDerived : public AbstractBase {

       public:

              void interface() const;

};

inline void ConcretDerived::interface() const {

       AbstractBase::interface();

       //……

}

但是否这样做,取决于类的设计者,但是对于纯虚析构函数,类设计者一定得定义,因为每一个派生类析构函数会被编译器加以扩展,以静态调用的方式调用其“每一个虚基类”以及“上一层基类”的析构函数,因而只要缺乏任何一个基类析构函数的定义,链接将会失败。否则就不要把虚析构函数声明为纯虚。

【并不是好的笔记,应该按书中的问题提出到问题解决方式】

5.1 无继承下的对象构造

5.1.1 Plain OI’ Data形式

声明如下的Point(第一版本),在C++标准中称之为Plain OI’ Data声明形式:

typedef struct {

       float _x, _y, _z;

} Point;

编译时,观念上编译器会为Point声明一个trivial默认构造函数,一个trivial析构函数,一个trivial拷贝构造函数,一个trivial拷贝赋值运算符,取址运算符及其const版六个成员函数。但实际上,编译器分析这个声明,并为它贴上Plain OI’ Data标签。

编译器遇到如下定义Point global;时,观念上其构造函数和析构函数都会被产生并被调用,即构造函数在程序的起始处而析构函数在exit()exit()由系统产生,放在main结束之前)处被调用,然而,事实上那些trivial成员要不是没有被定义,就是没被调用,程序的行为一如它在C中表现的一样。

Point *heap = new Point;编译器会转换为Point *heap = _new( sizeof( Point ));注意其中并没有默认构造函数施行于new运算符所传回的Point对象身上。

另外拷贝构造函数,赋值运算符以及析构函数等都一样并没有被产生调用,只是涉及到一些简单的位拷贝操作。

【好像Plain OI’ Data适用于Cstruct形式】

5.1.2 ADT形式(不含虚函数)

Plain OI’ Data重写为如下不含虚函数的ADT形式:

class Point {

       public:

              Point(float x = 0.0, float y = 0.0, float z = 0.0) : _x(x), _y(y), _z(z) {}

              //no other copy constructor, copy operator or destructor

       private:

              float _x;

              float _y;

              float _z;

};

经过如上封装,但是类大小并没有改变,因为对于privatepublicprotected存取权限,或者是成员函数的声明,都不会占用额外的对象空间。

现在定义Point global;将会调用默认的构造函数。

而对Point *heap = new Point;则被附加为一个“对默认构造函数有条件的调用操作”:

Point* heap = _new( sizeof( Point ) );

if ( heap != 0)

      heap->Point::Point();

而对于delete heap;并不会导致析构函数调用,因为并没有明确地提供一个析构函数实体。

5.1.2 ADT形式(含虚函数)

为了继承以及多态,将上述ADT改写为如下带有虚函数的类定义:

class Point {

       public:

              Point(float x = 0.0, float y = 0.0 ) : _x(x), _y(y) {}

              //no other copy constructor, copy operator or destructor

              virtual float z();

       protected:

              float _x;

              float _y;

};

Point global;

Point foobar() {

       Point local;

       Point *heap = new Point;

       *heap = local;

       delete heap;

       return local;

}

引入虚函数后带来的变化:

1. 每一个Point对象拥有一个vptr,指向类的vtbl,这个指针提供了虚拟接口的弹性,但是使得每一个对象需要一个额外的指针空间;

2. 使得类Point定义将会产生如下的膨胀:

① 定义的constructor中被附加了一些代码,以便将vptr初始化,这些代码附在任何基类构造函数的调用之后,但是位于任何用户定义的代码之前:

Point* Point::Point( Point *this, float x, float y) : _x(x), _y(y) {

       this->_vptr_Point = _vtbl_Point;

       this->_x = x;

       this->_y = y;

       return this;

}

② 合成一个拷贝构造函数和一个拷贝赋值运算符,而且其操作不再是trivial,但隐式析构函数仍然是trivial,这种情况下以位位基础的操作可能给vptr带来非法设定。

inline Point* Point::Point( Point *this, const Point& rhs) {

       this->_vptr_Point = _vtbl_Point;

       this->_x = rhs.x;

       this->_y = rhs.y;

       return this;

}

3. 引入虚函数后操作也会发生变化

① 对于Point global;以及Point* heap = new Point;全局对象初始化以及堆对象同ADT的非虚拟函数,调用默认构造函数;

② 对于释放堆对象delete heap;的操作由于没有定义析构函数实体,同上并不会调用析构函数;

③ 对于*heap = local;将会触发内部合成的拷贝赋值运算符;

④ 对于return local;以传值的方式返回局部对象,将会调用合成的拷贝构造函数,即:

Point foobar( Point& _result ) {

       Point local;

       local.Point::Point(0.0, 0.0);

       _result.Point::Point( local );

       local.Point::~Point();  //如果定义或合成了的话

       return;

}

5.2 继承体系下的对象构造

5.2.1 构造函数的扩充概述

对于定义一个对象如下:

T object;

如果T有一个构造函数(不论是用户提供或由编译器合成),那么它会被调用,同时根据类T的继承体系,编译器可能做如下的扩充操作:

1. 记录在成员初始化列表中的数据成员初始化操作会被放进构造函数本身,并以成员声明的顺序作为顺序;

2. 如果有一个成员没有出现在成员初始化列表中,但他有一个默认构造函数,那么该默认构造函数被调用;

3. 在那之前,如果类对象有vptr,那么必须被设定初值,指向适当的vtbl

4. 在那之前,所有上一层的基类构造函数被调用,以基类的声明顺序为顺序(与在成员初始化列表中的顺序没有关联):

  ★ 如果基类被列于成员初始化列表中,那么任何明确指定的参数都应该传递过去;

  ★ 如果基类没有被列于成员初始化列表中,而他有默认构造函数,那么调用默认构构造函数;

  ★ 如果基类是多重继承下的第二或后继的基类,那么this指针必须做调整;

5. 在那之前,所有虚基类构造函数必须被调用,从左到右,从最深到最浅;

  ★ 如过类被列于成员初始化列表中,那么如果有任何明确指定的参数都应该传递过去,若没有列于初始化列表中,但类有一个默认构造函数,就调用构造函数;

  ★ 类中的每一个虚基类subobject的偏移量在执行期可被存取;

  ★ 如果类对象是最底层的类,其构造函数可能被调用,用以支持这个行为的机制也必须放进来。

5.2.2 单一非虚拟继承

显然这是继承中最为简单的构造,根据先基类然后类成员(即成员的构造函数)然后类本身(用户定义的代码)的构造顺序初始化即可。

5.2.2 虚拟继承

对于虚拟继承而言,由于虚拟继承就是为了避免基类在派生类中的多份拷贝问题,对单一虚拟继承而言就没有任何好处,反而会付出空间与存取时间的代价,因此一般只考虑多重继承的情况。

虚拟继承中虚拟基类构造函数的被调用有着明确的定义:只有当一个完整的类对象被定义出来时,它才会被调用;如果对象只是某个完整对象的subobject,它就不会被调用(详细讨论见书P211213)。

5.2.3 vptr初始化语意学

如果调用操作限制在构造函数或析构函数中直接调用,那么将每一个调用操作以静态方式决议,并不用到虚拟机制(详细讨论见书P214215)。

决定虚函数名单的关键在于虚表vtbl,而vtbl又通过vptr处理。Vptr初始化操作处理在基类构造函数调用操作之后,但是在程序员供应的代码或是成员初始化列表中所列的成员初始化之前进行,因此引入vptr初始化后的构造函数执行步骤为:

1. 在派生类构造函数中,所有虚基类及上一层基类的构造函数被调用;

2. 对象的vptr被初始化,指向相关的vtbl

3. 如果有成员初始化列表,那么在构造函数体内展开;

4. 执行自定义的代码。

5.3 对象拷贝语意学

5.3.1 类对象赋值的选择

当设计一个类,并以该类的一个对象赋给另一个类对象时,有如下三种选择:

1. 什么都不作,实施默认行为,即概念上的memberwise与实现上的bitwise拷贝语意;

2. 提供一个显式copy assignment operator

3. 明确地拒绝赋值操作,可以采用将copy assignment operator声明为private,并且不提供定义即可。因为声明为private就不再允许在任何地点(除了成员函数以及友元之中)进行赋值操作;不提供定义则一旦某个成员函数或友元企图影响一份拷贝,程序在链接时会失败。

5.3.2 copy assignment语意

对于支持的只是一个简单的拷贝操作,如没有类成员,没有指针以及不存在资源分配等,那么默认的bitwise copy已经足够而且有效率,没有必要提供自己的copy assignment operator。但是默认行为导致的语意不安全或不正确时,即发生内存泄漏或者别名化等问题时,需要自己提供一个正确的copy assignment operator

一个类对于默认的copy assignment operator,在以下情况下不会表现bitwise copy语意:

1. 类内带一个成员对象,而其类有一个copy assignment operator,不管这个copy assignment operator是明确定义或者隐式合成的;

2. 类的基类有一个copy assignment operator时;

3. 类声明了任何虚函数时;

4. 类继承自一个虚基类,而且不论该基类有没有copy assignment operator时。

5.3.3 继承下的copy assignment运算符

派生类的copy assignment operator将会调用基类的copy assignment operator,然后再执行其他增加的操作。

copy assignment operator在虚拟继承情况下行为不佳,需要小心地设计和说明。

5.4 析构语意学

如果类没有定义析构函数,那么只有在class内带的成员对象(或基类)拥有析构函数的情况下,编译器才会自动合成出一个来,否则析构函数被视为不需要,也就不需要被合成及调用,因此对于定义了一个构造函数,那么就必须提供一个析构函数的想法并不正确。

delete一个对象前没有强制要求得先将其内容清除干净。一个由程序员定义的析构函数被扩展的方式类似构造函数被扩展的方式,但是顺序相反:

1. 如果对象内带一个vptr,那么首先重设指向vtbl

2. 析构函数本身被执行,也就是说vptr会在程序员的代码执行前被重设;

3. 如果类拥有成员类对象,而后者有析构函数,那么他们以其声明的相反顺序被调用;

4. 如果有任何直接的非虚函数基类拥有析构函数,以其声明的相反顺序调用;

5. 如果有任何虚基类拥有析构函数,而当前讨论的这个类是最尾端的类,那么他们会以其原来的构造顺序的相反顺序被调用。

 
原创粉丝点击