面向对象程序设计基于三个基本概念:数据抽象、继承和动态绑定
。通过数据抽象,可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,从而以统一的方式使用它们的对象。
定义基类和派生类
定义基类
在C++语言中,当我们使用基类的引用或者指针调用一个虚函数时将发生动态绑定,即在运行时选择函数的版本。基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此
。因为在C++中,当派生类对象经由一个基类对象指针被删除,而该基类带有一个非虚析构函数,那么基类成分将被销毁,而派生类对象的析构函数未被执行,从而造成“局部销毁”对象。
是否应该声明virtual析构函数的原则:
如果类带有任何virtual函数,它就应该拥有一个virtual析构函数
如果类设计的目的不是作为基类使用,或者不是为了具备多态性,就不该声明virtual析构函数
基类通过在其成员函数的声明语句之前加上关键字virtual使得该函数执行动态绑定。关键字virtual仅出现在声明处而不能用于类外部的函数定义。在基类中为virtual函数,则该函数在派生类中隐式地也是虚函数。
定义派生类
C++11新标准允许派生类显式地注明它使用某个成员函数覆盖了它继承的虚函数,即在函数声明的最后添加一个关键字override
。
编译器会隐式执行派生类到基类的转换
,因此我么可以将基类的指针或引用绑定到派生类对象上,当我们使用基类的引用或指针时,实际上我们并不清楚该引用或者指针绑定的真实类型,该对象可能是基类的对象,也可能是派生类的对象。
每个类控制它自己的成员初始化过程。在继承类结构中,首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
派生类对象不能直接初始化基类的成员,应该通过调用基类的构造函数来初始化那些从基类中继承而来的成员。
如果基类定义了一个静态成员,则整个继承体系中只存在该成员的唯一定义。
C++11新标准提供一种防止继承发生的方法:即在类名后跟一个关键字final
。
1 | class NoDerived final { }; //NoDerived不能作为基类 |
类型转换与继承
表达式的静态类型在编译时总是已知的,是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。
如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。
如果在基类中有一个或多个虚函数,我们可以使用dynamic_cast
请求一个类型转换,该转换的安全检查将在运行时执行。同样,如果我们已知某个基类向派生类的转换是安全的,则我们可以使用static_cast
来强制覆盖掉编译器的检查工作。
当用派生类对象为一个基类对象初始化或者赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分被切掉(slice down)。
虚函数
动态绑定只有当我们通过指针或者引用调用虚函数时才会发生。当我们通过普通类型(非指针非引用)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。
在C++11新标准中我们可以使用override关键字来说明派生类中的虚函数,这样做的好处是让程序员的意图更加清晰,并且能够让编译器帮助我们发现错误。如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。
当把某个函数指定为final,则之后任何尝试覆盖的操作都将引发错误。
override和final修饰符都位于形参列表(包括任何const或引用修饰符)以及尾置返回类型之后。
虚函数与默认实参
如果虚函数中使用了默认实参,则基类和派生类中定义的默认实参最好一致,因为该实参的值由调用的静态类型决定。在通过基类的指针或引用调用函数时,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。此时,传入派生类函数的将是基类函数定义的默认实参。
如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。
1 | //强行调用基类中定义的函数版本而不管baseP的动态类型 |
抽象基类
纯虚函数清晰告诉用户当前函数是没有意义的,一个纯虚函数无须定义。
含有纯虚函数的类是抽象基类,抽象基类负责定义接口,而后续的其他类可以覆盖该接口。
不能创建抽象基类的对象。
派生类构造函数只初始化它的直接基类。
继承中的类作用域
当存在继承关系时,派生类的作用域嵌套在其基类作用域之内,如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。
派生类的成员将隐藏同名的基类成员,我们可以通过作用域运算符来使用隐藏的成员。除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
构造函数与拷贝控制
如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数的版本。
在C++11新标准中,派生类能够使用using声明直接重用其直接基类定义的构造函数。
1 | class Bulk_quote : public Disc_quote { |
using声明语句将令编译器产生代码,对于基类的每个构造函数,编译器都生成一个与之对应的形参列表完成相同的派生类构造函数。