面向对象程序设计的核心思想是数据抽象、继承和动态绑定。
通过使用数据抽象,可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
继承
通过继承联系在一起的类构成一种层次关系。
通过在层次关系的根部有一个基类,其他类则直接或间接地从基类继承而来,这些继承得到的类称为派生类。
基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数。
派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。
类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有访问说明符。
派生类必须在其内部对所有重新定义的虚函数进行声明。
派生类可以在这样的函数之前加上 virtual 关键字,但是并不是非得这么做。
C++11 新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个 override 关键字。
动态绑定
通过使用动态绑定,我们能用同一段代码分别处理基类和派生类的对象。
动态绑定有时又被称为运行时绑定。
基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
成员函数与继承
派生类需要对这些与类型相关的操作提供自己的新定义以覆盖从基类继承而来的旧定义。
在C++语言中,基类必须将它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数;另一种是基类希望派生类直接继承而不要改变的函数。
对于前者,基类通常将其定义为虚函数。
基类通过在其成员函数的声明语句之前加上关键字 virtual 使得该函数执行动态绑定。
任何构造函数之外的非静态函数都可以是虚函数。
关键字 virtual 只能出现在类内部的声明语句之前而不能用于类外部的函数定义。
成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时。
访问控制与继承
派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。
和其他使用基类的代码一样,派生类能访问公有成员,而不能访问私有成员。
在某些时候基类中还有这样一种成员,基类希望它的派生类有权访问该成员,同时禁止其他用户访问。用受保护访问运算符说明这样的成员。
派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。
类派生列表的形式是:首先是一个冒号,后紧跟以逗号分隔的基类列表,其中每个基类前面可以有以下三种访问说明符中的一个:public、protected 或者 private。
如果一个派生是公有的,则基类的公有成员也是派生类接口的组成部分。此外,我们能将公有派生类型的对象绑定到基类的引用或指针上。
大多数类都只继承自一个类,这种形式的继承被称作为“单继承”。
派生类中的虚函数
派生类经常(但不总是)覆盖它继承的虚函数。
如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。
派生类对象及派生类向基类的类型转换
一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与该派生类继承的基类对应的子对象,如果有多个基类,那么这样的子对象也有多个。
对虚函数的调用可能在运行时才被解析
当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。
被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。
派生类中的虚函数
当在派生类中覆盖了某个虚函数时,可以再一次使用 virtual 关键字指出该函数的性质。然而这么做并非必须,因为一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数。
一个派生类的函数如果副高了某个击沉而来的虚函数,则它的形参类型必须与被它副高的基类函数完全一致。
派生类中的虚函数的返回类型也必须与基类函数匹配。
虚函数与默认实参
虚函数也可以拥有默认实参。如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。
如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。此时,传入派生类函数的将是基类函数定义的默认实参。
含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。
抽象基类负责定义接口,而后续的其他类可以覆盖该接口。
不能(直接)创建一个抽象基类的对象。
学习参考资料:
C++ 中文版 Primer (第5版)