其它小问题
最后,继承中有一些小的语法限制和规则。我简单过一下:
不会被继承的成员函数
基类的构造函数不会被继承。如果有可访问的(公开的、非 =delete
的)基类的复制赋值运算符重载,那么它也不会被继承。基类的析构函数不会被继承。
不继承构造函数的原因是:如果允许,那么派生类部分成员则是潜在的未初始化状态。这被认为是危险的,所以要求代码编写者需要手动指明派生类的构造函数,并且在初始化列表中将派生成员和基类的初始化方法指明。
不继承后两者的原因是:如果基类有可访问的对应函数,那么编译器总是会尝试预置派生类的对应函数,而无需从基类继承它。
using
声明继承构造函数(选读)
通过 尽管如此,在一些场合为了减少不必要的代码,C++ 也允许继承构造函数。它的思路则是通过 using
声明,将基类的构造函数导出到派生类。其写法是:
struct Base {
Base(int) { }
Base(const char *) { }
};
struct Derived : Base {
using Base::Base; // 将基类中的构造函数 Base 全数导入进来
};
int main() {
Derived d1(42), d2("hi"); // 使用继承的构造函数
}
当使用继承的构造函数时,派生类部分成员被默认初始化。如果派生类部分成员无法默认初始化,则编译错误。继承基类构造函数无关乎 using
声明的可访问性。
再谈预置函数
因为派生类不继承构造函数、复制赋值和析构函数,所以编译器生成预置默认构造和预置复制构造的条件与一般类无异。如果我们将基类考虑为一部分派生类成员构成的集合(或者称基类是一个“成员组”),那么不难推出在继承与以下编译器生成预置函数的规则:
当:
- 派生类未声明任何构造函数,且
- 基类和派生类各成员可以默认初始化
则派生类生成预置默认构造函数,其效果是:先默认初始化基类,随后默认初始化派生类各成员。
当:
- 派生类未声明复制构造函数,且
- 基类和派生类各成员可以复制初始化
则派生类生成预置复制构造函数,其效果是先复制初始化基类,随后复制初始化派生类各成员。
当:
- 派生类未声明复制赋值重载,且
- 基类和派生类均有可访问的复制赋值重载
则派生类生成预置复制赋值运算符重载,其效果是先调用基类的复制赋值重载,随后调用派生类各成员的复制赋值重载。
当:
- 派生类未声明析构函数,且
- 基类和派生类均有可访问的析构函数
则派生类生成预置析构函数,其效果是什么都不做。当派生类对象被析构时,会先调用自身的析构函数(若是预置析构函数则什么都不做),随后按成员列表的逆序调用派生类各成员的析构函数,最后调用基类的析构函数。
如果不满足生成预置函数的条件,则编译器不会生成预置构造函数。下面的例子中,基类不可默认初始化,那么派生类就无法生成预置默认构造:
class Base {
public:
Base() = delete;
Base(const Base&) = delete;
};
// 由于基类缺失默认构造和复制构造,所以 Derived 不会生成预置构造函数
class Derived : public Base { };
int main() {
Derived d; // 错误
}
上面的代码会出现编译错误。
三之原则与零之原则
在本部分的结尾,我简单介绍一下所谓的“三之原则”和“零之原则”。这两条原则并不是语法规则,而是教你养成一些良好的编码习惯。
- 三之原则(The rule of 3)是这样说的:如果需要自己定义复制构造函数、复制赋值重载或析构函数的其中一个,则这三个尽可能都要被手动定义。也就是说,这三者要么都不用(采用预置的),要么都得写。这条原则保证了所有内存的管理都是妥当的,一定程度上避免了内存泄漏的可能。
- 零之原则(The rule of 0)和三之原则相反,它是说如果你不需要手动管理内存(比如在构造函数里执行
new
),那么就尽可能地使用编译器生成的预置复制构造、预置复制赋值和预置析构。这保证了代码的简洁性和可复用性。