定义基类和派生类
基类通常都应该定义一个虚析构函数,即使该类不执行实际操作。
成员函数与继承
派生类可以继承其基类成员,但派生类需要对虚函数提供新的定义来覆盖override继承而来的旧定义。
基类应区分其两种成员函数:
- 基类希望派生类覆盖的函数:通常定义为虚函数
- 基类希望派生类继承而不改变的函数
使用指针或引用调用虚函数时,该调用被动态绑定。根据引用或指针所绑定的对象类型不同,可能调用基类或某个派生类的版本。
任何构造函数之外的非静态函数都可以是虚函数。virtual只能出现在类内部的声明语句之前而不能用于外部定义。基类的虚函数在派生类也是隐式的虚函数。没有被声明为虚函数的成员函数,解析过程发生在编译时而非运行时。
访问控制与继承
派生类可以继承定义在基类中的成员,但是派生类的成员函数未必有权限访问继承来的成员:只能访问公有成员以及受保护的protecetd成员(使用protected访问运算符)
定义派生类
派生类必须通过类派生列表class derivation list)明确指出是从哪个(哪些)基类继承而来。
类派生列表的形式是:一个冒号,后面紧跟逗号分割的基类列表。其中每个基类前面可以有访问说明符之一:protected,public,private,作用是控制派生类从基类继承而来的成员是否对派生类用户可见。
派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明。
如果一个派生是共有的,则基类的公有成员也是派生类接口的组成部分。此外,能将共有派生类型的对象绑定到基类的引用或指针上。
派生类中的虚函数
若派生类未覆盖其基类的虚函数,则该函数行为类似普通成员,直接继承自基类的版本。
派生类可以在覆盖的函数前使用virtual关键字,但不是必须。
显式注明派生类中使用的成员函数覆盖了继承的虚函数,可以在const成员函数的const关键字后、或者引用成员函数的引用限定符后添加override。
派生类对象及派生类向基类的类型转换
派生类对象包含多个部分:含有派生类自己定义的成员的子对象,以及与该派生类继承的基类对应的子对象。如果有多个基类,则这种子对象也有多个。
C++并未明确规定派生类对象在内存中的分布,因此继承的部分和派生类自定义的部分不一定是连续存储的。
派生类到基类的(derived-to-base)类型转换:可以将派生类对象当作基类对象使用,也可以将基类的引用或指针绑定到派生类对象的基类部分上。编译器会隐式执行该类型的转换。因此可以把派生类对象或派生类对象的引用用在需要基类引用的地方;也可以把派生类对象的指针用在需要基类指针的地方。
继承的关键:派生类对象中含有与其基类对应的组成部分。
派生类构造函数
派生类不能直接初始化继承自基类的成员:每个类控制其自己的成员初始化过程。派生类也必须使用基类的构造函数初始化其基类部分。
派生类对象的基类部分和派生类自身的成员都是在构造函数初始化阶段进行初始化的。
派生类构造函数也是通过构造函数初始化列表将实参传递给基类的构造函数。
除非特别指出,派生类对象的基类部分会像数据成员一样执行默认初始化。如果想用其他类型的基类构造函数,需要以类名加圆括号内实参列表的形式为构造函数提供初始值。这些实参将帮助编译器决定用哪个构造函数初始化派生类对象的基类部分。
首先初始化基类的部分,然后按声明顺序依次初始化派生类成员。
派生类使用基类的成员
每个类负责定义各自的接口。若要与类的对象交互,必须使用该类的接口,即使该对象是派生类的基类部分。
因此,派生类对象不直接初始化基类的成员。尽管可以在派生类构造函数体内给public或protected的基类成员赋值,但不建议。
继承与静态成员
若基类定义了一个静态成员,则整个继承体系中只存在该成员的唯一定义,不论从基类中派生多少个派生类。
对于每个静态成员,都只存在唯一的实例。
静态成员也遵循访问控制规则。
派生类的声明
派生类声明中包含类名但不包含其派生列表。
被用作基类的类
若要使用某个类作为基类,则该类必须已经定义,而不是只进行了声明。
直接基类direct base:派生类直接继承的类
间接基类indirect base:派生类的基类继承的类
每个类都会继承直接基类的所有成员,对于最终的派生类,会继承其直接基类的成员;该直接基类又含有其基类的成员;以此类推到继承类的顶端。因此,最终的派生类会包含其直接基类的子对象以及每个间接基类的子对象。
防止继承的发生
若不希望某个类被继承或不想考虑是否适合作为基类,可以使用final关键字实现,即在类名后大括号前添加关键字final
类型转换与继承
通常要把引用或指针绑定到一个对象需要类型一致或具有const类型转换规则。
继承关系中则有特例:可以将基类的指针或引用绑定到派生类对象上:当使用基类的引用或指针时,实际上并不清楚该引用或指针绑定对象的真实类型。该对象可能是基类也可能是派生类的对象。
智能指针类也支持派生类向基类的类型转换,因此可以将一个派生类对象的指针存储在基类的智能指针内。
静态类型与动态类型
使用存在继承关系的类时,需要将静态类型static type和动态类型dynamic type区分开来。
- 静态类型在编译时已知,是变量声明时的类型或表达式生成的类型;
- 动态类型则是变量或表达式表示的内存中对象类型。动态类型直到运行时才可知。
如果表达式既不是引用也不是指针,则其静态类型永远与动态类型一致。但是基类的指针或引用的静态类型可能与动态类型不一致。
不存在从基类向派生类的隐式类型转换
存在派生类向基类的类型转换:每个派生类都一定有基类部分,基类指针或引用也可以绑定到这些部分上。
不存在从基类到派生类的类型转换:基类对象可能是派生类对象的一部分,也可能不是。
基类的对象可以独立存在,也可以作为派生类对象的一部分存在。
若基类对象不是派生类对象的一部分,则它只含基类定义的成员,而不含派生类定义的成员。
即使一个基类指针或引用绑定在一个派生类对象上,也不能执行从基类到派生类的转换。
编译时无法确认某个特定转换在运行时是否安全,因为编译器只能检查指针或引用的静态类型。
- 如果基类中含有一个或多个虚函数,则可以使用dynamic_cast请求一个类型转换,该转换在运行时执行。
- 如果已知某个基类向派生类的类型转换安全,则可以使用static_cast强制覆盖编译器的检查工作。
对象之间不存在类型转换
派生类向基类的自动类型转换只对指针和引用有效。在派生类类型和基类类型间不存在这样的转换。确实希望实现这种转换时,可能会有意料之外的结果。
初始化或者赋值一个类类型的对象,实际上是调用了某个函数。执行初始化时,调用构造函数;执行赋值操作时,调用赋值运算符;这些成员通常包含一个参数,该参数的类型是类类型的const版本的引用。
这些成员接受引用作为参数,所以派生类向基类的转换允许给基类的拷贝、移动操作传递一个派生类的对象。这些操作不是虚函数。
当给基类的构造函数传递一个派生类对象时,实际运行的构造函数是基类中定义的那个。显然该构造函数只能处理基类自己的成员,
类似地,如果将一个派生类对象赋给一个基类对象,则实际运行的赋值运算符也是基类中定义的那个,该运算符只能处理基类自己的成员。
也就是说,派生类中基类的成员才能给基类对象初始化或赋值,或者只有派生类中的基类成员才会被传递给基类的构造函数,其余部分可以被认为被切掉(sliced down)了。
存在继承关系的类型之间的转换规则
- 从派生类向基类的类型转换只对指针或引用类型有效
- 基类向派生类的类型转换只对指针或引用类型有效
- 和任何其他成员一样,派生类向基类的类型转换也可能由于访问受限而不可行。
尽管自动类型转换只对指针和引用有效,但是继承体系中大多数类仍然(显式或隐式)定义了拷贝控制成员。因此可以将派生类对象靠欸、赋值给一个基类对象,但是只会处理其中的基类部分。