文章目录
- C++多态篇
- 1.什么是多态
- 1.多态分类-静态多态和动态多态(早绑定和晚绑定)
- 3.普通虚函数-实现动态多态(晚绑定)
- 4.虚析构函数-解决动态多态中存在的问题
- 5.虚函数的实现原理-多态的实现原理
- 6.函数的覆盖与隐藏
- 7.虚析构函数的实现原理
- 8.虚函数小结
- 9.纯虚函数
- 10.抽象类
- 11.接口类
- 12.RTTI-运行时内存识别(Run-Time Type Identification)
- 13.异常处理
C++多态篇
本章主要的内容:
-
普通虚函数、虚析构函数。
-
纯虚函数(抽象类、接口类)
-
RTTI
-
异常处理
-
隐藏VS覆盖,之间的关系
-
早绑定、晚绑定。
-
虚函数表。
1.什么是多态
-
多态具体到语法中是指,使用父类指针指向子类对象,并可以通过该指针调用子类的函数(方法)。
-
产生多态的基础是继承关系,没有继承就没有多态。
-
多态的语法核心是virtual关键字,必须使用virtual才能使多个类间建立多态关系。(virtual是可以继承的,父类的函数(方法)写了virtual,子类同名的函数(方法)继承virtual;但是,还是推荐自己手动写上,以起到提示的作用)
-
封装、继承、多态是面向对象的三大特性。
1.多态分类-静态多态和动态多态(早绑定和晚绑定)
分为静态多态和动态多态。
1.**静态多态:**也称“早绑定”。
如下所示:
如下的调用方式(函数的重载),程序在编译阶段就知道rect.calcArea()
到底要调用哪个函数了,很早的就将函数编译进去了,所以又叫早绑定。
class Rect{
public:
int calcArea(int width);
int calcArea(int width, int height);
}
int main(){
Rect rect;
rect.calcArea(10);
rect.calcArea(10,20);
//...
}
2.**动态多态:**也叫“晚绑定”。
对不同的对象,下达相同的指令,导致对象做着不同的操作。
例如:shape父类有Circle和Rect两个子类,利用父类的指针调用Circle和Rect中计算面积的同名函数,那么同名的函数,却能做出不同的动作,这就是动态多态。
动态多态的前提:必须以封装和继承为基础。
3.普通虚函数-实现动态多态(晚绑定)
在父类指针中调用子类的成员函数:
Shape
为Circle
和Rect
父类,3个类都有calcArea()
,现在想要分别调用Circle
和Rect
中的calcArea()
。
int main(){
Shape *shape1 = new Circle(4.0);
Shape *shape2 = new Rect(3.0,5.0);
shape1->calcArea();
shape2->calcArea();
//...
}
/*输出:
Shape->calcArea()
Shape->calcArea()
*/
当运行程序时,就会发现实际上调用的都是Shape
中的calcArea()
,但是实际上我们想要调用的是Circle
和Rect
中的calcArea()
,如果要解决当前的问题,就需要使用动态多态。
实现动态多态:
我们需要使用virtual
关键字,使其成为虚函数,步骤如下:
1.在父类声明中,对需要实现动态多态的函数添加virtual
:
class Shape{
public:
virtual double calcArea(){ // 添加virtual
cout << "calcArea" << endl;
return 0;
}
}
2.在子类声明中,对同名函数中添加virtual
:
系统会自动加,但是手动加上能起到提示的作用,还是建议手动添加。
class Circle : public Shape{
public:
Circle(double r);
virtual double calcArea(); // 添加virtual
private:
double m_dR;
}
3.然后就可以使用本章最开始的代码,对子类中的函数进行调用了。
int main(){
Shape *shape1 = new Circle(4.0);
Shape *shape2 = new Rect(3.0,5.0);
shape1->calcArea();
shape2->calcArea();
//...
}
/*输出:
Circle->calcArea()
Rect->calcArea()
*/
4.虚析构函数-解决动态多态中存在的问题
首先说一下动态多态的问题:内存泄露问题。用上一节的代码来讲解。
导致内存泄露的原因是:
-
动态多态的调用如下:
父类的指针去指向子类对象,
Shape *shape1 = new Circle(4.0);
;并通过父类指针去操作子类中的虚函数,shape1->calcArea();
。 -
使用
delete
手动释放内存时,析构函数的调用:当使用
delete
去销毁对象时,如果delete
后面跟着的是父类的指针,那么将只调用父类的析构函数。如果delete
后面跟着的是子类的指针,那么将调用子类以及父类的析构函数。
在此表现为,子类的析构函数没有被调用,只调用了父类的析构函数。如果我们子类的构造函数手动申请了内存(用new,申请在堆中),那么这段内存也就自然没有被释放,比如下面这种类的声明:
// Circle类的声明
class Circle : public Shape{
public :
//...
virtual double calcArea();
private:
//...
Coordinate *m_pCenter;// Coordinate类的指针
}
// Circle类的实现-构造函数
Circle::Circle(int x,int y,double r){
//...
m_pCenter = new Coordinate(x,y);// 在堆上创建Coordinate类
}
// Circle类的实现-析构函数
Circle::~Circle(){
delete m_pCenter;// 释放堆中申请的内存
m_pCenter = nullptr;
}
如上所示,如果没有调用Circle类的析构函数,m_pCenter所指向的类是不会被释放的。
再次强调 - 使用delete释放对象:
当使用delete
去销毁对象时,如果delete
后面跟着的是父类的指针,那么将只调用父类的析构函数。如果delete
后面跟着子类的指针,那么将调用子类和父类的析构函数。
解决动态多态带来的内存释放问题:
要解决这个问题就需要使用虚析构函数,在上一篇笔记有说明以及代码(04继承篇第4章“isA语法“)。
在析构函数上使用virtual修饰即可;需要注意的是,和动态多态中的普通虚函数一样,virtual会被继承,所以在父类的析构函数上加virtual修饰之后,也需要在子类的析构函数上加virtual修饰,起到提示的作用。
虚函数的使用限制:
- 只能修饰类中的成员函数。
- 不能修饰静态的成员函数。(因为静态成员函数不属于任何一个对象,它是和类同生共死的)
- 不能修饰内联函数,如果使用会使计算机忽视inline修饰符。
- 构造函数不能成为虚函数。
关于类的构造函数和析构函数调用顺序:
本例中使用了new
来在堆上创建Coordinate的内存空间,而堆上的空间是需要我们手动申请和释放的;
所以Coordinate对象析构函数的调用,完全取决于我们什么时候使用delete释放对象,所以Coordinate对象析构函数并不一定在Circle的析构函数之前调用;
例如本例中,就是将Coordinate对象的释放放在Circle的析构函数中,导致Coordinate对象析构函数在Circle的析构函数运行时进行调用。
本章小总结:
- 只有虚析构函数,没有虚构造函数。
- 虚函数特性可以被继承,当子类中定义的函数与父类中虚函数的声明相同时,该函数也是虚函数。
- 虚析构函数是为了避免使用父类指针释放子类对象时造成内存泄露。
5.虚函数的实现原理-多态的实现原理
虚函数的实现:
在本例中,子类Circle没有实现calcArea()
这个虚函数,所以直接继承父类的虚函数。
// 父类,声明了calcArea()这个虚函数
class Shape{
public:
// 虚函数
virtual double calcArea(){
}
protected:
int m_iEdge;
};
// 子类,没有声明calcArea()虚函数,继承父类的虚函数
class Circle : public Shape {
public:
Circle(double r);
private:
double m_dR;
}
虚函数表的指针:
在具有虚函数的情况下,实例化一个对象的时候,这个对象的第一块内存当中是一个指针,那么这个指针就是虚函数表的指针。
父类、子类与虚函数表之间的关系:
父类:
[外链图片转存失败(img-s7WRdUvO-1563352606307)(images/muke_C++/010.png)]
vftable_ptr:虚函数表指针,指向虚函数表。
calcArea_ptr:在虚函数表中的函数指针,指向需要调用的函数。
子类:
[外链图片转存失败(img-UycqnOhN-1563352606308)(images/muke_C++/011.png)]
如果对当前的子类添加虚函数的声明:
class Circle : public Shape {
public:
Circle(double r);
virtual double calcArea();// 添加了虚函数的声明
private:
double m_dR;
}
则子类与虚函数表之间的关系变为:
在这里calcArea_ptr这个指针函数指向的地址发生了变化,不再指向与父类相同的calcArea()
函数的地址。(覆盖对0x3355的指向,重新指向到0x4B2C上)
[外链图片转存失败(img-LBetAv9r-1563352606309)(images/muke_C++/012.png)]
上面这些就是实现多态的原理了。
6.函数的覆盖与隐藏
函数的隐藏:
在我们还没有学习多态的时候,如果定义了父类和子类,父类和子类出现了重名函数,这个时候就称之为 函数的隐藏。(看第4篇继承篇第3章“继承中同名成员的隐藏”)
// Person为Soldier的父类
int main(){
Soldier so;
so.play();
// 调用子类中的play()
return 0;
}
函数的覆盖:
函数的覆盖特点是使用了虚函数。
如果没有在子类当中定义同名的虚函数,那么在子类虚函数表当中,就会写上父类的相应的那个虚函数的函数入口地址。如果我们在子类当中也定义了重名的虚函数,那么在子类的虚函数表当中,我们就会把原来的父类的虚函数的函数地址,覆盖成子类的虚函数的函数地址,那么这种情况就称值为 函数的覆盖。(看本篇第2章“普通虚函数”)
7.虚析构函数的实现原理
虚析构函数的特点是:
当我们通过父类当中,通过virtual
修饰析构函数之后,我们通过父类的指针再去指向子类的对象,然后通过delete
接父类指针,就可以释放掉子类对象了。
虚析构函数的理论前提:
执行完子类的析构函数就会执行父类的析构函数。
结合特点和理论前提:
可以看出,实际上虚析构函数的特点就是函数的覆盖带来的。父类的指针指向子类,然后就可以通过delete调用子类和父类的析构函数。
对象的大小:
对象的大小包含类的成员属性、虚函数表的指针大小,不包含成员函数的大小。
8.虚函数小结
-
在C++中多态的实现是通过虚函数表实现的。
-
当类中仅含有虚析构函数,不含其它虚函数时,
不产生虚函数表也会产生虚函数表。 -
每个类只有一份虚函数表,所有该类的对象共用同一张虚函数表。
-
两张虚函数表中的函数指针可能指向同一个函数。
问:
类普通成员函数怎么调用?
为什么成员数据有内存存放,虚成员函数能通过虚函数指针->虚函数表找到?
普通成员函数至少有函数指针吧,不然怎么找到实现呢?
如果有为什么对象大小只有数据成员的大小?
答:
因为,内存中有程序代码区,堆区,栈区,全局区(静态区),文字常量区。在定义一个类时,它的成员函数,虚构函数,构造函数就被存入程序代码区,供所有对象调用。
在实例化一个类的对象时,并没有拷贝类的函数,仅仅存入了数据成员,因此类的对象中有数据成员,然而当用父类的指针指向子类的对象时,调用同名函数时会调用父类的同名函数,当想要调用子类的同名函数引入了虚函数,
当调用普通成员函数时,计算机可以在代码区识别该函数,无需用函数指针,因而,在对象中只有数据成员的大小。
转自:类普通成员函数怎么调用?
问:
为什么要用父类的指针实例化子类的对象?为什么不直接使用子类的指针实例化子类的对象?
答:
父类A既可以用A本身实例化,也可以用子类B来实例化,一种方法,多种表达方式,所以是多态。
转载自:为什么要用父类的指针实例化子类的对象?
9.纯虚函数
写法:没有函数体,等于0。即,只有函数声明没有函数定义的虚函数。
class Shape{
public:
// 虚函数
virtual double calcArea(){
return 0;
}
// 纯虚函数
virtual double calcPerimeter() = 0;
}
纯虚函数与虚函数表:
[外链图片转存失败(img-pTWRysfZ-1563352606309)(images/muke_C++/013.png)]
纯虚函数的不像普通虚函数那样有函数指针(或者说函数指针没有指向实现的函数,因为实现的函数压根不存在)。
10.抽象类
含有纯虚函数的类叫抽象类。
-
不允许抽象类实例化对象。
-
抽象类的子类也可能是抽象类。
11.接口类
没有构造函数、析构函数,没有数据成员,只有纯虚函数。
- 仅含有纯虚函数的类称为接口类。
- 接口类也是抽象类
- 接口类
不能可以被继承 - 不能使用接口类实例化对象接口类中仅有纯虚函数,不能含有其它函数,但
可以不能含有数据成员。 - 可以使用接口类指针指向其子类对象,并调用子类对象中实现的接口类中纯虚函数。
- 一个类可以继承一个接口类,也可以继承多个接口类。
- 一个类可以继承接口类的同时也继承非接口类。
接口类和抽象类在构成上的区别:
接口类:只有纯虚函数,不包含其它非纯虚函数。(无其他成员函数、构造函数、析构函数、数据成员)
抽象类:含有纯虚函数,还可以有其他成员函数。(可以含有成员函数、数据成员、构造函数、析构函数)
12.RTTI-运行时内存识别(Run-Time Type Identification)
RTTI中主要会用到dynamic_cast 和typeid。
看一下常见用法:
// 判断当前指针或引用的类型
// 头文件: #include<typeinfo>
if(typeid(*obj) == typeid(Bus)){
}
// 指针类型转换
// Bird是Flyable的子类(必须要有继承关系)
// 并且必须是多态类型(类中含有虚函数)
Flyable *p2 = new Bird();
Bird *b2 = dynamic_cast<Bird *>(p2);
用代码举个例子:
// Flyable,接口类
class Flyable{
public:
virtual void takeoff() = 0;// 起飞,纯虚函数
virtual void land() = 0;// 降落,纯虚函数
};
// Bird,继承Flyable,,实现了父类的纯虚函数
class Bird : public Flyable{
public:
void foraging(){......}
virtual void takeoff(){......}
virtual void land(){......}
private:
// ...
};
// Plane,继承Flyable,实现了父类的纯虚函数
class Plane : public Flyable{
public:
void carry(){......}
virtual void takeoff(){......}
virtual void land(){......}
}
// 全局函数:
void doSomethine(Flyable *obj){
obj->takeoff();
// 这里要实现如下效果:
// 如果是Bird,则运行“觅食”
// 如果是Plane,则运行“运输”
cout << typeid(*obj).name() << endl; // 输出类名
if(typeid(*obj) == typeid(Bird)){
// 判断类的类型(typeid)
Bird *bird = dynamic_cast<Bird *>(obj);// 转化为子类指针
bird->foraging();
// 调用子类函数
}
obj->land();
}
// 调用全局函数,传递地址到指针中
int main() {
Bird b;
doSomething(&b);
return 0;
}
dynamic_cast 注意事项:
- 只能应用于指针和引用的转换。
- 要转换的类型中必须包含虚函数。
- 转换成功返回子类的地址,失败返回NULL。
typeid 注意事项:
- type_id返回一个type_info对象的引用。
- 如果想通过基类的指针获得派生类的数据类型,基类必须带有虚函数。
- 只能获取对象的实际类型
RTTI小结:
-
RTTI技术可以通过父类指针识别其所指向对象的真实数据类型
-
运行时类型别必须建立在虚函数的基础上,否则无需RTTI技术
-
只要存在继承关系就一定可以使用运行时类型识别技术要使用RTTI,必须存在继承关系,继承关系不是RTTI的充分条件,只是必要条件,所以存在继承关系的类不一定可以用RTTI技术。
13.异常处理
常见的异常:
- 数组下表越界
- 除数为0
- 内存不足
异常处理关键字:
try...catch...
:在try运行逻辑代码,如果要抛出异常,使用throw
,在catch中进行捕获并处理。
throw
:抛出异常。(注意:throw之后的代码将不会再运行)
try与catch是一对多的关系,举个例子:
void fun1(){
throw 1;
}
int main(void){
try{
fun1();
}
catch(int){// 当抛出的异常为int类型
//...
}
catch(double){// 当抛出的异常为double类型
//...
}
catch(...){// 这种写法可以捕获所有的异常,用来兜底
//...
}
}
如何获取到抛出的异常信息:
#include <iostream>
#include <string>
#include <stdlib.h>
using namespace std;
/**
* 定义函数division
* 参数整型dividend、整型divisor
*/
int division(int dividend, int divisor)
{
if(0 == divisor)
{
// 抛出异常,字符串“除数不能为0”
throw string("除数不能为0");
}
else
{
return dividend / divisor;
}
}
int main(void)
{
int d1 = 10;
int d2 = 2;
int r = 0;
cin >> d1;
cin >> d2;
// 使用try...catch...捕获异常
try{
int x = division(d1,d2);
cout << "division = " << x << endl;
}catch(string &errorStr){
cout << errorStr << endl;
}
return 0;
}
异常处理和多态之间的关系:
可以创建一个Exception
的父类,下面有IndexException
、MemoryException
等子类,然后我们就可以利用多态的特性,用父类捕获各子类,然后做出不同的操作了。
举个例子:
#include <iostream>
using namespace std;
// 父类Exception定义
class Exception{
public:
// 虚析构函数,用于父类指向子类时,
// 防止子类的构造函数不被调用,导致的内存泄漏问题
virtual ~Exception(){}
// 纯虚函数,在子类中实现
//virtual void printException() = 0;
// 虚函数,子类的实现可以将其覆盖
virtual void printException() {
cout << "Exception -- printException" << endl;
}
};
// 两个Exception子类定义
class IndexException : public Exception {
public:
virtual void printException(){
cout << "IndexException -- printException" << endl;
}
};
class MemoryException : public Exception {
public:
virtual void printException(){
cout << "MemoryException -- printException" << endl;
}
};
// 业务处理函数,假如在此函数中发生异常,我们将异常进行抛出
void businessProcessing(){
// 抛出索引异常
//throw IndexException();
// 抛出内存异常
throw MemoryException();
}
int main() {
// 对抛出的不同异常,可以做不同的操作(调用相应类中的printException函数)
try{
businessProcessing();
}
catch(Exception &e){
e.printException();
}
return 0;
}
/*输出:
MemoryException -- printException
或
IndexException -- printException
*/
这里就利用了类的多态 ,这样在业务处理中我们就能用Exception
去使用MemoryException
,IndexException
。
本篇为视频教程笔记,视频如下:
C++远征之多态篇
最后
以上就是顺心犀牛最近收集整理的关于C++面向对象笔记(5):多态篇C++多态篇的全部内容,更多相关C++面向对象笔记(5)内容请搜索靠谱客的其他文章。
发表评论 取消回复