构造函数与拷贝控制
虚析构函数
继承关系对基类拷贝控制最直接的影响是基类通常应该定义一个虚析构函数,这样就能动态分配继承体系中的对象了。
当 delete 一个动态分配的对象的指针时将执行析构函数。如果该指针指向继承体系中的某个类型,则有可能出现指针的静态类型与被删除对象的动态类型不符的情况。
通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本。
析构函数的虚属性也会被继承。只要基类的析构函数是虚函数,就能确保当 delete 基类指针时将运行正确的析构函数版本。
经验准则:如果一个类需要析构函数,那么它也同样需要拷贝和赋值操作。
基类的虚构函数并不遵循上述准则,一个基类总是需要析构函数,而且它能将析构函数设定为虚函数。此时,该析构函数为了成为虚函数而令内容为空,显然无法由此推断该基类还需要赋值运算符或拷贝构造函数。
虚析构函数将阻止合成移动操作
基类需要一个析构函数这一事实还会对基类和派生类的定义产生另一个间接的影响:
如果一个类定义了析构函数,即使它通过 =default 的形式使用了合成的版本,编译器也不会为这个类合成移动操作。
合成拷贝控制与继承
基类或派生类的合成拷贝控制成员的行为与其他合成的构造函数、赋值运算符或析构函数类型:
它们对类本身的成员依次进行初始化、赋值或销毁的操作。
无论基类成员是合成的版本还是自定义的版本都没有太大的影响,唯一的要求是相应的成员是可访问并且不是一个被删除的函数。
继承体系中所有类使用合成的析构函数,派生类隐式得使用而基类通过将其析构函数定义成 =default 而显式地使用。合成地析构函数体是空地,其隐式的析构部分负责销毁类的成员。对于派生类的析构函数来说,除了销毁派生类自己的成员外,还负责销毁派生类的直接基类,该直接基类又销毁它的直接基类,以此类推直至继承链的顶端。
基类定义了析构函数而不能拥有合成的移动操作,当移动对象时实际使用的是合成的拷贝操作,基类没有移动操作意味着它的派生类也没有。
派生类中删除的拷贝控制与基类的关系
基类或派生类也能将其合成的默认构造函数或者任何一个拷贝控制成员定义成删除的函数。
某些定义基类的方式也可能导致有的派生类成员成为被删除的函数:
- 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或者不可访问,则派生类中对应的成员将是被删除的,原因是编译器不能使用基类成员来执行派生类对象来执行派生类对象基类部分的构造、赋值或销毁操作。
- 如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分。
- 和过去一样,编译器将不会合成一个删除掉的移动操作。当我们使用 =default 请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的。
移动操作与继承
大多数基类都会定义一个虚析构函数,在默认情况下,基类通常不含有合成的移动操作,而且它的派生类中也没有合成的移动操作。
因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当确实需要执行移动操作时首先在基类中进行定义。
派生类的拷贝控制成员
派生类构造函数在其初始化阶段中不但要初始化派生类自己的成员,还负责初始化派生类对象的基类部分。
派生类的拷贝和移动构造函数在拷贝和移动自由成员的同时,也要拷贝和移动基类部分的成员。
派生类赋值运算符也必须为其基类部分的成员赋值。
析构函数只负责销毁派生类自己分配的资源,对象的成员是被隐式销毁的,派生类对象的基类部分也是自动销毁的。
定义派生类的拷贝或移动构造函数
当为派生类定义拷贝或移动构造函数时,通常使用对应的基类构造函数初始化对象的基类部分。
在默认情况下,基类默认构造函数初始化派生类对象的基类部分。
如果想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类地拷贝(或移动)构造函数。
派生类赋值运算符
与拷贝和移动构造函数一样,派生类的赋值运算符也必须显式地为其基类部分赋值。
派生类析构函数
对象销毁的顺序正好与其创建的顺序相反:
派生类析构函数首先执行,然后是基类的析构函数,以此类推,沿着继承体系的反方向直至最后。
在构造函数和析构函数中调用虚函数
派生类对象的基类部分首先被构建,当执行基类的构造函数时,该对象的派生类部分是未被初始化的状态。销毁派生类对象的次序正好相反,因此当执行基类的析构函数时,派生类部分已经被销毁掉了。当执行上述基类成员的时候,该对象处于未完成的状态。
编译器认为对象的类型在构造或析构的过程中仿佛发生了改变一样。当构建一个对象时,需要把对象的类和构造函数的类看作是同一个。对虚函数的调用绑定正好符合这种把对象的类和构造函数的类看成同一个的要求,对析构函数也是同样的道理。上述的绑定不但对直接调用虚函数有效,对间接调用也是有效的,这里的间接调用时指通过构造函数(或析构函数)调用另一个函数。
为了理解上述行为,考虑当基类构造函数调用虚函数的派生类版本时会发生什么情况:
这个虚函数可能会访问派生类的成员,如果它不需要访问派生类成员的话,则派生类直接使用基类的虚函数版本就可以了。当执行基类构造函数时,它要用到的派生类成员尚未初始化,如果允许这样的访问,程序可能崩溃。
继承得构造函数
派生类能够重用其直接基类定义得构造函数,这些构造函数并非以常规的方式继承而来,为了方便称其为“继承”。
一个类只初始化它的直接基类,出于同样的原因,一个类也只继承其直接的构造函数。
类不能继承默认、拷贝和移动构造。
如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。
派生类继承基类构造函数的方式是提供了一条注明了(直接)基类名的 using 声明语句。
using 声明语句作用于构造函数时,对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数。
编译器生成的构造函数形如:
derived(parms) : base(args) { }
derived 是派生类的名字,base 是基类的名字,parms 是构造函数的形参列表,args 将派生类构造函数的形参传递给基类的构造函数。
如果派生类含有自己的数据成员,则这些成员将被默认初始化。
继承的构造函数的特点
一个构造函数的 using 声明不会改变该构造函数的访问级别:
基类的私有构造函数在派生类中还是一个私有构造函数,受保护的构造函数和公有构造函数也是同样的规则。
一个 using 声明语句不能指定 explicit 或 constepr。
如果基类的构造函数是 explicit 或者 constexpr,则继承的构造函数也拥有相同的属性。
当一个基类构造函数含有默认实参时,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。
如果基类含有多个构造函数,大多数时候派生类会继承所有构造函数,除了两个例外情况:
-
派生类可以继承一部分构造函数,而为其他构造函数定义自己的版本。如果派生类定义的构造函数与基类的构造函数具有相同的参数列表,则该构造函数将不会被继承,定义在派生类中的构造函数将替换继承而来的构造函数。
-
默认、拷贝和移动构造函数不会被继承,这些构造函数按照正常规则被合成。继承的构造函数不会被作为用户定义的构造函数来使用。

浙公网安备 33010602011771号