C++对象模型学习——构造、析够、拷贝语意学

   考虑下面这个abstract base class 声明:ios

class Abstract_base
{
  public:
    virtual ~Abstract_base() = 0;
    virtual void interface() const = 0;
    virtual const char*
      mumble() const { return _mumble; }
  
  proteceted:
    char *_mumble;
};

    虽然这个class被设计为一个抽象的base class(其中有pure virtual function,使得程序员

Abstract_base不可能拥有实例),但它仍然须要一个显式的构造函数以初始化其data member算法

_mumble。若是没有这个初始化操做,其局部性对象_mumble将没法决定初值,例如:数组

class Concrete_derived : public Abstract_base
{
  public:
    Concrete_derived();
    // ...
};

void foo()
{
  // Abstract_base::_mumble 未被初始化
  Concrete_derived trouble;
  // ...
}

     若是Abstract_base的设计者意图让其每个derived class提供_mumble的初值。然而若是是安全

这样,derived class的惟一要求就是Abstract_base必须提供一个带有惟一参数protected数据结构

constructor:ide

Abstract_base::
Abstract_base( char *mumble_value = 0 )
  : _mumble( mumble_value )
  { }

       通常而言,class的data member应该被初始化,而且只在constructor中或是在class的其余函数

member functions中指定初值。其余任何操做都将被破坏封装性质,使class的维护和修改更加测试

困难。   优化

       一、纯虚函数的存在(Presence of a Pure Virtual Function)

        pure virtual function能够被定义和调用(invoke),不过它只能被静静地调用(invoked

statically),不能经由虚拟机制调用。例如:

// ok:定义pure virtual function
// 但只可能被静态地调用(invoked statically)

inline void 
Abstract_base::interface() const  // 先前声明这是一个pure virtual const function
{
  // ...
}

inline void
Concrete_derived::interface() const 
{
  // 静态调用(static invocation)
  Abstract_base::interface(); // 咱们居然可以调用一个pure virtual function
  
  // ...
}

        要不要这样作,全由class设计者决定。惟一例外是pure virtual destructor:class设计者一

定得定义它。由于每个derived class destructor会被编译器加以扩张,以静态调用的方式调用

其“每个virtual base class”以及“上一层base class”的destructor。所以,只要缺少任何一个

base class destructors的定义,就会致使连接失败。

        这样设计是以C++语言的一个保证为前提:继承体系中每个class object的destructor都会

被调用。因此编译器不能压抑这一调用。而编译器也没有足够的知识合成一个pure virtual

destructor的函数定义。

#include <iostream>

class Abstract_base
{
  public:
    virtual ~Abstract_base() = 0;
    virtual void interface() const = 0;
    virtual const char* mumble() const { return _mumble; }
  
  protected:
    char *_mumble;
};

class Concrete_derived : public Abstract_base
{
  public:
    Concrete_derived() { }
    ~Concrete_derived() { }
    void interface() const;

};

inline void 
Abstract_base::interface() const 
{
  std::cout << "mimiasd调用了Abstract_base::interface()" << std::endl;
}

Abstract_base:: ~Abstract_base() 
{
  std::cout << "mimiasd调用了Abstract_base::~Abstract_base()" << std::endl;
}

inline void
Concrete_derived::interface() const 
{
  Abstract_base::interface();
}

int main()
{
  Concrete_derived concrete;
  
  concrete.interface();
}

       

   一个好的代替方案是,不要把virtual destructor声明为pure。

1、“无继承”状况下的对象构造

     考虑下面这个程序片断:

(1)  Point global;
(2) 
(3)  Point foobar()
(4)  {
(5)    Point local;
(6)    Point *heap = new Point;
(7)    *heap = local;
(8)    // ... stuff ...
(9)    delete heap;
(10)   return local;   
(11) }

     L一、L五、L6表现出三种不一样的对象产生方式:global内存配置、local内存配置和heap内存配

置。L7把一个class object指定给另外一个,L10设定返回值,L9则显式地以delete运算符删除

heap object。

     一个object的生命,是该object的一个执行期属性。local object的生命从L5的定义开始,到

L10为止。global object的生命和整个程序的生命相同。heap object的生命从它被new运算符配

置出来开始,到它被delete运算符摧毁为止。

     下面是Point的第一次声明,能够写成C程序。C++ Standard说这是一种所谓的Plain OI‘ Data

声明形式:

typedef struct
{
  float x, y, z;
} Point;

     若是以C++来编译这段代码。观念上,编译器会为Point声明一个trivial default constructor、

一个trivial destructor、一个trivial copy constructor,以及一个trivial copy assignment

operator。但实际上,编译器会分析这个声明,并为它贴上Plain OI’ Data标签。

      当编译器遇到这样的定义:

(1)   Point global;

       观念上Point的trivial constructor和destructor都会被产生并调用,constructor在程序起始

(startup)处被调用而destructor在程序的exit()处被调用。然而,事实上那些trivial members要

不是没被定义,就是没被调用,程序的行为一如它在C中的表现同样。

       只有一个小小的例外。在C中,global被视为一个“临时性的定义”,由于它没有显示的初始

化操做。一个“临时性的定义”能够在程序中发生屡次。那些实例会被连接器折叠起来,只留下单

独一个实例,被放在程序data segment中一个“特别保留给未初始化之global objects使用”的空

间。因为历史缘由,这块空间被称为BSS,这是Block Started by Symbol的缩写。

       C++并不支持“临时性的定义”,这是由于class构造行为的隐式应用之故。虽然公认这个语言

能够判断一个class objects或是一个Plain OI' Data,但彷佛没有必要搞得那么复杂。因

此,global在C++中被视为彻底定义(它会阻止第二或更多个定义)。C和C++的一个差别就在

于,BSS data segment在C++中相对地不重要。C++的全部全局对象都被以“初始化过的数据”来

对待。

      foobar()函数中的L5,有一个Point object local,一样也是既没有被构造也没有被析够。当

然,Point object local若是没有先通过初始化,可能会成一个潜在的程序“臭虫”——万一第一次

使用它就须要其初值的话(像L7)。至于heap object在L6的初始化操做:

(6)   Point *heap = new Point;

      会被转换为new运算符(由library提供)的调用:

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

      并无default constructor施于new运算符传回的Point object身上。L7对此object有个指派

(赋值,assign)操做,若是local曾被适当地初始化过,一切就没有问题:

(7) *heap = local;

      观念上,这样的指定操做会触发trivial copy assignment operator作拷贝搬运操做。然而实际

上该object是一个Plain Ol‘ Data,因此赋值操做(assignment)将只是像C那样的纯粹位搬移操

做。L9执行一个delete操做。

(9)   delete heap;

      会被转换为对delete运算符(由library提供)的调用:

_delete( heap );

      观念上,这样的操做会触发Point的trival destructor。但一如咱们所见,destructor要不是没

有被产生就是没有被调用。最后,函数以传值(by value)的方式将local当作返回值传回,这

在观念上会触发trivial copy constructor,不过实际上return操做只是一个简单的位拷贝操做,因

为对象是一个Plain Ol’ Data。

      一、抽象数据类型(Abstract Data Type)

       如下是Point的第二次声明,在Public接口之下多了private数据,提供完整的封装性,但没

有提供任何virtual function:

class Point
{
  public:
    Point( float x = 0.0, float y = 0.0, float z = 0.0 )
      : _x( x ), _y( y ), _z( z ) { }
    // mo copy constructor,copy operator
    // or destructor defined ...
    
    // ...
  private:
    float _x, _y, _z;
};

      这个通过封装的Point class,其大小并无改变,仍是三个连续的float。不论private或

public存取层,或是member function的声明,都不会占用额外的对象空间。

      并无为Point定义一个copy constructor或copy operator,由于默认的位语意(default

bitwise semantics)已经足够了。咱们也不须要提供一个destructor,由于程序默认的内存管理

方法也足够了。

     对于global实例:

Point global; // 实施Point::Point( 0.0, 0.0, 0.0 );

     如今有了default constructor做用于其上。因为global被定义在全局范畴中,其初始化操做将

延迟到程序启动(startup)时才开始。

     若是要将class中的全部成员都设定常量初值,那么给予一个explicit initialization list会比较有

效率些(比起意义相同的constructor的inline expansion而言)。甚至在local scope中也是如

此。例如:

void mumble()
{
  Point local1 = { 1.0, 1.0, 1.0 }; 
  Point local2;
  
  // 至关于一个inline expansion
  // explicit initialization会稍微快一些
  // local2._x = 1.0
  // local2._y = 1.0
  // local2._z = 1.0
}

        local1的初始化操做会比local2的有效率些。这是由于当函数的activation record被放进程序

堆栈时,上述initialization list中的常量就能够被放进去local1内存中了。

        Explicit initialization list带来三项缺点:

       1)只有当class members都是public,此法才奏效;   

       2)只能指定常数,由于它们在编译时期就能够被评估求值。

       3)因为编译器并无自动施行之,因此初始化行为的失败可能性会高一些。

       那么,explicit initialization list所带来的效率优势,通常而言不可以弥补其软件工程上的缺

点,然而在某些特殊状况下又不同。例如,或许你以手工打造了一些巨大的数据结构如调色

盘(color palette),或是你正要把一堆常量数据倾倒给程序,那么explicit initialization list 的

效率会比inline constructor好得多,特别是对全局对象(global object)而言。

        在编译器层面,会有一个优化机制用来识别inline constructors,后者简单地提供一个

member-by-member的常量指定操做。而后编译器会抽取出那些值,而且对待它们就好像是

explicit initialization list所供应的同样,而不会把constructor扩展成为一系列的assignment指

令。

       因而,local Point object的定义:

{
  Point local;
  // ...
}

       如今被附加上default Point constructor的inline expansion:

{
  // inline expansion of default constructor
  Point local; 
  local._x = 0.0; local._y = 0.0; local._z = 0.0;
  // ...
}

        L6配置出一个heap Point object:

(6)   Point *heap = new Point;

        如今则被附加一个“对default Point constructor的有条件调用操做”:

// C++伪码
Point *heap = _new( sizeof( Point ) );
if( heap != 0 )
  heap->Point::Point();

        而后才被编译器进行inline expansion操做。至于把heap指针指向local object:

(7)  *heap = local;

        则保持着简单的位拷贝操做。以传值方式传回local object,状况也是同样:

(10)   return local;

        L9删除heap所指的对象:

(9)   delete heap;

        该操做不会致使destructor被调用,由于咱们并无显式提供一个destructor函数实例。

        观念上,咱们的Point class有一个相关的default copy constructor、copy operator、和

destructor。然而它们都是无用的(trivial),并且编译器实际上根本没有产生它们。

      二、为继承作准备

      第三个Point声明,将为“继承性质”以及某些操做的动态决议(dynamic resoluton)作准备。

目前咱们限制对z成员作存取操做:

class Point
{
  public:
    Point( float x = 0.0, float y = 0.0, float z = 0.0 )
      : _x( x ), _y( y ), _z( z ) { }
    // mo copy constructor,copy operator
    // or destructor defined ...
    
    virtual float z();
    // ...
  private:
    float _x, _y, _z;
};

      并无定一个copy constructor、copy operator、destructor。咱们的全部members都以数值

来存储,所以在程序层面的默认语意之下,行为良好。可能virtual functions的导入应该老是附

带着一个virtual destuctor的声明。但这样作在这个例子中对咱们并没有好处。

      virtual functions的导入促使每个Point object拥有一个virtual table pointer。这个指针给我

们提供virtual接口的弹性,其成本是:每个object须要额外的一个word空间。具体影响视状况

而定,这可能有意义,也可能没有意义,必须视它对多态(ploymorphism)设计所带来的实际

效益的比例而定。只有在实际完成以后,才能评估要不要避免之。

      除了每个class object多负担一个vptr以外,virtual function的导入也引起编译器对于咱们

的Point class产生膨胀做用:

     1)咱们所定义的constructor被附加了一些代码,以便将ptr初始化。这些代码必须被附加在

任何base class constructors的调用以后,但必须在任何由使用者供应的代码以前。例如,下面

就是可能的附加结果:

// C++伪码:内部膨胀
Point* Point::Point( Point *this, float x, float y )
         : _x( x ), _y( y )
{
  // 设定object的virtual table pointer(vptr)
  this->_vptr_Point = _vtbl_Point;

  // 扩展member initialization list
  this->_x = x;
  this->_y = y;

  // 传回this对象
  return this;
}

     2)合成一个copy constructor和一个copy assignment operator,并且其操做再也不是

trivaial(但implicit destuctor仍然是trivial)。若是一个Point object被初始化或以一个derived

class object赋值,那么以位为基础(bitwise)的操做可能对vptr带来非法设定。

// C++伪码
// copy constructor的内部合成
inline Point*
Point::Point( Point *this, const Point &rhs )
{
  // 设定object的virtual table pointer( vptr )
  this->_vptr_Point = _vtbl_Point;
  
  // 将rhs坐标中的连续位拷贝到this对象,
  // 或是经由member assignment提供一个member ...
  
  return this;  
}

     编译器在优化状态下可能会把object的连续内容拷贝到另外一个object身上,而不会实现一个精

确地“以成员为基础(memberwise)”的赋值操做。C++ Standard要求编译器尽可能延迟nontrivial

members的实际合成操做,直到真正遇到其使用场合为止。

      L1的global初始化操做、L6的heap初始化操做以及L9的heap删除操做,都仍是和稍早的

Point版本相同,然而L7的memberwise赋值操做:

*heap = local;

      颇有可能触发copy assignment operator的合成,及其调用操做的一个inline expansion(行

内扩张):以this取代heap,而以rhs取代local。

      最戏剧性的冲击发生在以传值方式传回local的那一行(L10)。因为copy constructor的出

现,foobar()颇有可能被转化为下面这样:

// C++伪码:foobar()的转化,
// 用以支持copy constructor

Point foobar( Point &_result )
{
  Point local;
  local,Point::Point( 0.0, 0.0 );
  // heap的部分与前面相同...
  
  // copy constructor的应用
  _result.Point::Point( local );

  // local 对象的destructor将在这里执行。
  // 调用Point定义的destructor:
  // local.Point::~Point();

  return;
}

     若是支持named return value(NRV)优化,这个函数进一步被转化为:

// C++伪码:foobar()的转化,
// 以支持named return value(NRV)优化
Point foobar( Point&_result )
{
  _result.Point::Point( 0.0, 0.0 );

  // heap的部分与前相同......
  
  return;
}

     通常而言,若是你的设计之中,有许多函数都须要以传值方式(by value)传回一个local

class object,例如:

T operator+( const T&, const T& )
{
  T result;
  // ...真正的工做在此
  return result;
}

      那么提供一个copyconstructor就比较合理——甚至即便default memberwise语意已经足够。

它的出现会触发NRV优化。然而,NRV优化后再也不须要调用copy constructor,由于运算结果已

经被直接计算于“将被传回的object”体内了。

2、继承体系下的对象构造

      当咱们定义一个object以下:

T object;

       若是T有一个constructor(不管是由用户提供或是由编译器合成的),它会被调用。这很明

显,比较不明显的是,constructor的调用真正伴随了什么?

       Constructor可能内含大量的隐藏码,由于编译器会扩充每个constructor,扩充程度视

class T的继承体系而定。通常而言编译器所作的扩充操做大约以下:

       1)记录在member initialization list中的data members初始化操做会被放进constructor的函

数本体,并以members的声明顺序为顺序。

       2)若是有一个member并无出如今member initialization list之中,但它有一个default

constructor,那么该default constructor必须被调用。

       3)在那以前,若是class object有virtual table pointer(s),它(们)必须被设定初值,指向适当

的virtual table(s)。

       4)在那以前,全部上一层的base class constructors必须被调用,以base class的声明顺序

为顺序(与member initialization list中的顺序没关联):

        若是base class被列于member initialization list中,那么任何显示指定的参数都应该传递过

去。

        若是base class没有被列于member initialization list中,而它有default constructor(或

default memberwise copy constructor),那么就调用之。

        若是base class是多重继承下的第二或后继的base class,那么this指针必须有所调整。

       5)在那以前,全部virtual base class constructors必须被调用,从左到右,从最深到最浅:

         若是class被列于member initialization list中,那么若是有任何显式指定的参数,都应该传

递过去。若没有列于list之中,而class有一个default constructor,亦应该调用之。

         此外,class中的每个virtual base class subobject的偏移位置(offset)必须在执行期可

被存取。

         若是class object是最底层(most-derived)的class,其constructors可能被调用;某些用以

支持这一行为的机制必须被放进来。

         下面要从“C++语言对classes所保证的语意“这个角度,探讨constructor扩充的必要性。再

次以Point为例,并为它增长一个copy constructor、一个copy operator、一个virtual

destructor,以下所示:

class Point
{
  public:
    Point( float x = 0.0, float y = 0.0 );
    Point( const Point& );            // copy constructor
    Point& operator=( const Point& ); // copy assignment operator
  
    virtual ~Point();                 // virtual destructor
    virtual float z() { return 0; }
    // ...
  
  protected:
    float _x, _y;
};

       在开始介绍并一步步走过以Point为根源的继承体系以前,先很快地看看Line class的声明和

扩充结果,它由_begin和_end两个点构成:

class Line
{
  Point _begin, _end;
  
  public:
    Line( float = 0.0, float = 0.0, float = 0.0, float = 0.0 );
    Line( const Point&, const Point& );

    draw();
    // ...
};

       每个explicit constructor都会被扩充以调用其两个member class objects的constructor。若是咱们定义constructor以下:

Line::Line( const Point &begin, const Point &end )
  : _end( end ), _begin( begin )
{ }

       它被编译器扩充并转换为:

// C++伪码:Line constructor的扩充
Line* Line::Line( Line *this, const Point &begin, const Point &end )
{
  this->_begin.Point::Point( begin );
  this->_end.Point::Point( end );
  return this;
}

      因为Point声明了一个copy constructor、一个copy operator、以及一个destructor(本例为

virtual),因此Line class的implicit copy constructor、copy operator和destructor都将具备具体

效用(nontrivial)。

      当程序员写下:

Line a;

      时,implicit Line destructor会被合成出来(若是Line派生自Point,那么合成出来的

destructor将会是virtual。然而因为Line只是内含Point objects而非继承自Point,因此被合成出

来的destructor只是nontrivial而已)。其中,它的member class objects的destructor会被调用

(以其相反顺序):

// C++伪码:合成出来的Line destrutor
inline void
Line::~Line( Line *this )
{
  this->_end.Point::~Point();
  this->_begin.Point::~Point();
}

     固然,若是Point destructor是inline函数,则每个调用操做会在调用地点被扩展开来。虽然

Point destructor是virtual,但其调用操做(在containing class destructor之中)会被静态地决议

出来。

      相似的道理,当写下:

Line b = a;

       时,implicit Line copy constructor会被合成出来,成为一个inline public member。

       最后,当写下:

a = b;

       时,implicit copy assignment operator会被合成出来,成为一个inline public member。

      关于在产生copy operator的时候,要加入以下的条件过滤:

if( this == &rhs ) return *this;

      防止自我指派(赋值),例如自我赋值以下的失败:

// 使用者提供的copy assignment operator
// 忘记提供一个自我拷贝时的过滤

String& String::operator= ( const String &rhs )
{
  // 这里须要过滤(在释放资源以前)
  delete [] str;
  str = new char[ strlen( rhs.str ) + 1 ];
}

    一、虚拟继承(Virtual Inheritance)

     考虑下面这个虚拟继承:

class Point3d : public virtual Point
{
  public:
    Point3d( float x = 0.0, float y = 0.0 )
      : Point( x, y ), _z( z ) { }
    Point3d( const Point3d& rhs )
      : Point( rhs ), _z( rhs._z ) { }
    ~Point3d();
    Point3d& operator=( const Point3d& );

    virtual float z() { return _z; }
    // ...
  
  protected:
    float _z;
};

   传统的“constructor扩充现象”并无用,这是由于virtual base class的“共享性”之故:

// C++伪码
// 不合法的constructor扩充内容
Point3d*
Point3d::Point3d( Point3d *this, float x, float y, float z )
{
  this->_vptr_Point3d = _vtbl_Point3d;
  this->_vptr_Point3d_Point = _vtbl_Point3d_Point;
  this->_z = rhs._z;
 
  return this;
}

     试着想如下三种类的派生状况:

class Vertex : virtual public Point{ ... };
class Vertex3d : public Point3d, public Vertex { ... };
class PVertex : public Vertex3d{ ... };

    Vertex的constructor必须也调用Point的constructor。然而,当Point3d和Vertex同为Vertex3d

的subobjects时,它们对Point constructor的调用操做必定不能够发生;取而代之的是,做为一个

底层的class, Vertex3d有责任将Point初始化。而更日后的继承,则由PVertex来负责完成“被共

享之Point subobject”的构造。

     传统策略若是要支持“初始化virtual base class”,会致使constructor中有更多的扩充内容,用

以指示virtual base classconstructors应不该该被调用。constructor的函数本体于是必须条件式地

测出传进来的参数,而后决定调用或不调用相关的virtual base class constructors。下面就是

Point3d的constructor扩充内容:

// C++代码
// 在virtual base class状况下的consrtuctor扩充内容
Point3d*
Point3d::Point3d( Point3d *this, bool _most_derived, float x, float y, float z )
{
  if( _most_derived != false )
    this->Point::Point( x, y );

  this->_vptr_Point3d = _vtbl_Point3d;
  this->_vptr_Point3d_Point = _vtbl_Point3d_Point;
  this->_z = rhs._Z;

  return this;
}

     在更深层的继承状况下,例如Vertex3d,调用Point3d和Vertex的constructor时,总会把

_most_derived参数设为false,因而就压制了两个constructor中对Point constructor的调用操

做。

// C++伪码
// 在virtual base class状况下的constructor扩充内容
Vertex3d*
Vertex3d::Vertex3d( Vertex3d *this, bool _most_derived, 
                    float x, float y, float z )
{
  if( _most_derived != false )
    this->Point::Point( x, y );

  // 调用上一层base classes
  // 设定_most_derived为false

  this->Point3d::Point3d( false, x, y, z );
  this->Vertex::Vertex( false, x, y );

  // 设定vptrs
  // user code
  
  return this;
}

    这样的策略得以保持语意的正确无误。举个例子,当咱们定义:

Point3d origin;

    时,Point3d constructor能够正确地调用其Point virtual base class subobject。而当咱们定

义:

Vertex3d cv;

    时,Vertex3d constructor正确地调用Point constructor。Point3d和Vertex的constructor会作

每一件该作的事情——对Point的调用操做除外。若是这个行为是正确的,那么什么是错误的呢?

    在一种状态中,“virtual base class constructors的被调用”有着明确的定义:只有当一个完整的

class object被定义出来(例如origin)时,它才会被调用;若是object的subobject,它就不会被调

用。

    以此为杠杆,咱们能够产生更有效率的constructors。某些新进的编译器把每个constructor

分裂为二,一个针对完整的object,另外一个针对subobject。“完整object”版无条件地调用virtual

base constructors,设定全部ptrs等。“subobject”版则不调用virtual base constructors,也可能

不设vptrs等。

    二、vptr初始化语意(The Semantics of the vptr Initialization)

    当咱们定义一个PVertex object时,constructor的调用顺序是:

Point( x, y );
Point3d( x, y, z );
Vertex( x, y, z );
Vertex3d( x, y, z );
PVertex( x, y, z );

    假设这个继承体系中的每个class都定义了一个virtual function size(),此函数数负责传回

class的大小。若是咱们写: 

PVertex pv;
Point3d p3d;

Point *pt = &pv;

    那么这个调用操做:

pt->size();

    将传回PVertex的大小,而:

pt = &p3d;
pt->size();

    将传回Point3d的大小。

     更进一步,咱们假设这个继承体系中的每个constructor内含一个调用操做,像这样:

Point3d::Point3d( float x, float y, float z )
  : _x( x ), _y( y ), _z( z )
{
  if( spyOn )
    cerr << "Within Point3d::Point3d()"
         << "size: " << size() << endl;
}

    当咱们定义PVertx object时,前述的5个constructors会如何会如何?每一次size()调用会被决

议为PVertex::size()吗?或者每次调用会被决议为“目前正在执行之constructor所对应之class”的

size()函数实例?

     C++语言规则告诉咱们,在Point3d constructor中调用size()函数,必须被决议为

Point3d::size()而不是PVertex::size)。更通常性地说,在一个class(本例为Point3d)的

constructor(和destructor)中,经由构造中的对象(本例为PVertex对象)来调用一个virtual

function,其函数实例应该是在此class(本例为Point3d)中有做用的那个。因为各个

constructor的调用顺序,上述状况是必需的。Constructors的调用顺序是:由根源而末端

(bottom up)、由内而外(inside out)。当base constructor执行时,derived实例尚未被构

造起来。

     意思是,当每个PVertex base class constructors被调用时,编译系统必须保证有适当的

size()函数实例被调用。怎样才能办到这一点?

     若是调用操做限制必须在constructor(或destructor)中直接调用,那么答案十分明显:将每

一个调用操做以静态方式决议,千万不要用到虚拟机制。若是是在Point3d constructor中,就显

式调用Point3d::size()。

      然而若是size()之中又调用一个virtual function,会发生什么事情?这种状况下,这个调用也

必须决议为Point3d的函数实例。而在其余状况下,这个调用是纯正的virtual,必须经由虚拟机

制来决定归向。也就是说,虚拟机制自己必须知道是否这个调用源自于一个constructor中。

       另外一个咱们能够采起的方法是,在constructor(或destructor)内设立一个标志,用静态方

式来决议。而后咱们就能够用标志值做为判断依据,产生条件式的调用操做。

        这的确可行,虽然感受起来有点不够优雅和有效率。

        这个解法方法感受起来比较像是咱们的第一个设计策略失败后的一个策略,而不是釜底抽

薪的办法。根本的解决之道是,在执行一个constructor时,必须限制一组virtual function候选名

单。

        virtual table是决定一个class的virtual function名单的关键。而Virtual table经过vptr。因此

为了控制一个class中所做用的函数,编译系统只要简单地控制住vptr的初始化和设定操做即

可。固然,设定vptr是编译器的责任,任何程序员都没必要操心此事。

        vptr初始化操做的处理,本质而言,这得视vptr在constructor之中“应该在什么时候被初始化”而

定。咱们有三种选择:

        1)在任何操做以前。

        2)在base class constructors调用操做以后,可是程序员供应的代码或是”member

initialization list中所列的members初始化操做“以前。

        3)在每一件事情发生以后。

        答案是2。另两个选择没有什么价值。策略2解决了”在class中限制一组virtual functions名单

“的问题。若是每个constructor都一直等待到其base class constructors执行完毕以后才设定其

对象的vptr,那么每次它都可以调用正确的virtual function实例。

          令每个base class constructor设定其对象的vptr,使它指向相关的virtual table以后,构

造中的对象就能够严格而正确地变成”构造过程当中所幻化出来的每个class“的对象。也就是

说,一个PVertex对象会先造成一个Point对象、一个Point3d对象、一个Vertex对象、一个

Vertex对象。在每个base class constructor中,对象能够与constructor‘s class的完整对象作

比较。对于对象而言,”个体发生学“归纳了”系统发生学“。constructor的执行算法一般以下:

       1)在derived class coinstructor中,”全部virtual base classes“及”上一层base class“的

constructor会被调用。

       2)上述完成以后,对象的vptr(s)被初始化,指向相关的virtual table(s)。

       3)若是有member initialization list的话,将在constructor体内扩展开来。这必须在vptr被设

定以后才作,以避免有一个virtual member function被调用。

        4)最后,执行程序员所提供的代码。

        例如,已知下面这个由程序员定义的PVertex constructor:

PVertex::PVertex( float x, float y, float z )
  : _next( 0 ), Vertex3d( x, y, z ), Point( x, y )
{
  if( spyOn )
    cerr << "Within PVertex::PVertex()"
         << "size: " << size() << endl;
}

        它颇有可能被扩展为:

// C++伪码:
// PVertex constructor 的扩展结果
PVertex*
PVertex::PVertex( PVertex* this, bool _most_derived,
                  float x, float y, float z )
{
  // 条件式地调用virtual base constructor
  if( _most_derived != false )
    this->Point::Point( x, y );

  // 无条件地调用上一层base
  this->Vertex3d::Vertex3d( x, y, z );

  // 将相关的vptr初始化
  this->_vptr_PVertex = _vtbl_PVertex;
  this->_vptr_Point_PVertex = 
        _vtbl_Point_PVertex;

  // 程序员所写的代码
  if( spyOn )
    cerr << "Within PVertex::PVertex()"
         << "size: "
         // 经由虚拟机制调用
         << ( *this->_vptr_PVertex[ 3 ].faddr )( this )
         << endl;

  // 传回构造的对象
  return this;
  
}

     这真是个完美的解答吗?假设咱们的Point constructor定义为:

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

      咱们的Point3d constructor定义为:

Point3d::Point3d( float x, float y, float z )
  : Point( x, y ), _z( z ) { }

       更进一步假设咱们的Vertex和Vertex3d constructors有相似的定义。

       下面是vptr必须被设定的两种状况:

        1)当一个完整的对象被构造起来时。若是咱们声明一个Point对象,则Point constructor必

须设定其vptr。

         2)当一个subobject constructor调用一个virtual function(不管是其直接调用或间接调用)

时。

       若是咱们声明一个PVertex对象,而后因为咱们对其base class constructors的最新定义,其

vptr将再也不须要在每个base class constructor中被设定。解决之道是把constructor分裂为一个

完整的object实例和一个subobject实例。在subobject实例中,vptr的设定能够省略(若是可能

的话)。

       知道了这些以后,你应该可以回答下面的问题了:在class的constructor的member

initialization list中调用该class的一个虚拟函数,安全吗?就实际而言,将此函数施于其class’s

data member的初始化行动中,老是安全的。这是由于,正如咱们所见,vptr保证可以在

member initialization list被扩展以前,由编译器正确地设定好。可是在语意上这多是不安全

的,由于函数自己可能还得依赖未被设立初值的members。因此并不推荐这种作法。然而,

从vptr的总体角度来看,这是安全的。

      什么时候须要供应参数给一个base class constructor?这种状况下在”class的constructor的

member initialization list中“调用该class的虚拟函数,是不安全的。此时,vptr若不是还没有被设

定好,就是被设定指向错误的class。更进一步地说,该函数所存取的任何class‘s data

members必定尚未被初始化。

3、对象复制语意学(Object Copy Semantics)

     当咱们设计一个class,并以一个class object指定另外一个class object时,咱们有三

种选择:

     1)什么都不作,所以得以实施默认行为。

     2)提供一个explicit copy assignment operator。

     3)显式地拒绝把一个class object指定给另外一个class object。

     若是选择第3点,不许将一个class object指定给另外一个class object,那么只要将copy

assignment operator声明为private,而且不提供其定义便可。把它设为private,咱们就再也不允

许于任何地点(除了在member functions以及该class的friends之中)作赋值(assign)操做。

不提供其函数定义,则一旦某个member function或friend企图影响一份拷贝,程序在连接时就

会失败。通常认为这和连接器的性质有关(也就是说并不属于语言自己的性质),因此不是很

使人满意。

      这里要验证copy assignment operator的语意,以及它们如何被模塑出来。再次利用Point

class来帮助讨论:

class Point
{
  public:
    Point( float x = 0.0, float y = 0.0 );
    // ... ( 没有virtual function )
  
  protected:
    float _x, _y;
};

       没有什么理由须要禁止拷贝一个Point object。所以问题就变成了:默认行为是否足够?如

果咱们要支持的只是一个简单的拷贝操做,那么默认行为不但足够并且有效率,咱们没有理由

再本身提供一个copy assignment operator。

       只有在默认行为所致使的语意不安全或不正确时,咱们才须要设计一个copy assignment

operator(memberwise copy及其潜在陷阱)。默认的memberwise copy行为对于咱们的Point

object不安全吗?不正确吗?不,因为坐标都内含数值,因此不会发生”别名化(aliasing)“或”

内存泄漏(memory leak)“。若是咱们本身提供一个copy assignment operator,程序反倒会执

行得比较慢。

       若是咱们不对Point供应一个copy assignment operator,而关是依赖默认的memberwise

copy,编译器会产生出一个实例吗?这个答案和copy constructor的状况同样:实际上不会!由

于此class已经有了bitwise copy语意,因此implicit copy assignment operator被视为毫无用处,

也根本不会被合成出来。

        一个class对于默认的copy assignment operator,在如下状况,不会变现出bitwise copy语

意:

      1)当class内含一个member object,而其class有一个copy assignment operator时。

      2)当一个class的base class有一个copy assignment operator时。

      3)当一个class声明了任何virtual functions(咱们必定不要拷贝右端class object的vptr地

址,由于它多是一个derived class object)时。

       4)当class继承自一个virtual base class(不论此base class 有没有copy operator)时。

       C++ Standard上说copy assignment operators并不表示bitwise copy semantics是

nontrivial。实际上,只有nontrivial instances才会被合成出来。

       因而,对于咱们的Point class,这样的赋值(assign)操做:

Point a, b;
...
a = b;

        由bitwise copy完成,把Point b拷贝到Point a,其间并无copy assignment operator被调

用。从语意上或从效率上考虑,这都是咱们所须要的。注意,咱们仍是可能提供一个copy

constructor,为的是把name return value(NRV)优化打开。copy constructor的出现不该该让

咱们觉得也必定要提供一个copy assignment operator。

         如今要导入一个copy assignment operator,用以说明该operator在继承之下的行为:

inline
Point&
Point::operator=( const Point &p )
{
  _x = p._x;
  _y = p._y;
  
  return *this;
}

       如今派生一个Point3d class(虚拟继承):

class Point3d : virtual public Point
{
  public:
    Point3d( float x = 0.0, float y = 0.0, float z = 0.0 );
    // ...
 
  protected:
    float _z;
};

      若是咱们没有为Point3d定义一个copy assignment operator,编译器就必须合成一个(由于

前述的第二项和第四项理由)。合成而得的东西可能看起来像这样:

// C++伪码:被合成copy assignment operator
inline Point3d&
Point3d::operator=( Point3d* const this, const Point3d &p )
{
  // 调用base class的函数实例
  this->Point::operator=( p );
  
  // memberwise copy the derived class members
  _z = p._z;
  
  return *this;
}

       copy assignmentoperator有一个非正交性状况(nonorthogonal aspect,意指不够理想、不

够严谨的状况),就是它缺少一个member assignment list(也就是平行于member initialization

list的东西)。所以咱们不可以写:

// C++伪码,如下性质并不支持
inline Point3d&
Point3d::operator=( const Point3d &p3d )
  : Point( p3d ), z( p3d._z )
{ }

        咱们必须写成如下两种形式,才能调用base class的copy assignment operator:

Point::operator=( p3d );

        或

( *( Point* )this ) - p3d;

        缺乏copy assignment list,看起来或许只是一件小事,但若是没有它,编译器通常而言就

没有办法压抑上一层base class的copy operator被调用。例如,下面是个Vertex copy

operator,其中Vertex也是虚拟继承自Point:

// class Vertex : Virtual public Point
inline Vertex&
Vertex::operator=( const Vertex &v )
{
  this->Point::operator=( v );
  _next = v._next;
  
  return *this;
}

       如今让咱们从Point3d和Vertex中派生出Vertex3d。下面是Vertex3d的copy assignment

operator:

inline Vertex3d&
Vertex3d::operator=( const Vertex3d &v )
{
  this->Point::operator=( v );
  this->Point3d::operator=( v );
  this->Vertex::operator=( v );
  ...
}

       编译器如何可以在Point3d和Vertex的copy assignment operators中压抑Point 的copy

assignment operators呢?编译器不可以重复传统的constructor解决方案(附加额外的参数)。

这是由于,和constructor以及destructor不一样的是,”取copy assignment operator地址“的操做是

合法的。所以下面这个例子是毫无瑕疵的合法程序代码(虽然它也毫无瑕疵地推到了咱们但愿

把copy assignment operator作得更灵巧的企图):

typedef Point3d& ( Point3d::*pmfPoint3d )( const Point3d& );

pmfPoint3d pmf = &Point3d::operator=;
( x.*pmf )( x );

       然而咱们没法支持它,咱们仍然须要根据其独特的继承体系,安插任何可能个数的参数给

copy assignment operator。这一点在咱们支持由class objects(内含virtual base classes)所

组成的数组的配置操做时,也被证实是很是有问题的。

       另外一个方法是,编译器可能为copy assignment operator产生分化函数(splict functions),

以支持这个class成为most-derived class或成为中间的base class。若是copy assinment

operator被编译器产生的话,那么”split function解决方案“可说是定义明确。但若是它是被class

设计者所完成的,那就不能算是定义明确。例如,一我的如何分化像下面这样的函数呢(特别

当init_bases()是virtual时):

inline Vertex3d&
Vertex3d::operator=( const Vertex3d &v )
{
  init_bases( v );
  ...
}

       事实上,copy assignment operator在虚拟继承状况下行为不佳,须要当心地设计和说明。

许多编译器甚至并不尝试取得正确的语意,它们在每个中间(调停用)的copy assignment

operator中调用每个base class instance,因而形成virtual base class copy assignment

operator的多个实例被调用。而C++ Standard的说法是:

        咱们并无规定那些表明virtual base class的subobjects是否应该被”隐式定义(implicitly

defined)的copy assignment operator“指派(赋值,assign)内容一次以上。

       若是使用一个语言为基础的解决办法,那么应该为copy assignment operator提供一个附加

的”member copy list“。简单地说,任何解决方案若是是以程序操做为基础,就将致使较高的复

杂度和较大的错误倾向。通常公认,这是语言的一个弱点,也是一我的应该老是当心检验其程

序代码的地方(当他使用virtual base classes时)。

        有一种方法能够保证most-derived class 会引起(完成)virtual base class subobject的

copy行为,那就是在derived class的copy assignment operator函数实例的最后,显式调用那个

operator,像这样:

inline Vertex3d&
Vertex3d:operator=( const Vertex3d &v )
{
  this->Point3d::operator=( v );
  this->Vertex::operator=( v );
  // must place this last if your compiler does
  // not suppress intermediate class invocations
  this->Point::operator=( v );
  ...
}

      这并不可以省略subobjects的多重拷贝,但却能够保证语意正确。另外一个解决方案要求把

virtual subobject拷贝到一个分离的函数中,并根据call path,条件化地调用它。

      建议尽量不要容许一个virtual base class 的拷贝操做。甚至:不要在任何virtual base

class中声明数据。

4、对象的效能(Object Efficiency)

     在如下的效率测试中,对象构造和拷贝所需的成本是以Point3d class声明为基准的,从简单

形式逐渐到复杂形式,包括Plain Ol' Data、抽象数据类型(Abstract Data Type,ADT)、单一

继承、多重继承、虚拟继承。如下函数是测试的主角:

Point3d lots_of_copies( Point3d a, Point3d b )
{
  Point3d pC = a;
  
  pC = b;  // (1)
  b = a;   // (2)
  
  return pC;
}

       它内含4个memberwise初始化操做,包括两个参数、一个传回值以及一个局部对象pC。它

也内含两个memberwise拷贝操做,分别是标示为(1)和(2)那两行的pC和b。main()函数如

下:

main()
{
  Point3d pA( 1.725, 0.875, 0.478 );
  Point3d pB( 0.315, 0.317, 0.838 );
  Point3d pC;

  for( int iters = 0; iters < 10000000; iters++ )
    pC = lots_of_copies( pA, pB );

  return 0;
}

       第一个程序中数据类型是一个struct:

struct Point3d { float x, y, z; };

       第二个程序是拥有public数据的class:

class Point3d { public: float x, y, z; };

       第三个测试,惟一改变的是数据的封装以及inline函数的使用,以及一个inline constructor,

用以初始化每个object。class仍然展示出bitwise copy语意,因此常识告诉咱们,执行期的效

率应该相同。

class Point3d 
{
  public:
    inline Point3d( float _x, float _y, float _z )
        : x( _x ), y( _y ), z( _z ) { }
  
    inline Point3d lots_of_copies( Point3d b )
    {
      Point3d pC = *this;
  
      pC = b;  // (1)
      b = *this;   // (2)
  
      return pC;
    }

  private:
   float x, y, z;
};

       如今修改main()函数的初始化过程:

Point3d pA;
  pA.x = 1.725; pA.y = 0.875; pA.z = 0.478;
  Point3d pB; 
  pB.x = 0.315; pB.y = 0.317; pB.z = 0.838;

      封装和未封装过的两种Point3d声明之间,另外一个差别是关于下一行的语音:

Point3d pC;

       若是使用ADT表示法,pC会以其default constructor的inline expansion自动进行初始化——

甚至虽然在此例而言,没有初始化也很安全。从某一个角度来讲,虽然这些差别实在小,但它

们扮演警告角色,警告说“封装加上inline支持,彻底至关于C程序中的直接数据存取”。从另外一

个角度来讲,这些差别并不具备什么意义,所以也就没有理由放弃“封装”特性在软件工程上的利

益。它们是一些你得记在心中以备特殊状况下可以派上用场的东西。

        下一个测试,把Point3d的表现法切割为三个层次的单一继承:

class Point1d{}; // x
class Point2d : public Point1d{}; // y
class Point3d : public Point2d{}; // z

       

       下面的多重继承,通常认为是比较高明的设计。因为其member的分布,它完成了任务:

class Point1d{}; // x
class Point2d{}; // y
class Point3d : public Point1d, public Point2d{}; // z

       

         因为Point3d class仍然显现出bitwise copy语意,因此额外的多重继承关系不该该在

memberwise的对象初始化操做或拷贝操做上增长成本。

         下面是单层的虚拟继承:

class Point1d{}; // x
class Point2d : public virtual Point1d{}; // y
class Point3d : public Point2d{}; // z

         再也不容许class拥有bitwise copy语意(第一层虚拟继承不容许之,第二层继承则更加复

杂)。合成型的inline copy constructor和copy assignment operator因而被产生出来,并派上用

场,这致使效率成本上的一个重大增长。

5、析够语意学(Semantics of Destruction)

       若是class没有定义destructor,那么只有在class内含的member object(抑或class本身的

base class)拥有destructor的状况下,编译器才会自动合成出一个来。不然,destructor被视为

不须要,也就不须要被合成(固然更不须要被调用)。例如,咱们的Point,默认状况下并无

被编译器合成出来一个destructor——甚至虽然它拥有一个virtual function:

class Point
{
  public:
    Point( float x = 0.0, float y = 0.0 );
    Point( const Point& );

    virtual float z();
    // ...
  private:
    float _x, _y;
};

       相似的道理,若是咱们把两个Point对象组合成一个Line class:

class Line
{
  public:
    Line( const Point&, const Point& );
    // ...

    virtual draw();
    // ...
  
  protected:
    Point _begin, _end;
};

      LIne也不会拥有一个合成出来的destructor,由于Point并无destructor。

      当咱们从Point派生出Point3d(即便是一种虚拟派生关系)时,若是咱们没有声明一个

destructor,编译器就没有必要合成一个destructor。

      不论Point仍是Point3d,都不须要destructor,为它们提供一个destructor反而是低效率的。

应该拒绝那种被称为“对称策略”的奇怪想法:“你已经定义了一个constructor,因此你觉得提供

一个destructor也是天经地义的事”。事实上,应该由于“须要”而非“感受”来提供destructor,更不

要由于不肯定是否须要一个destructor,因而就提供它。

       为了决定class是否须要一个程序层面的destructor(或是constructor),请想一想一个class

object的生命在哪里结束(或开始)?须要什么操做才能保证对象的完整?这是写程序时比较需

要了解的(或是你的class使用者比较须要了解的)。这也是constructor和destructor何时其

做用的关键。例如:

{
  Point pt;
  Point *p = new Point3d;
  foo( &pt, p );
  ...
  delete p;
}

       咱们看到,pt和p在做为foo()函数参数以前,都必须初始化为某些坐标值。这时候须要一个

constructor,不然使用者必须显示提供坐标值。通常而言,class的使用者没有办法检验一个

local变量或heap变量以知道它们是否被初始化。把constructor想象为程序的一个额外负担是错

误的,由于它们的工做有其必要性。若是没有它们,抽象化(abstraction)的使用就会有错误

的倾向。

       当咱们显示地delete掉p,会如何?有任何程序上必须处理的吗?是否须要在delete以前这

么作:

p->x( 0 ); p->y( 0 );

        固然不须要。没有任何理由说在delete一个对象以前先得将其内容清除干净。也不须要归还

任何资源。在结束pt和p的生命以前。没有任何“class使用者层面”的程序操做是绝对必要的,因

此,也就不必定须要一个destructor。 

        然而请考虑咱们的Vertex class,它维护了一个由紧邻的“顶点”所造成的链表,而且当一个

顶点的生命结束时,在链表上来回移动以完成删除操做。若是这(或其余语意)正是程序员所

须要的,那么这就是Vertex destructor的工做。

        当咱们从Point3d和Vertex派生出Vertex3d时,若是咱们不供应一个explicit Vertex3d

destructor,那么咱们仍是但愿Vertex destructor被调用,以结束一个Vertex3d object。所以编译

器必须合成一个Vertex3d destructor,其惟一任务就是调用Vertex destructor。若是咱们提供一

个Vertex3d destructor,编译器会扩展它,使它调用Vertex destructor(在咱们所供应的程序代

码以后)。一个由程序员定义的destructor被扩展的方式相似constructor被扩展的方式,但顺序

相反:

        1)destructor的函数本体如今被执行,也就是说vptr会在程序员的代码执行前被重设

(reset)。

        2)若是class拥有member class objects,然后者拥有destructors。那么它们会以其声明数

怒的相反顺序被调用。

        3)若是object内含一个vptr,那么首先重设(reset)相关的virtual table。

        4)若是有任何直接的(上一层)nonvirtual base classes拥有destructor,它们会议其声明

顺序的相反顺序被调用。

        5)若是有任何virtual base classes拥有destructor,而目前讨论的这个class是最尾端

(most-derived)的class,那么它们会以其原来的构造顺序的相反顺序被调用。

        就像constructor同样,目前对于destructor的一种最佳实现策略就是维护两份destructor实

例:

        1)一个complete object实例,老是设定好vptr(s),并调用virtual base class destructor。

        2)一个base class subobject实例;除非在destructor函数中调用一个vritual function,不然

它毫不会调用virtual base class destructors并设定vptr。

       一个object的生命结束于其destructor开始执行之时。因为每个base class destructor都轮

番被调用,因此derived object实际上变成了一个完整的object。例如一个PVertex对象归还其内

存空间以前,会依次变成一个Vertex3d对象、一个Vertex对象,一个Point3d对象,最后称为一

个Point对象。当咱们在destructor中调用member functions时,对象的蜕变会由于vptr的从新设

定(在每个destructor中,在程序员所供应的代码执行以前)而受到影响。

相关文章
相关标签/搜索