面向对象特点:抽象 封装 继承 多态
可重用型 software reusability
继承 inheritance
作用:在一个已存在的类的基础上建立一个新的类
已存在的类:父类 father class 基类 base class
新类:子类 son class 派生类 derived class
单继承 single inheritance:一个派生类只从一个基类派生 树结构
多重继承 multiple inheritance:一个派生类有两个或以上基类 图结构
派生类是基类的具体化
基类是派生类的抽象
派生类的声明方式:
class 派生类名:[继承方式] 基类名
{派生类新增加的成员};
继承方式:
|--public 公用的
|--private 私有的(默认)
|--protected 受保护的
派生类的构成:
|--从基类继承过来的成员 共性
|--自己增加的成员 个性
构造派生类的3个步骤:
1、从基类接受成员(不包括构造函数和析构函数) 谨慎定义基类,因为会造成冗余
2、调整从基类接受的成员(公用变私有,重载)
3、增加派生类的个性成员,定义构造函数和析构函数
——————————————————————————————————
基类中的成员 公用派生类 私有派生类 保护派生类
私有成员 不可访问 不可访问 不可访问
公用成员 公用 私有 保护
保护成员 保护 私有 保护
派生类中的成员有4种访问属性
派生类的成员 派生类中 派生类外部 下层派生类
公用成员 1 1 1
受保护成员 1 0 1
私有成员 1 0 0
不可访问成员 0 0 0
私有继承和保护继承不常用
实际中常用的是公用继承
———————————————————————————————————
派生类的构造函数
派生类不能继承基类的构造函数和析构函数
在执行派生类的构造函数时,调用基类的构造函数
简单派生类的构造函数
派生类构造函数名(总参数列表):基类构造函数名(参数列表) //类似于函数初始化列表
{ 派生类中新增数据成员初始化语句 }
先调用基类构造函数,再执行派生类构造函数
先执行派生类析构函数,再执行基类析构函数
有子对象 subobject 的派生类的构造函数
派生类构造函数名(总参数列表):基类构造函数名(参数列表),子对象名(参数列表)
{ 派生类中新增数据成员初始化语句 }
顺序:
调用基类构造函数
调用子对象构造函数
执行派生类本身构造函数
对于多层派生的构造函数,只需写出其上一层派生类/直接基类的构造函数即可
析构函数
先执行派生类自己的析构函数
再执行子对象的析构函数
最后执行基类的析构函数
———————————————————————————————————
多重继承 multiple inheritance
允许一个派生类同时继承多个基类
多重继承派生类的构造函数
派生类构造函数名(总参数列表):基类1构造函数(参数列表),基类2构造函数(参数列表),基类3构造函数(参数列表)
{ 派生类中新增数据成员初始化语句 }
调用基类构造函数的顺序与声明顺序相同
多重继承引起的二义性 ambiguous
解决方法 在访问时加 基类名::变量/函数
———————————————————————————————————
虚基类 virtual base class
A派生B C
D多继承B C
虚基类让D只保存一份A的数据
在继承间接共同基类时,只保留一份成员
class A
{};
class B:virtual public A
{};
class C:virtual public A
{};
class D:public B, public A
{};
虚基类不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的
class 派生类名:virtual 继承方式 基类名
为保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承。
D的构造函数:D():A(),B(),C()
在最后的派生类中,负责对虚基类 直接基类的初始化
编译系统只执行最后的派生类对虚基类的构造函数的调用,保证了虚基类的数据成员不会被多次初始化
使用多重继承要十分小心二义性问题。
不提倡在程序中使用多重启程,能用单继承就不用多重继承。
———————————————————————————————————
基类与派生类的转换
只有公用派生类才是基类真正的子类型,它完整地继承了基类的功能。
不同类型数据之间的自动转换和赋值,称为赋值兼容
基类与派生类对象之间有赋值兼容关系
1、派生类对象可以向基类对象赋值;
2、派生类对象可以向基类对象的引用进行赋值或初始化;//此引用对应派生类中基类的部分
3、如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象;
4、指向基类对象的指针可以指向派生类对象。
——————————————————————————————————
组合 composition
在一个类中以另一个类的对象作为数据成员 has a 继承 is a
———————————————————————————————————
继承在软件开发中的重要意义
缩短软件开发过程的关键是 软件重用
典型例子:类库
对类库中类的声明一般放在头文件中
类的实现(函数的定义部分)单独编译,以代码形式存放在系统某一目录下
——————————————————————————————————
多态性与虚函数
多态性 polymorphism
一个事物有多种形态
向不同的对象发送同一个消息,不同的对象在接受时会产生不同的行为
从系统实现角度分类:
|--静态多态性 编译时多态 函数重载
|--动态多态性 运行时多态 虚函数 virtual function
虚函数的作用是允许在派生类中重新定义与基类同名的函数,
并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
通过指向基类的指针,指向基类或不同的派生类对象,可以调用各自的虚函数
virtual关键字
1、在基类中用virtual声明成员函数为虚函数,在类外定义虚函数时,不必再加virtual;
2、当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动称为虚函数
建议在派生类的虚函数前加virtual
3、虚函数与指向基类对象的指针变量配合使用
函数重载:横向 首部不同(参数不同)
虚函数:纵向 首部相同
编译系统要根据已有的信息,对同名函数的调用做出判断。
确定调用的具体对象的过程称为关联(binding)
把一个函数名与一个类对象捆绑在一起,建立关联
把一个标识符和一个存储地址联系起来
静态关联 static binding
在编译时即可确定其调用的虚函数属于哪一类
函数重载属于静态关联 studl.diaplay() gradl.display()均属于静态关联
运行前关联 早期关联 early binding
动态关联 dynamic binding
在运行阶段把虚函数和类对象binding在一起
这种多态性是动态的多态性,运行阶段的多态性
运行阶段关联 滞后关联 late binding
说明:
使用虚函数,系统要有一定的空间开销
当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function table)
它是一个指针数组,存放每个虚函数的入口地址
系统在进行动态关联时的时间开销是很少的,因此多态是高效的。
———————————————————————————————————
如果析构函数未声明为虚函数
在撤销动态存储空间时,仅能调用基类的析构函数。
建议将基类的析构函数声明为虚函数
保证在撤销动态存储空间时能得到正确处理。
———————————————————————————————————
纯虚函数与抽象类
纯虚函数 pure virtual function
在声明虚函数时被“初始化”为0的函数
一般形式:
virtual 函数类型 函数名(参数列表)=0;
注意:
1、纯虚函数没有函数体
2、最后面的“=0”仅起形式上的作用,告诉编译系统“这是纯虚函数”
3、声明语句加分号
抽象类 abstract class
不用来定义对象而只作为一种基本类型用作继承的类
由于它用作基类,通常称为抽象基类 abstract base class
凡是包含纯虚函数的类都是抽象类
因为虚函数不能被调用,所以无法建立对象
抽象类的作用是作为一个类族的共同基类,为一个类族提供一个公共接口。
可以建立对象的类称为具体类 concrete class
———————————————————————————————————
异常处理
程序中两大类错误:
|--编译错误/语法错误
|--运行错误
|--除0
|--内存不足
|--无法打开文件
|--数据类型出错
在设计程序时,应当事先分析程序运行时可能出现的各种意外情况,并分别制定相应的处理方法,这就是程序的异常处理的任务
如果在执行一个函数过程中出现异常,可以不在本函数中立即处理,而是发出一个信息传给它的上一级(调用它的函数)
它的上级捕捉到这个信息后进行处理,如果处理不了,继续向上级抛,、
如果最高一级也无法处理,终止程序。
异常处理机制的3个部分:
try 检查
catch 捕捉
throw 抛出
throw抛出什么样的数据由程序设计者自定,可以是任何类型的数据(包括自定义类型的数据,如类对象,结构体)
说明:
1、被检测的语句必须放在try块中,否则不起作用
2、try块和catch块作为一个整体出现,可以有try无catch
3、try catch 后必须跟{}
4、只能有一个try,可以有多个catch
5、catch后面的圆括号中,一般只写异常信息的类型名
catch(double)
catch(double d) //可以利用throw传递过来的具体值
6、catch(...) 捕捉任何类型的异常信息
7、throw;此语句代表:我不处理这个异常,请上级处理
9、如果throw抛出的异常信息找不到与之匹配的catch块,那么系统就会调用一个系统函数terminate,使程序终止运行
在函数声明中进行异常情况的指定
double triangle(double, double, doubl); //可抛任何类型的异常
double triangle(double, double, double) throw(double); //可抛double型异常
double triangle(double, double, double) throw(double, int, char, float); //可抛double int char float类型的异常
double triangle(double, double, double) throw(); //不能抛出异常
在异常处理中处理析构函数
C++的异常处理机制会在throw抛出异常信息被catch捕获时,对有关的局部对象进行析构
析构对象的顺序与构造的顺序相反
然后执行与异常信息匹配的catch块中的语句
——————————————————————————————————
命名空间
命名空间是可以由用户命名的作用域,用来处理程序中常见的同名冲突
所谓命名空间,实际上就是一个由程序设计者命名的内存区域。
程序设计者可以根据需要指定一些有名字的空间域,把一些全局实体分别放在各个命名空间中,从而与其他全局实体分隔开来。
namespace ns //namespace 关键字
{
int a;
double b; //a b 称为命名空间成员 namespace member
}
ns::a
ns::b //命名空间限定 qualified 被限定名 qualified name
命名空间的作用类似于目录与文件的关系
命名空间的作用是建立一些相互分隔的作用域,把一些全局实体分割开来,以免产生名字冲突。
全局变量在全局命名空间,独立于所有有名的命名空间之外,它不需要用namespace声明,由系统隐式声明,存在于每个程序之中。
命名空间可以包括:
变量(可以带有初始化)
常量
函数(定义或者声明)
结构体
类
模板
命名空间(嵌套命名空间)
使用命名空间成员的方法
1、命名空间名::命名空间成员名
2、使用命名空间别名
namespace 别名 = 命名空间名;
3、using 命名空间成员名
using std::cout;
4、using namespace 命名空间名
using namespace std;
无名的命名空间
file
namespace
{
void fun()
{...}
}
fun();
在无名的命名空间中的东西只在本文件的作用域中有效。C++
对全局变量用static声明使其只在本文件有效。C
标准命名空间 std
存放标准库的有关内容的命名空间
C++库的所有标识符都是在一个名为std的命名空间中定义的
或者说
标准头文件中函数、类、对象、类模板是在命名空间std中定义的
C传统方法 C++新方法
#include<stdio.h> #include<cstdio>
#include<math.h> #include<cmath>
#include<string.h> #include<cstring>
using namespace std;
无需命名空间 需要命名空间