15.1 OOP:概述
面向对象程序设计(object-oriented programming)的核心思想是数据抽象,继承和动态绑定(个人认为应该是多态,但是书里原话是动态绑定,因此不太确定).
一开始,C++只是C加上一些面向对象特性.C++最初的名称C with Classes 也反映了这个血缘关系 ——《Effective C++(第三版)》
C++中,若基类希望其派生类(子类,子孙类)各自定义适合自身的版本,则基类将函数声明成虚函数.(在函数声明前加上关键字virtual).派生类需在内部对所有重新定义的虚函数进行声明.派生类声明时可以加上也可以不必加上virtual关键字.因为一旦基类标注了某函数为虚函数则此函数后续永远都是虚函数.
派生类需通过类派生列表指明从哪个基类继承而来.
class Son: public Father{ //Son类以public方式继承Father类
/*
* 类实现
*/
}
子类可以同时继承多个父类,但是不推荐,一次最好是只继承一个父类,但是也可以通过父类去继承其他类来实现多继承.
以下例子说明一次继承多个父类的坏处:
可以看出s1调用speak函数报错,因为Student类没有此函数,因此去父类中寻找,而父类中有两个,无法确定调用哪一个,在下面再次调用时加入作用域后可以确认调用哪个父类的speak函数后不报错.
动态绑定在运行时选择函数的版本,所以动态绑定有时被称为运行时绑定.非虚函数的解析过程发生在编译时而非运行时.
当我们使用基类的引用或是指针调用一个虚函数时发生动态绑定.
以上例子可以看出同一个函数,我们可以用两个不同类型的对象来调用,且调用结果因对象的类型不同而不同.
15.2定义基类和派生类
15.2.1定义基类
基类通常都应该定义一个虚析构函数,即使该函数 不执行任何实际操作.这点应该不难理解,因为派生类大概率会需要释放比基类更多的资源.
任何构造函数之外的非静态函数都可以是虚函数,但关键字virtual只能出现在类内部的声明语句之前,而不能用于类外部的函数定义.
若我们希望基类的成员不被外部用户使用,而派生类可以访问,则可以使用protected访问运算符来说明这样的成员.
15.2.2定义派生类
派生类需要将继承来的成员函数中需要覆盖的重新声明.派生类经常但不总是覆盖它继承的虚函数.
C++11新标准可以显示地标注覆盖其继承的虚函数,在函数声明的后面加上关键字override.或override标记了没有覆盖已存在的虚函数的函数,则会发生报错.
//以刚刚的类为例
class person :public people {
public:
virtual void speak() override{ //override在这里
cout << "hello world!" << endl;
}
};
我们可以将基类的指针或是引用绑定到派生类对象中的基类部分(反过来不行),因为派生类有基类的成员,但基类不一定有派生类的成员.
每个类控制自己的成员初始化过程,因此派生类需使用基类的构造函数来初始化它的基类部分.除非我们特别指出,否则派生类中的基类部分会执行默认初始化.初始化顺序是先初始化基类部分,然后按照声明顺序依次初始化派生类的成员.
若基类中定义了一个静态成员,那么整个继承体系中只存在该成员的唯一定义.
在声明中派生类只包含类名不包含派生列表(继承自).
C++11新标准提供,若基类定义在类名后加上关键字final则可以防止继承的发生.final同样可以用于标记成员函数,使得成员函数不可被覆盖.
15.2.3类型转换与继承
不存在从基类向派生类的隐式类型转换,在对象之间不存在类型转换.
存在派生类向基类的类型转换是因为每个派生类对象都包含一个基类部分(反过来不成立).并且派生类想基类的自动类型转换只对指针或引用类型有效.在派生类类型与基类类型之间不存在这样的转换.
当我们使用一个派生类为基类初始化或是赋值时,只有派生类对象中基类部分会被拷贝,移动,赋值,多余部分会被忽略.
15.3虚函数
一个派生类的函数如果覆盖了某个继承来的虚函数,则形参类型必须和覆盖的基类函数一致.并且返回值也需要与基类函数匹配,除非返回值类型是类本身的指针或是引用.
虚函数可以拥有默认实参,派生类的默认实参最好与基类的保持一致,但不是必须.
使用作用域运算符可以使用指定版本的虚函数,可以参考上面的某个代码.
15.4抽象基类
含有纯虚函数的类是抽象基类,抽象类无法被创建对象,只能被继承.
纯虚函数即在虚函数的函数体位置接上=0即可,并且=0只能出现在类内部的虚函数声明语句处.
//例如
virtual void speak() = 0;
15.5访问控制与继承
继承时使用的访问权限控制符不影响派生类的访问权限,派生类的访问权限值取决于基类中定义成员时,成员所在的访问权限.
继承时使用的访问权限控制符影响的是派生类对象以及派生类的派生类.
友元关系不能被继承.每个类负责控制各自成员的访问权限.
若我们需要改变派生类继承的某个名字的访问级别可以使用using声明.派生类只能对自己可以访问的对象使用using声明.
class people {
protected:
int age;
};
class person :private people {
public:
using people::age;
};
15.6继承中的类作用域
派生类的作用域位于基类作用域之外.
派生类能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(派生类)的名字将隐藏定义在外层作用域(基类)的名字.简单来说即派生类的成员将隐藏同名的基类成员.
除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字.
若派生类的成员函数与基类的函数重名,那么将会隐藏基类的函数,即使形参列表不一致.
如果派生类希望基类中的重载函数(这里我们认为重载函数是有多个同名的函数),那么需要覆盖全部版本,或者一个也不覆盖.
也可以使用using声明语句来声明重载的成员,然后再覆盖自己想要重写的某个重载版本函数.这样即可以继承来自基类的多个重载函数,也可以重写某个版本的重载函数并且不必对每个版本都进行覆盖.
15.7构造函数与拷贝控制
在派生类的构造函数中可以直接实现基类的构造函数.
using不改变构造函数的访问级别.
derived(parms) : base(args) { }
derived:派生类名字
parms:构造函数形参列表
base:基类名字
args:传递给基类构造函数的参数
class people {
protected:
int age;
string name;
people(int a,string n):age(a),name(n){}
};
class person :private people {
public:
person(int a,string n):people(a,n){} //注意这行
};
15.8容器与继承
当派生类对象被赋值给基类对象时,其中的派生类部分会被忽略,因此容器和存在继承关系的类型无法兼容.
我们可以看到,在存储类型为基类的容器中,无论装进去的是基类还是派生类,最终都会变成基类.