1继承概念
面向对象程序设计有4个主要特点:抽象、封装、继承和多态性。说了类和对象,了解了面向对象程序设计的两个重要特征一数据抽象与封装,已经能够设计出基于对象的程序,这是面向对象程序设计的基础。
要较好地进行面向对象程序设计,还必须了解面向对象程序设计另外两个重要特 征——继承性和多态性。本章主要介绍有关继承的知识,多态性将在后续章节中讲解。
继承性是面向对象程序设计最重要的特征,可以说,如果没有掌握继承性,就等于没有掌握类和对象的精华,就是没有掌握面向对象程序设计的真谛。
1.1类之间的关系
has-A,uses-A 和 is-A
has-A 包含关系,用以描述一个类由多个"部件类"构成。实现has-A关系用类成员表示,即一个类中的数据成员是另一种已经定义的类。
uses-A 一个类部分地使用另一个类。通过类之间成员函数的相互联系,定义友员或对象参数传递实现。
is-A 机制称为"继承"。关系具有传递性,不具有对称性。
1.2继承关系举例
万事万物中皆有继承,是重要的现象
两个案例:1)植物继承图;2)程序员继承图
1.3 继承相关概念
1.4 派生类的定义
注意:C++中的继承方式(public、private、protected)会影响子类的对外访问属性。
1.5 继承重要说明
1、子类拥有父类的所有成员变量和成员函数
2、子类可以拥有父类没有的方法和属性
3、子类就是一种特殊的父类
4、子类对象可以当作父类对象使用
2派生类的访问控制
派生类继承了基类的全部成员变量和成员方法(除了构造和析构之外的成员方法),但是这些成员的访问属性,在派生过程中是可以调整的。
2.1单个类的访问控制
1、类成员访问级别(public、private、protected)
2、思考:类成员的访问级别只有public和private是否足够?
2.2不同的继承方式会改变继承成员的访问属性
1)C++中的继承方式会影响子类的对外访问属性
public继承:父类成员在子类中保持原有访问级别
private继承:父类成员在子类中变为private成员
protected继承: 父类中public成员会变成protected
父类中protected成员仍然为protected
父类中private成员仍然为private
2)private成员在子类中依然存在,但是却无法访问到。不论种方式继承基类,派生类都不能直接使用基类的私有成员 。
3)C++中子类对外访问属性表
父类成员访问级别 |
||||
继 承 方 式 |
public |
proteced |
private |
|
public |
public |
proteced |
private |
|
proteced |
proteced |
proteced |
private |
|
private |
private |
private |
Private |
4)继承中的访问控制
这个图说的是:public继承:父类的a可以在main函数中(即类外)访问;b,c不能访问;protected、private继承的方式父类的成员都不能在函数中(类外)访问到。
2.3"三看"原则
C++中的继承方式(public、private、protected)会影响子类的对外访问属性
判断某一句话,能否被访问
1)看调用语句,这句话写在子类的内部、外部
2)看子类如何从父类继承(public、private、protected)
3)看父类中的访问级别(public、private、protected)
2.3派生类类成员访问级别设置的原则
思考:如何恰当的使用public,protected和private为成员声明访问级别?
1、需要被外界访问的成员直接设置为public
2、只能在当前类中访问的成员设置为private
3、只能在当前类和子类中访问的成员设置为protected,protected成员的访问权限介于public和private之间。
2.4综合
练习:
public继承不会改变父类对外访问属性;
private继承会改变父类对外访问属性为private;
protected继承会部分改变父类对外访问属性。
结论:一般情况下class B : public A:一般都是公有继承父类
//类的继承方式对子类对外访问属性影响
#include #include
using
class { private: int a; protected: int b; public: int c;
A() { a = 0; b = 0; c = 0; }
void set(int { this->a = a; this->b = b; this->c = c; } };
class { public: void print() { //cout<<"a = "<<a; //err cout << cout << } };
class { public: void print() { //cout<<"a = "<<a; //err cout << cout << } };
class { public: void print() { //cout<<"a = "<<a; //err cout << cout << } };
int main_01(int { A aa; B bb; C cc; D dd;
aa.c = 100; //ok bb.c = 100; //ok //cc.c = 100; //err 类的外部是什么含义 //dd.c = 100; //err
aa.set(1, 2, 3); bb.set(10, 20, 30); //cc.set(40, 50, 60); //ee //dd.set(70, 80, 90); //ee
bb.print(); cc.print(); dd.print();
system("pause"); return 0; } |
3继承中的构造和析构
3.1类型兼容性原则
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。这样,公有派 生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。类型兼容规则中所指的替代包括以下情况:
子类对象可以当作父类对象使用
子类对象可以直接赋值给父类对象
子类对象可以直接初始化父类对象
父类指针可以直接指向子类对象
父类引用可以直接引用子类对象
在替代之后,派生类对象就可以作为基类的对象使用,但是只能使用从基类继承的成员。
类型兼容规则是多态性的重要基础之一。
总结:子类就是特殊的父类 (base *p = &child;)
#include #include
using
/* 子类对象可以当作父类对象使用 子类对象可以直接赋值给父类对象 子类对象可以直接初始化父类对象 父类指针可以直接指向子类对象 父类引用可以直接引用子类对象 */ //子类就是特殊的父类 class { protected: const public: Parent03() { name = "Parent03"; }
void print() { cout << } };
class { protected: int i; public: Child03(int { this->name = "Child2"; this->i = i; } };
int main() { Child03 child03(1000); //分别定义父类对象 父类指针 父类引用 child Parent03 parent = child03; Parent03* pp = &child03; Parent03& rp = child03;
parent.print(); pp->print(); rp.print(); system("pause"); return 0; } |
3.2继承中的对象模型
类在C++编译器的内部可以理解为结构体
子类是由父类成员叠加子类新成员得到的
继承中构造和析构
问题:如何初始化父类成员?父类与子类的构造函数有什么关系
在子类对象构造时,需要调用父类构造函数对其继承得来的成员进行初始化
在子类对象析构时,需要调用父类析构函数对其继承得来的成员进行清理
#include #include using
class { public: Parent04(const { cout << }
~Parent04() { cout << } };
class { public: Child04() : Parent04("Parameter from Child!") { cout << }
~Child04() { cout << } };
void run04() { Child04 child; }
int main_04(int { run04();
system("pause"); return 0; } |
3.3继承中的构造析构调用原则
1、子类对象在创建时会首先调用父类的构造函数
2、父类构造函数执行结束后,执行子类的构造函数
3、当父类的构造函数有参数时,需要在子类的初始化列表中显示调用
4、析构函数调用的先后顺序与构造函数相反
3.4继承与组合混搭情况下,构造和析构调用原则
原则: 先构造父类,再构造成员变量、最后构造自己
先析构自己,在析构成员变量、最后析构父类
//先构造的对象,后释放
//子类对象如何初始化父类成员 //继承中的构造和析构 //继承和组合混搭情况下,构造函数、析构函数调用顺序研究
#include
using
class { public: Object(const { cout << } ~Object() { cout << } };
class { public: Parent(const { cout << } ~Parent() { cout << } };
class { protected: Object o1; Object o2; public: Child() : o2("o2"), o1("o1"), Parent("Parameter from Child!") { cout << } ~Child() { cout << } };
void run05() { Child child; }
int main05(int { cout << run05();
system("pause"); return 0; } |
3.5继承中的同名成员变量处理方法
当子类成员变量与父类成员变量同名时,子类依然从父类继承同名成员,在子类中通过作用域分辨符::进行同名成员区分(在派生类中使用基类的同名成员,显式地使用类名限定符)。同名成员存储在内存中的不同位置
总结:同名成员变量和成员函数通过作用域分辨符进行区分
3.6派生类中的static关键字
继承和static关键字在一起会产生什么现象哪?
理论知识
- 基类定义的静态成员,将被所有派生类共享
- 根据静态成员自身的访问特性和派生类的继承方式,在类层次体系中具有不同的访问性质 (遵守派生类的访问控制)
- 派生类中访问静态成员,用以下形式显式说明:
类名 :: 成员
或通过对象访问 对象名 . 成员
总结:
1> static函数也遵守3个访问原则
2> static易犯错误(不但要初始化,更重要的显示的告诉编译器分配内存)
3> 构造函数默认为private
4多继承
4.1多继承的应用
多继承概念
- 一个类有多个直接基类的继承关系称为多继承
- 多继承声明语法
class 派生类名 : 访问控制 基类名1 , 访问控制 基类名2 , … , 访问控制 基类名n
{
数据成员和成员函数声明
};
- 类 C 可以根据访问控制同时继承类 A 和类 B 的成员,并添加
自己的成员
多继承的派生类构造和访问
- 多个基类的派生类构造函数可以用初始式调用基类构造函数初始化数据成员
- 执行顺序与单继承构造函数情况类似。多个直接基类构造函数执行顺序取决于定义派生类时指定的各个继承基类的顺序。
- 一个派生类对象拥有多个直接或间接基类的成员。不同名成员访问不会出现二义性。如果不同的基类有同名成员,派生类对象访问时应该加以识别。
多继承简单应用
4.2虚继承
如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性
分析:
总结:
- 如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性
- 如果在多条继承路径上有一个公共的基类,那么在继承路径的某处汇合点,这个公共基类就会在派生类的对象中产生多个基类子对象,要使这个公共基类在派生类中只产生一个子对象,必须对这个基类声明为虚继承,使这个基类成为虚基类。
- 虚继承声明使用关键字 virtual
实验:注意增加virtual关键字后,构造函数调用的次数。
5继承总结
- 继承是面向对象程序设计实现软件重用的重要方法。程序员可以在已有基类的基础上定义新的派生类。
- 单继承的派生类只有一个基类。多继承的派生类有多个基类。
- 派生类对基类成员的访问由继承方式和成员性质决定。
- 创建派生类对象时,先调用基类构造函数初始化派生类中的基类成员。调用析构函数的次序和调用构造函数的次序相反。
- C++提供虚继承机制,防止类继承关系中成员访问的二义性。
- 多继承提供了软件重用的强大功能,也增加了程序的复杂性。