第3章 继承与多态
面向对象的三大特征:封装,继承,多态。
3.1 继承
3.1.1 继承的概念
注意:
1. 基类的构造函数、拷贝构造函数和析构函数不可以被继承。
2. 派生类继承了基类的全部数据成员和除了构造、析构函数之外的所有成员函数。派生类对于基类成员的继承是没有选择的,不能选择接收或舍弃基类中的某些成员。
3. 派生类中除了与基类同名的成员外还可以添加新成员,用于实现新功能,保证派生类的功能在基类的基础上的发展。
4. 多个派生类可以继承自一个基类。
5. C++中可以通过派生形成类的层次结构,即一个基类可以是另一个更高层次类的派生类,而另一个派生类也可以继续产生派生类。
3.1.2 继承权限
总结出不同情况下的访问权限:
注意虽然继承的方法分为公有继承、私有继承、保护继承,默认为private(这一点也是class和struct的区别,struct默认是public继承),但不管哪种继承,派生类仅可以访问基类中所有的非私有成员,但我们可以通过基类的公有和保护成员访问它的私有成员,这是封装的重要概念,同时达到了数据抽象的效果。
(1)公有继承:基类的公有也是派生类的公有,基类的保护也是派生类的保护,基类的私有不能继承;
(2)私有继承:基类的公有和保护成为派生类的私有成员;
(3)保护继承:基类的公有和保护成为派生类的保护成员;
3.1.3 派生类操作基类对象的四种方法
1.派生类对象可以向基类对象赋值。(单项的,不可逆)
2.派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化。
ClassA obj_a; ClassB obj_b; ClassA &refa = obj_a;//可以将这一行改为 ClassA &refa = obj_b;
3.如果函数的参数是基类对象或基类对象的引用,函数调用时的实参可以是派生类对象。
void func(ClassA &ref) { cout<<ref.num<<endl; }
在调用func()函数时可以用派生类ClassB的对象obj_b作实参:
func(obj_b);
4.派生类对象的地址可以赋值给基类指针变量。
3.2 派生类
3.2.1 构造函数
派生类中定义带参数构造函数的形式如下所示:
派生类名::派生类构造函数名(参数列表):基类构造函数名(基类构造函数参数表)
{ 派生类新增成员的初始化语句 }
3.2.2 析构函数
若派生类中没有显式的定义析构函数,编译系统会提供一个默认的析构函数。显式定义中,对象生命周期结束时会先调用派生类的析构函数,再调用基类的析构函数。
3.2.3 隐藏基类函数
派生类中重新定义基类同名函数的方法,称为对基类函数的覆盖或改写,覆盖后基类同名函数在派生类中被隐藏。定义派生类对象调用该函数时,调用的是自身的函数,基类同名函数不被调用。
3.3 多重继承
3.3.1 声明多重继承的方式
class 派生类名:继承方式 基类1名称,继承方式 基类2名称,... { public: 新增加的公有成员 protected: 新增加的保护成员 private: 新增加的私有成员 }
3.3.2 多重继承派生类的构造函数
定义:
派生类名::派生类构造函数名(参数总表):基类1构造函数名(参数表1),基类2构造函数名(参数表2),... { 派生类构造函数体 }
假如派生类是多重继承,并且新增数据成员有一个或多个对象成员,那么派生类需要初始化的数据有三部分:继承的数据成员、新增类的数据成员、新增普通成员。
这种复杂派生类定义:
派生类名::派生类构造函数名(参数总表):基类1构造函数名(参数表1),基类2构造函数名(参数表2),...子对象名1(参数表n),子对象名2(参数表n+1) { 派生类构造函数体 }
派生类构造函数调用顺序:
首先,调用基类构造函数。
其次,调用对象成员的构造函数。
最后,调用派生类的构造函数。
3.3.3 多重继承引起的二义性
- 调用不同基类中的同名成员时产生的二义性
消除方法:
(1)使用作用域限定符
(2)派生类中定义与基类同名函数,将基类函数隐藏 - 派生类中访问公有成员时产生二义性
3.3.4 虚基类
在多重继承中,若一个类声明为虚基类,则能保证一个派生类间接地多次继承该类时,派生类中只继承该基类的一份成员,避免了派生类中访问公共基类公有属性多份拷贝的二义性,其效果等同于将基类函数隐藏的方法(上文3.3.3和3.2.3)。
定义:
class 派生类名:virtual 继承方式 基类名 { 派生类成员 };
3.4 多态
3.4.1 多态性概念
3.4.2 虚函数
虚函数是运行时多态,若某个基类函数声明为虚函数,则其公有派生类将定义与其基类虚函数原型相同的函数,这时,当使用基类指针或基类引用操作派生类对象时,系统会自动用派生类中的同名函数代替基类虚函数。
声明形式:
class 类名 { virtual 函数返回值类型 函数名(参数表) }
- 一般虚函数成员
带有虚函数时,C++编译器的操作步骤:
(1)为各个类建立虚函数列表,若无虚函数则不操作。
(2)暂不连接虚函数,只是将各个虚函数地址放入虚函数表。
(3)连接各静态函数。 - 虚析构函数
3.4.3 纯虚函数
在定义一个表示抽象概念的类时,有时无法或者不需要给出某些成员函数的具体实现,函数的实现在派生类中完成,基类中这样的函数声明为纯虚函数。
与虚函数相比,纯虚函数没有函数体,其作用是在基类中为派生类保留一个接口,方便派生类根据需要对它实现,实现多态。
声明形式:
virtual 函数返回值类型 函数名(参数表)= 0 ;
3.5 抽象类与内部类
3.5.1 抽象类
如果一个类中至少包含一个纯虚函数,则该类成为抽象类,因此抽象类是基于纯虚函数的。
定义形式:
class 类名 { public: virtual 函数返回值类型 函数名(参数表)= 0; 其他函数声明 }
3.5.2 内部类
class 外部类名 { 外部类成员; 访问限定符: class 内部类名 { 内部类成员 }; };