- 面向对象编程的三个基本概念:数据抽象、继承和动态绑定(多态):
- 基类应该提供一些类型无关的成员函数定义,将与类相关的函数留给不同的派生类定义:,派生类是通过类派生列表(
class derivation list
)对基类进行声明: - 对于那些与类相关的成员函数,我们需要在基类中声明为
virtual
,在派生类用用关键字override
显式强调我们要重新定义的成员函数: - 动态绑定(
dynamic binding
,也叫run-time binding
)可以用一份代码在多个对象上执行: - 对于涉及继承的几个类而言,基类的几个特征是:1. 对于类相关的成员函数定义成(纯)虚函数。2. 对于让派生类访问,但用户无法访问的成员,用
protected
关键字。3. 基类的析构函数必须为虚函数。 - 除了
static member
和constructor
,我们都可以声明为virtual
: - 基类中对成员的访问控制,影响派生类对成员的访问:,简单讲,我们可以将派生类当做基类的特殊用户来看待。目前将,有三个用户:类的实现者、类的用户和派生类。
- 对于派生类而言,需要在(
class derivation list
,类派生列表)指定其基类和打算override来自基类的成员:。对于派生列表中用public
继承的基类,我们可以正常使用动态绑定
,此外可以将基类的接口作为派生类接口的一部分: - 派生类对象由多个部分组成,正因如此,才允许动态绑定。以及
Derived-to-Base
(指针)的转换,注意转换的部分是we can bind a base-class reference or pointer to the base-class part of a derived object.:,即 - 不管是什么类,每个类都负责自己成员的初始化:,关于在继承的情况下,构造函数的执行顺序是从基类到派生类:
- 派生类可以直接访问基类中
protected
和public
限定的成员,但是要尊重类的接口。此外值得注意的是:派生类的作用域被嵌入在基类作用域的里面: - 对于
static
成员,它们不属于对象,所以它们自始至终只有一份代码: - 通过
final
关键字防止该类被继承: - 派生类(指针)到基类(指针)的转换是理解C++中OOP的重中之重:
- 在继承场景下,基类的指针和引用的静态类型和动态类型可能会不同:,对于变量或表达式的静态类型在编译时确定,而动态类型是其在内存中表示的类型,在运行时确定。
- 因为派生类到基类的转换是因为基类的指针/引用可以绑定到派生类的基类部分。但是基类作为一个独立的对象时,它可能不存在派生类的成员,所以不存在基类到派生类的隐式转换:
- 派生类到基类的隐式转换的前提是用基类的指针/引用,派生类与基类的对象之间是无法转换的:,强制将派生类转换成基类时就会发生sliced down:
- 继承层次下的
virtual
function必须要定义,因为其是否使用只能在运行时确定: dynamic binding
happens only when a virtual function is called through a pointer or a reference of base class: ,对于其他对象类型在编译时确定的情况下,调用的成员函数就是确定的:,dynamic binding引出C++的polymorphism:- 如果一个成员函数在base class是virtual,那么它在后续的派生类中也是隐式的virtual,此外派生类override基类的虚函数时,需要保证参数列表一致:
- 在派生类中,覆盖相应的虚函数时,可以使用
override
关键字通知编译器来检查派生类中重定义时是否与基类中的参数列表相同:,也可以声明成员函数为final
来防止该函数被override
。 - 我们也可以使用作用域操作符来规避动态绑定机制:
- 纯虚函数通常是一个通用的概念,负责规定函数签名,实现细节由具体的派生类决定:,如果非要定义纯虚函数,我们只能在类外进行。
- 抽象基类不能定义对象,不过应该可以定义指针/引用:
- 每个类都负责自身成员的初始化,这里要注意在继承框架下,构造函数的调用顺序:
- 每个类也控制它的成员是否允许被“用户(类的使用者/派生类)”访问:
- 从派生类访问基类基础来的成员受两个方面影响:该成员在基类内的访问声明符,在派生类的派生列表中的访问声明符:
- 派生类到基类的隐式转换需要条件:
- 类的三个使用者:类的实现者、类的用户和派生类:
friendship
不可被传递和继承:- 关键字
struct
和class
的默认访问声明符和默认派生访问声明符不同: - 派生类的作用域是嵌入到基类里面:
- 编译时的静态类型确定某个成员函数是否可以被调用,因为命名查询是从静态类型所在的类开始,然后向基类搜索的:
- 如果派生类具有与基类的同名成员名称,因为派生类的作用域是嵌入在基类的作用域的,所以此时派生类的该成员会隐藏基类同名成员:,我们也可以使用作用域操作符来显式地调用基类中隐藏的成员:
- 在继承场景下,函数调用的解析过程,注意在类型检查前做命名查询:
- 在继承的场景下, 为了能够让基类的指针释放动态绑定的派生类对象,通过将基类的析构函数声明为
virtual
:,否则就会发生undefined behavior
: - 如果一个类显式定义了一个析构函数,那么编译器不会默认合成
move operation
: - 派生类的析构函数会自动调用基类的析构:
- 在继承场景下,析构函数的执行顺序与构造函数相反:,因为在执行基类的构造/析构时,如果是派生类的对象,那么该对象此时就是不完整的。为了安全考虑,在构造/析构执行虚函数时就不谈动态绑定一说:
- 容器存储继承层次中的对象时应该间接进行:,例如使用指向基类的指针等。