C++对象模型剖析(十二)一一构造、析构、拷贝语义学(二)

构造、析构、拷贝语义学(二)

上一期我们对一个 class 的定义进行了优化,并讨论了三种变量的初始化情况还有显式处置列表(explicit initialization list)。那现在我们就接着来看看,成员函数的继承把。

  • 为继承做准备

    老样子,我们先给处我们的例子。

    class Point {
    public:
        Point( float x = 0.0, float y = 0.0 )
        	: _x(x), _y(y)	// 注意了,这个是初值化列表,跟我们上节课讲的显式初值化列表完全不同
        {}
        // 没有定义析构函数,拷贝构造函数,或者拷贝运算符
        
        virtual float z();
        
    protected:
        float _x, _y;
    };
    

    由于引入了 virtual function,编译器会对我们的代码进行拓展。

    • 我们所定义的 constructor 被附加上一些代码,以便讲 vptr 初始化。这些代码必须被附在任何 base class constructor 的调用之后,但必须在任何由使用者供应的代码之前。

      Point*
      Point::Point(Point *this, float x, float y)
          : _x(x), _y(y)
      {
      	// 设定 object 的 virtual table pointer
              this->__vptr_Point = __vtbl_Point;
              
           // 拓展 member initialization list
           this->_x = x;
           this->_y = y;
              
           // 传回this对象
           return this;
      }
      
    • 合成一个 copy constructor 和一个 copy assignment operator,而且其操作不再是 trivial(但是implicit destructor 仍然是 trivial)。如果一个 Point object 被初始化或以一个 derived class object赋值,那么以位为基础的操作可能对 vptr 带来非法的设定。

      编译器在优化状态下可能会把object的连续内容拷贝到另一个object身上,而不会实现一个精确地“以成员为基础”的赋值操作。C++ standard 要求编译器尽量延迟 nontrivial members 的实际合成过程,直到真正遇到其使用场景为止。

      还是之前的例子

      Point global;
      
      Point foobar() {
          Point local;
          Point *heap = new Point;
          *heap = local;
          
          delete heap;
          return local;
      }
      
      // 我们看到 *heap = local 这个操作
      // 这个操作就有可能会触发一个 copy assignment operator
      // 的合成,及其调用操作的一个inline expansion (行内扩张)
      // 以this取代heap,以rhs取代local
      
      // 我们在看到return local 这个操作
      // 编译器可能通过优化会将其转换成
      void foobar(Point &__result) {
          Point local;
          local.Point::Point(0.0, 0.0);
          // 非重点
          Point *heap = new Point;
          *heap = local;
          // 合成一个 copy constructor
          __result.Point::Point(local);
          local.Point::~Point();
          
          return;
      } 
      
      // 如果支持NRV优化的化
      void foobar( Point &__result )
      {
          __result.Point::Point( 0.0, 0.0 );
          
          return;
      }
      

      一般而言,如果一个类中有比较多的函数返回一个 local class object,那么为这个类设计一个拷贝构造函数是有必要的。

继承体系下的对象构造

一般而言,编译器会对类的每一个构造函数进行扩张,扩张的程度由类的继承体系而定。编译器所做的扩充操作如下:

  • 记录在 member initialization list 中的 data members 初始化操作会被放进构造函数的函数本体,并以 data member 的声明顺序为顺序。
  • 如果有一个 member 并没有出现在 member initialization list 之中,但它有一个 default constructor,那么该 default constructor 必须被调用。
  • 在这之前,如果class object有virtual table pointers,它们必须被设定初值,指向适当的virtual table
  • 在这之前,所有上一层的 base class constructors 必须被调用,以base class 的声明顺序为顺序
    • 如果base class 都被列在 member initialization list 中,显式指定的参数都应该传过去。
    • 如果 base class 没有被列于 member initialization list 中,而它有 default constructor (或 data memberwise copy constructor),那么就调用它。
    • 如果base class 是多重继承下的第二个或后继的class,那么this指针需要被调整
  • 在这之前,所有的virtual base class constructors 必须被调用,从左到右,从最深到最浅:
    • 如果class 被列于 member initialization list 中,那么如果有任何显式指定的参数,都应该传递过去。如果没有列于list 中,而class有一个default constructor,也应该调用它。
    • 此外,class中的每一个 virtual base class subobject 的偏移位置必须在执行期可被存取
    • 如果class object是最底层的class,其constructor可能被调用:某些用以支持这一行为的机制必须被放进来。

现在给出这个小节的核心用例

class Point {
public:
    Point( float x = 0.0, float y = 0.0 );
    Point( const Point& );
    Point& operator=(const Point&);
    
    virtual ~Point();
    virtual float z() { return 0.0; }
    
protected:
    float _x, _y;
};

书上还给出一个用例,给我们展示了编译器对类的扩充的结果,我们先来看看把

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

	void draw();
};

// 我们看看第二个构造函数的声明
Line::Line(const Point& begin, const Point& end)
	: _end(end), _begin(begin)
{}

// 它会被编译器扩充为
Line* 
Line::Line(Line* this, const Point& beign, const Point& end)
{
	// 因为Point中生成了拷贝构造和赋值运算符
	// 所以编译器直接使用我们定义的函数,无需额外合成
	this->_begin = begin;
	this->_end = end;
	return this;
}

// 当我们执行这样的操作
Line a;
// 由于在Line中并未显式定义构造函数
// 所以编译器会帮我们合成一个
void inline
Line::~Line(Line *this)
{
	// 调用的顺序和析构的顺序相反的,调用栈了解一下
	this->_end.Point:~:Point()l
	this->_begin.Point::~Point();
}

Line b = a;
// 如果是这样的化,编译器就会为我们
// 合成一个拷贝构造函数

Line c;
c = a;
// 如果是这种,编译器就会为我们合成一个
// 赋值运算符

我们在编写拷贝构造函数和赋值运算符的时候,还需要注意一点:我们需要防止自身拷贝。

所以,在书上,作者大大建议我们在进行拷贝之前加上这样一句话

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

能够避免一个多余的无效操作,也能避免在拷贝的时候将原来的对象释放,造成悬空引用。

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值