🥒构造函数与拷贝控制
2022-6-10
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property

与其它类一样,继承层次中的类控制本类对象如何进行创建、拷贝、移动和赋值或析构。与任何别的类一样,如果基类或派生类本身没有定义自己的拷贝控制操作,编译器会合成这些操作。同样,在某些情况下,合成的版本可能是被删除的函数。
 

派生类构造函数

  • 基类成员需调用自己的构造完成初始化, 即派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。
  • 如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用。
  • 派生类对象初始化先调用基类构造再调派生类构造。
调用基类构造函数初始化继承自基类的成员,自己再初始化自己的成员(规则参考普通类)。析构、靠别构造、赋值重载也是类似的。
 
如何设计一个不能被继承的类?
将基类的构造函数私有化:
基类A的构造函数私有化后 B 就无法构造对象,因为 B 的构造函数必须要调用 A 的。但是好像A也没办法构造了。
可以这样:
 

虚析构函数

继承对于基类的拷贝控制的最大最直接的影响就是基类必须定义虚析构函数。析构函数是virtual的,将允许继承层级中的对象可以被动态析构。由于使用delete对动态对象的指针进行删除会调用其析构函数,指针指向对象的静态类型可能与动态类型不一样。
如 Quote* 指针可能指向 Bulk_quote 对象,如果要让编译器成功调用 Bulk_quote 的析构函数,就必须让Quote的析构函数是虚函数:
析构函数的virtual属性是可以继承的。因而,Quote的派生类的析构函数也是虚函数,而不管析构函数是合成的还是用户提供的。只要基类的析构函数是虚函数,那么 delete 基类的指针,将会调用正确的析构函数。
如果在基类析构函数不是虚函数的情况下调用实际上指向派生对象的基类指针,行为将是未定义的。
基类的析构函数是:“如果一个需要析构函数那么它同样需要拷贝和赋值操作”的例外。基类几乎总是需要析构函数,这样才能使得析构函数是 virtual 的。如果基类的空析构函数仅仅是为了使其为虚函数,那么类具有析构函数并不意味着其需要赋值操作或拷贝构造函数。
 
虚析构函数将阻止合成移动操作
基类需要虚析构函数对基类和派生类的定义有一个重大的间接影响:如果一个类定义了析构函数,即便使用的是 = default 来使用合成版本的,编译器也不会为这个合成任何移动操作。
 

合成拷贝控制和继承

子类的拷贝构造函数必须调用父类的拷贝构造完成拷贝初始化。
 
在基类和派生类中合成的拷贝控制成员与任何别的合成的构造函数、赋值操作符和析构函数是一样的:它们将逐成员初始化、赋值、销毁类的成员。另外,合成的成员将使用基类的对应操作初始化、赋值或销毁其直接基类子对象。如:合成的 Bulk_quot默认构造函数将调用Disc_quote的默认构造函数,而Disc_quote则继续调用Quote的默认构造函数;
同样的,合成的Bulk_quote拷贝构造函数使用合成的Disc_quote拷贝函数,而Disc_quote拷贝构造函数则继续调用Quote的拷贝构造函数。基类的成员是合成的还是用户定义的都是不要紧的,关键在于基类的对应成员是可访问的,并且不是被删除的函数。
析构函数除了析构其自己的成员,在析构阶段还会销毁其直接基类,这个过程是通过调用直接基类自己的析构函数完成的。然后一直向上调用直到继承层次的根。
Quote 没有合成的移动操作,这是由于它定义了析构函数。当任何时候需要用到 Quote 的移动操作时,就会用拷贝操作来替换它。 Quote 没有移动操作意味着它的派生类也没有移动操作。
 

基类和派生类中被删除的拷贝控制成员

就像其他任何类的情况一样,基类或派生类也能出于同样的原因将其合成的默认构造函数或者任何一个拷贝控制成员定义成被删除的函数。此外,某些定义基类的方式也可能导致有的派生类成员成为被删除的函数:
  • 如果基类的默认构造函数、拷贝构造函数或拷贝赋值操作符或析构函数是被删除的或者不可访问的,那么派生类的对应成员也被定义为被删除的函数
  • 如果基类有一个被删除的或不可访问的析构函数,那么派生类合成的默认和拷贝构造函数将是被删除的函数
  • 与往常一样,编译器不会合成被删除的移动操作。当使用 = default 来请求移动操作时,如果基类的对应操作是被删除的或者不可访问的,或者基类的析构函数是被删除的或不可访问的;
在实践中,如果基类没有默认或拷贝或移动构造函数,派生类也不应该有对应的成员。
 
 

移动操作和继承

由于基类缺少移动操作将抑制派生类合成移动操作,那么在确实需要移动操作时应该在基类中定义移动操作。即便是使用合成版本,也是需要显式定义的。一旦显式定义了移动操作,也就必须显式定义拷贝操作,因为,当定义了移动构造函数或移动赋值操作符时,其合成拷贝构造函数和拷贝赋值操作符将被定义为被删除的。
 
 

派生类的拷贝控制成员

当派生类定义拷贝、移动操作,这些操作需要拷贝、移动整个对象,包括基类成员。
定义派生拷贝、移动构造函数
当定义派生类的拷贝、移动构造函数,通常需要调用基类对应的构造函数来初始化对象的基类部分。如果不调用基类的构造函数,那么编译器将隐式调用基类的默认构造函数,但这肯定是不正确的。如果想要拷贝、移动基类部分,需要在构造函数初始值列表中显式调用基类对象的拷贝、移动构造函数。
 
定义派生赋值操作符
派生类的赋值操作符必须显式对基类部分进行赋值:
此操作从显式调用基类的赋值操作符来对派生对象的基类部分进行赋值开始,基类操作符可以正确处理基类对象的赋值,如:自赋值,并在合适的时机释放左操作数中的资源,并在将rhs的值赋值给左操作数。一旦基类操作符完成后,将继续执行派生类自己的赋值操作。
 
派生类的 operator= 必须要调用基类的operator=完成父类的复制。
 
派生类的析构函数
派生类的成员和基类部分都会在析构函数后的析构阶段隐式销毁。因而,与构造函数和赋值操作符不一样,派生析构函数只需要销毁派生类自己分配的资源。对象的销毁顺序与构造顺序刚好相反:派生析构函数先执行,然后基类构造函数被调用,沿着继承链一直往上执行析构。
子类析构先子后父,子类对象的析构清理是先调用子类析构再调父类析构:
 
在构造函数和析构函数中调用虚函数
派生对象中的基类部分先构建,当基类构造函数执行时,其对象的派生部分还没有初始化。而,派生对象的析构则是反方向的,所以,当基类的析构函数执行时,其派生部分已经被销毁了。所以,当基类的这两个成员执行时,对象是不完全的。
为了兼容这种不完全,当对象正在构造时,其类型被认为与构造函数所在类是一样的;调用虚函数会被解析为构造函数所在类的那个版本。这对于析构函数来说也是一样的。调用虚函数可以是直接调用,也可以是构造或析构函数调用的函数间接调用了这个虚函数。
如果在基类的构造函数中调用了派生类的虚函数版本,此时,派生部分的成员还没有被初始化,如果允许这样做将导致程序无法正确运行。
当构造函数或析构函数调用一个虚函数,所调用的版本是与构造函数或析构函数在同一个类中的版本。
 

继承的构造函数

在新标准中,派生类可以复用直接基类的构造函数。尽管,这不是常规意义上的继承。一个只能继承来自直接基类的构造函数,并且派生类不会继承它的默认、拷贝和移动构造函数,原因是编译器会其合成这些构造函数。
通过using声明可以让派生类继承基类的构造函数:
常规的 using声明只是让名字可见而已。当其运用到构造函数时,using声明将导致编译器生成代码。编译器将生成与基类一一对应的构造函数,这些编译器生成的构造函数有如下形式:
如果派生类有自己的成员,要么执行类内初始化,要么就是默认初始化的。
 
继承的构造函数的特质
using 声明的构造函数不会随着 using 所在的位置改变继承来的构造函数的访问级别。不管 using 身处何处,基类中的 private 构造函数依然是 private 的;protectedpublic 构造函数也是一样。
另外 using 声明不能指定 explicitconstexpr,如果基类构造函数是 explicit 的,派生构造函数具有一样的性质。如果基类构造函数具有默认参数,这些参数不会被继承,相反,派生类将有多个继承而来的构造函数,每一个会将连续省略一个默认实参。
在继承基类的构造函数的同时,派生类可以定义自己的构造函数版本。如果定义的参数列表与基类中的一样,那么基类中的版本将不继承。继承的构造函数不会被认为是用户提供的构造函数,所以,如果一个类只有继承而来的构造函数,那么它还会有合成的默认构造函数。
  • C++
  • 继承中的类作用域多重继承与虚继承
    目录