拷贝构造函数Copy Constructor

  拷贝构造函数不同于默认构造函数,class X的拷贝构造函数要求传递参数中必须有class X对象,当然参数列表可以是多参数的,但是第一个参数必须是class X类,且第二个及后续参数需要给与默认值。

默认的拷贝构造函数的形式:

  当class没有提供一个显式(explicit)的拷贝构造函数时,将有两种处理方式,这与默认构造函数是一致的:1.不生成一个默认拷贝构造函数实体。2.生成一个默认拷贝构造函数实体。这两点依赖于该类的拷贝构造函数是否会是一个nontrivial的。

  无论是处理方式1还是处理方式2,他们的区别在于是否生成了一个默认拷贝构造函数的实体。如果没有显式的提供一个默认拷贝构造函数,当需要进行“以相同class的另一个object作为此object的初值”时,方式1和方式2都是按照逐member初始化的形式来进行拷贝的,当member中有成员也是一个类时,将会递归的把这个属于类的成员又进行逐member的拷贝。

  方式1与方式2的区别在于是否产生了一个默认拷贝构造函数的实体,当然,对于nontrivial情况下生成的默认拷贝构造函数,它的逐member拷贝在某些情况下会不同于trivial情况,如后面所说的对vptr的处理。

位逐次拷贝(Bitwise Copy Semantics):

  位逐次拷贝就是对应于trivial情况。在这种情况下,没有必要为类提供一个默认拷贝构造函数的实体,对于需要拷贝构造函数的情况下时,如X X1=X2;,直接用X2的成员逐一拷贝给X1,且是一种“无脑”的拷贝形式,即像位逐次拷贝的名字一样,他们对应的成员完全一模一样。

  而不需要位逐次拷贝的时候,对于该类来说,它的默认拷贝构造函数就已经是nontrivial的了,此时与默认构造函数类似,以下四种情况下可以判定为nontrivial而不要进行位逐次拷贝:

  1.class内含一个member object且该object属于的class中含有一个copy constructor(无论这个copy constructor是设计类时就有的还是因为其nontrivial特性而合成出来的)。这种情况下X X1=X2;必须合成一个默认拷贝构造函数给X类,且将该member object的copy constructor插入到这个生成的默认拷贝构造函数中。

  2.class继承自一个base class且该base class中存在有一个copy constructor.

  3.class中声明了virtual function.此时如果同级别的两个类间赋值,仍然是位逐次拷贝的,问题主要出现在基类与派生类间的赋值。假设A是基类,B是A的派生类,则 B b;A a=b;时会出现问题。因为虚函数的出现,在类中引入了新成员vptr,而同级别间的赋值不会出现,因为他们属于同一种类,所使用的虚函数都是一致的,因此使用位逐次拷贝而令他们的vptr指向同一张vtbl即可。但当在基类与派生类间赋值时会出现问题,因为派生类的vptr指向的vtbl与基类的vptr指向的vtbl已经不同了(无论是覆写还是新增了虚函数),如果还是像位逐次拷贝那样令vptr指向同一张表格,天知道会出现什么鬼问题,因此需要合成一个默认拷贝构造函数,使a的vptr指向一张新的,被从b中切割的一个vtbl。

  4.class中出现了虚基类。与3类似,同级别的两个类间赋值仍然是位逐次拷贝的,这不会有什么问题。问题主要出现在某个类和它的派生类间的赋值上。因为虚基类的出现,在类中需要引入新成员,这个成员可能是一个指针(或是一个offset)来指向虚基类区域,如果仍按照位逐次拷贝的方式,对于基类使用派生类的指针(或偏移),则对虚基类的寻址将不知道寻到什么鬼地方上去。

  3和4都是很清楚的,因为引入了新成员,所以不能无脑地进行拷贝。对于1,2,因为其成员中有提供默认拷贝构造函数,虽然我们不知道这个成员的默认拷贝构造函数的目的是什么(甚至可能这个成员的默认构造函数只是瞎比写了个玩玩而已),但一旦引入就应该认定对这个成员来说这是nontrivial的,因为该默认拷贝构造函数很可能是为了解决3,4而写的,如果不按照nontrivial的方式为class生成一个默认拷贝构造函数,反而无脑地位逐次拷贝,将破坏这个成员的nontrivial性。

传递参数,返回值与NRV的实现:

1.传递参数:
  当传递一个参数进入函数时,具体是如何实现的呢?

  如void foo(X x0); foo(xx);时xx是如何传递进入foo的:

  有两种方式,根据编译器的不同而不同:

  1.改写函数为引用传递,然后将xx传递给一个临时对象,将这个临时对象引用传递进入函数内部:

  X _temp0;//临时生成的暂时对象

  _temp0.X::X(xx);//调用拷贝构造函数,使它等于xx;

  foo(_temp0)://传递进入函数内部

  这种方式的问题在于foo().本来它是值传递的,但是为了实现这种传参,需要改写为 void foo(X& x0);

  2.拷贝建构:直接将实际参数建构在它应该在的位置上,该位置又函数的活动范围决定。

2.返回值:

  对如返回值,具体是如何实现的呢?

  如X bar() {X xx; .....;return xx;}

  首先,增加一个额外参数_result,这个_result实际上就是最后返回的值,但是是以引用传递的方式直接改写。然后在return前加入一个copy constructor,这个操作用于将返回的xx内容写到_result上。

  bar()转换如下:

  void bar(X& _result)//注意X改为了void

  {

    X xx;

    ..............;//处理xx

    _result.X::X(xx);

    return;

  }

  现在,一个X sb=bar();操作等价于X xx;bar(xx);//这个bar是改写为void后的bar.

3.NRV:Named Return Value

  NRV优化,用于提高效率。

  前面看到,第二点对返回值的处理中,其实xx的出现完全是没有必要的,因为它的出现只是为了处理后赋值给_result,为了xx需要额外付出构造,拷贝构造和函数退出时的析构,这是不值得的.NRV的引入就是为了提高效率,消除xx带来的中间操作。引入NRV后相当于:

  void bar(X& _result)

  {

    _result.X::X();//默认构造函数构造_result;

    ................;//_result

    return;

  }

  可以看到,与2相比,这个操作直接处理_result,而省略了对xx的操作,避免了引入xx。

  但是NRV的引入也会造成问题,如:

  void foo()

  {

    X xx=bar();//本希望在此处有copy constructor

    ....;//使用destructor

  }

  问题出在哪呢?

  当我们使用X xx=bar();时,我们可能本来希望此处有一个copy ctor,原本是应该xx作为_result传入bar,然后bar内生成一个临时对象 temp,在对temp处理完后使用X的拷贝构造函数将temp拷贝到_result也就是xx上。但是!由于引入了NRV,我们发现,temp直接被忽略了,也就是说直接将_result传入并修改,传入后只经历了一个_result::X::X()的默认构造函数后,经过处理就返回了,这个过程中没有经历本希望有的拷贝构造函数!!!而如果我们在 拷贝 构造函数中设计了某些东西,并配对的在foo中针对这些东西使用了destructor,这样我们会发现这个destructor并没有配对上使用了拷贝构造函数的xx,而是配对上了一个只使用了默认构造函数的xx,在这种情况下是我们不希望看到的。

  

原文地址:https://www.cnblogs.com/lxy-xf/p/11044017.html