C++学习笔记9——多态篇

本文深入解析C++中动态多态的概念,通过具体实例展示如何利用虚函数实现不同对象相同方法的不同行为,并探讨虚析构函数的重要性及其在避免内存泄漏中的作用。

教程:https://www.imooc.com/learn/474

在这里插入图片描述
在这里插入图片描述
静态多态(早绑定)
举例说明:
在这里插入图片描述
两个函数互为重载
在这里插入图片描述
第一个只有一个参数,调用的是第一个函数;第二个有两个参数,调用的是第二个函数。可见,程序在运行之前,在编译阶段,就已经确定使用哪个函数了,这样子叫做“早绑定”,也就是“静态多态”

动态多态(晚绑定)
举例说明:
在这里插入图片描述
都是要计算面积,但圆形算面积的方法跟矩形的是不同的,像这样,对不同的对象下达相同的指令,但却做不同的操作,叫做“动态多态”
在这里插入图片描述
使用动态多态要有封装和继承为基础。在封装篇,把数据封装到类中;在继承篇,将封装了的各个类形成了继承关系。
动态多态至少有两个类,只有使用三个类及以上,动态多态才表现得更为明显

举例讲解:

定义一个形状类:
在这里插入图片描述
定义一个圆形类(是形状类的子类):
在这里插入图片描述
圆面积的计算(是圆形类中的public方法):
在这里插入图片描述
定义一个矩形类(是形状类的子类):
在这里插入图片描述
矩形面积的计算(是矩形类中的public方法):
在这里插入图片描述
主函数:
在这里插入图片描述
可以使用一个父类的指针,指向其中一个子类的对象Circle,使用另外一个父类的指针,指向另一个子类的对象Rect,如此,两个子类的对象都被父类的指针所指向,在进行操作时,是通过父类的两个指针分别调用两个子类的计算面积方法(calcArea())。
像这样写,结果只是调用了两次父类的calcArea(),即下图定义时写的方法:
在这里插入图片描述
所以要实现动态多态,用以上方法不可实现

方法:用“virtual”关键字修饰成员函数,使其成为虚函数

以上例举例:
定义父类时:
在这里插入图片描述
定义子类时:
在这里插入图片描述
在这里插入图片描述
此时这个virtual不是必须的,如果没写,系统会自动加,但建议加上,方便阅读区分

主函数:
在这里插入图片描述

编码示例:
Shape.h

#ifndef SHAPE_H  //用宏定义避免重复包含
#define SHAPE_H

#include <iostream>
using namespace std;

class Shape
{
public:
	Shape();
	~Shape();
	double calcArea();
};
#endif // !SHAPE_H

Shape.cpp

#include "Shape.h"
#include <iostream>
using namespace std;

Shape::Shape()
{
	cout << "Shape()" << endl;
}

Shape ::~Shape()
{
	cout << "~Shape()" << endl;
}

double Shape::calcArea()
{
	cout << "Shape->calcArea()" << endl;
	return 0;
}

Circle.h

#ifndef CIRCLE_H
#define CIRCLE_H

#include "Shape.h"
class Circle : public Shape
{
public :
	Circle(double r);
	~Circle();
	double calcArea();
protected:
	double m_dR;
};
#endif // !CIRCLE_H

Circle.cpp

#include "Circle.h"
#include <iostream>
using namespace std;

Circle::Circle(double r)
{
	cout << "Circle()" << endl;
	m_dR = r;
}

Circle :: ~Circle()
{
	cout << "~Circle()" << endl;
}

double Circle::calcArea()
{
	cout << "Circle->calcArea()" << endl;
	return 3.14*m_dR*m_dR;
}

Rect.h

#ifndef RECT_H
#define RECT_H

#include "Shape.h"

class Rect : public Shape
{
public :
	Rect(double width, double height);
	~Rect();
	double calcArea();
protected:
	double m_dWidth;
	double m_dHeight;
};
#endif // !RECT_H

Rect.cpp

#include "Rect.h"
#include <iostream>
using namespace std;

Rect::Rect(double width, double height)
{
	cout << "Rect()" << endl;
	m_dHeight = height;
	m_dWidth = width;
}

Rect :: ~Rect()
{
	cout << "~Rect()" << endl;
}

double Rect::calcArea()
{
	cout << "Rect->calcArea()" << endl;
	return m_dHeight * m_dWidth;
}

demo.cpp

#include "Circle.h"
#include "Rect.h"
#include <iostream>
using namespace std;

int main()
{
	Shape *shape1 = new Rect(3, 6);
	Shape *shape2 = new Circle(5);

	shape1->calcArea();
	shape2->calcArea();

	delete shape1;
	shape1 = NULL;
	delete shape2;
	shape2 = NULL;
	return 0;
}

在这里插入图片描述
由输出结果知,这样子编码,通过shape1、shape2只能调用到Shape类的calcArea(),且析构函数只执行了父类的

修改:
Shape.h
改为:

virtual double calcArea();

在这里插入图片描述
这样子实现了动态多态,但还是建议在子类的函数前也加上virtual

虚析构函数

动态多态中可能会存在内存泄漏问题
举例说明:

定义一个Shape类:
在这里插入图片描述
定义一个Circle类:
在这里插入图片描述
多定义了一个数据成员(Coordinate的指针),代表圆心坐标。
会在构造函数中实例化一个坐标对象:
在这里插入图片描述
在析构函数执行时释放对象
主函数:
在这里插入图片描述
如果delete后跟着的是父类的指针,只会执行父类的析构函数,如果跟着的是子类的指针,则会执行父类和子类的析构函数。
所以这样写,没有执行子类的析构函数,造成内存泄漏(因为在实例化Circle对象的时候,从对中申请了内存作为原点——m_pCenter = new Coordinate(x,y);

用虚析构函数解决
虚析构函数: virtual->析构函数
在这里插入图片描述
如上图,用virtual修饰(父类的)析构函数
此时子类的析构函数可加可不加,最好加(以便还有类继承)

virtual在函数中的使用限制
1、普通函数不能是虚函数:
只有类里面的成员函数才能用virtual修饰
2、静态成员函数不能是虚函数
静态成员函数不属于任何一个对象,是和类“同生共死”的。
在这里插入图片描述
3、内联函数不能是虚函数
在这里插入图片描述
如果用virtual修饰,会让系统忽视“inline”关键字,视为纯粹的虚函数
4、构造函数不能为虚函数

编码示例:
Coordinate.h

#ifndef COORDINATE_H
#define COORDINATE_H

class Coordinate
{
public:
	Coordinate(int x, int y);
	~Coordinate();
private:
	int m_iX;
	int m_iY;
};
#endif // !COORDINATE_H

Coordinate.cpp

#include "Coordinate.h"
#include <iostream>
using namespace std;

Coordinate::Coordinate(int x, int y)
{
	cout << "Coordinate()" << endl;
	m_iX = x;
	m_iY = y;
}

Coordinate ::~Coordinate()
{
	cout << "~Coordinate()" << endl;
}

Shape.h

#ifndef SHAPE_H  //用宏定义避免重复包含
#define SHAPE_H

#include <iostream>
using namespace std;

class Shape
{
public:
	Shape();
	~Shape();
	virtual double calcArea();
};
#endif // !SHAPE_H

Shape.cpp

#include "Shape.h"
#include <iostream>
using namespace std;

Shape::Shape()
{
	cout << "Shape()" << endl;
}

Shape ::~Shape()
{
	cout << "~Shape()" << endl;
}

double Shape::calcArea()
{
	cout << "Shape->calcArea()" << endl;
	return 0;
}

Circle.h

#ifndef CIRCLE_H
#define CIRCLE_H

#include "Shape.h"
#include "Coordinate.h"
class Circle : public Shape
{
public :
	Circle(double r);
	~Circle();
	double calcArea();
protected:
	double m_dR;
	Coordinate *m_pCenter;
};
#endif // !CIRCLE_H

Circle.cpp

#include "Circle.h"
#include <iostream>
using namespace std;

Circle::Circle(double r)
{
	cout << "Circle()" << endl;
	m_dR = r;
	m_pCenter = new Coordinate(3, 5);  //从堆中申请内存
}

Circle :: ~Circle()
{
	cout << "~Circle()" << endl;
	delete m_pCenter;
	m_pCenter = NULL;
}

double Circle::calcArea()
{
	cout << "Circle->calcArea()" << endl;
	return 3.14*m_dR*m_dR;
}

Rect.h

#ifndef RECT_H
#define RECT_H

#include "Shape.h"

class Rect : public Shape
{
public :
	Rect(double width, double height);
	~Rect();
	double calcArea();
protected:
	double m_dWidth;
	double m_dHeight;
};
#endif // !RECT_H

Rect.cpp

#include "Rect.h"
#include <iostream>
using namespace std;

Rect::Rect(double width, double height)
{
	cout << "Rect()" << endl;
	m_dHeight = height;
	m_dWidth = width;
}

Rect :: ~Rect()
{
	cout << "~Rect()" << endl;
}

double Rect::calcArea()
{
	cout << "Rect->calcArea()" << endl;
	return m_dHeight * m_dWidth;
}

demo.cpp

#include "Circle.h"
#include "Rect.h"
#include <iostream>
using namespace std;

int main()
{
	Shape *shape1 = new Rect(3, 6);
	Shape *shape2 = new Circle(5);

	shape1->calcArea();
	shape2->calcArea();

	delete shape1;
	shape1 = NULL;
	delete shape2;
	shape2 = NULL;
	return 0;
}

在这里插入图片描述
执行Coordinate的析构函数是因为Circle.cpp Circle的析构函数实例化了一个Coordinate的对象,该对象的销毁在~Circle()中,但这里并没有执行Circle的析构函数,造成内存泄漏

解决:
Shape.h

virtual ~Shape();

在这里插入图片描述
如果Circle类中没有Coordinate *m_pCenter; 那么没有虚析构函数也是可以的

虚函数与虚析构函数原理

指针指向对象——对象指针
指针指向函数:
在这里插入图片描述
中间时5个函数的函数地址,当指针指向函数入口,命令计算机开始执行时,计算机使函数体得到执行,直到执行完毕
函数指针与普通指针本质一样,也是由四个基本内存单元组成,存储一个内存的地址,这个内存的地址就是函数的首地址
举例说明:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当实例化一个Shape对象的时候,在这个Shape对象中,除了m_iEdge这个数据成员外,还有一个数据成员,称为虚函数表指针,虚函数表指针也是一个指针,占有四个基本内存单元。虚函数表指针指向一个虚函数表,这个虚函数表会与Shapele的定义一起出现。虚函数表也会占用空间,这里假设虚函数表的起始位置为0xCCFF,那么这个虚函数表指针的值就是0xCCFF。父类的虚函数表只有一个,通过父类实例化出来的所有对象,其虚函数表指针的值都是0xCCFF,确保该类的每一个对象的虚函数表指针都指向自己的虚函数表。在父类Shape的虚函数表中,肯定定义了一个函数指针,这个函数指针指向calcArea()这个函数的地址,这里假设calcArea()函数的入口地址是0x3355,那么虚函数表中的calaArea_ptr当前的位置就是0x3355。
调用时,先找到虚函数表指针,通过虚函数表指针找到虚函数表,通过位置的偏移找到相应的虚函数的入口地址,从而找到当前定义的虚函数calcArea()

例2:
在这里插入图片描述
实例化一个Circle对象,Circle类中没有定义虚函数,但从父类中继承了虚函数,所以在实例化一个Circle对象时,也会产生一个虚函数表,这个虚函数表是Circle的虚函数表,起始地址是0x6688,与Shape类的不一样,但在虚函数表中,calcArea()
的偏移地址与Shape类的是一样的,这样保证在Circle中访问calcArea()函数时,能通过虚函数指针找到自己的虚函数表,在自己的虚函数表中找到calcArea()函数是指向父类的calcArea()函数的入口的

如果在Circle中也定义了calcArea()函数:
在这里插入图片描述
对Shape类无变化
对Circle类:
在这里插入图片描述
虚函数表无变化,但此时已在勒种定义了自己的calcArea()函数,所以其虚函数表中关于calcArea()的指针已经覆盖了原有父类指针的值,已经变为0x4B2C
所以,按上图写法,用Shape的指针指向Circle的对象,会通过Circle对象中的虚函数表指针找到Circle的虚函数表,通过Circle的虚函数表,以和父类相同的偏移量,找到Circle的虚函数的入口地址,从而执行子类中的虚函数

函数的覆盖与隐藏

隐藏:父类和子类出现同名
覆盖:如上例,如果没在Circle中定义有同名的虚函数,虚函数表中虚函数的地址会继续沿用父类的,但在Circle中定义了,虚函数表中虚函数的地址会覆盖父类的,改为子类的虚函数的地址

虚析构函数的实现原理

通过在父类中用virtual修饰析构函数后,用父类的指针指向子类的对象,使用delete 父类指针就可以释放子类对象

理论前提:
执行完子类的析构函数就会执行父类的析构函数

思路:用父类的指针指向子类的析构函数就可以了

在这里插入图片描述
上图,在父类中定义了虚析构函数,那么在父类的虚函数表中,就会有一个父类析构函数的函数指针~Shape_ptr
子类虚函数表中,也会产生一个子类的析构函数的函数指针,指向子类的析构函数。
这时如果使用父类的指针指向子类的对象(这里是用Shape的指针指向Circle的对象),通过delete Shape的指针,可以通过Shape找到子类的虚函数表指针,通过子类的虚函数表指针找到虚函数表,通过虚函数表找到子类的析构函数,从而使得子类的析构函数可以执行,子类的析构函数执行完毕后系统会自动执行父类的析构函数

证明虚函数表的存在

概念说明:
1、对象的大小:在类实例化对象时,其数据成员所占内存大小(注意是数据成员,不包括成员函数)(例如,Shape类没有数据成员,理论上不占内存,但为了标记他的存在,会用一个内存单元来标记,用sizeof()输出会显示是1,但一旦后面又增加了数据成员,例如加一个int类型的(占4个内存单元),用sizeof()会显示4而不是5;Circle类有一个int数据成员, 占4个内存单元)
2、对象的地址:通过一个类实例化一个对象,该对象会在内存中占一定的内存单元,内存单元的第一个地址就是该对象的地址
3、对象成员的地址:当用一个类去实例化一个对象之后,这个对象中可能有一个或多个数据成员,每个数据成员所占的地址就是对象成员的地址。对象的每个数据成员,因为类型不同,占据的内存大小也有不同,所以地址也是不同的
4、虚函数表指针:在具有虚函数的情况下,实例化一个对象的时候,该对象的第一块内存中存储的是一个指针,这个指针是虚函数表的指针,因为是指针,所以占据内存大小也是4。据此特点,可以通过计算对象的大小来证明虚函数表指针的存在

注意:

Shape shape;
int *p = (int*)&shape;  //要记得强制转换为int

每个类只有一份虚函数表,所有该类的对象共用同一张虚函数表(√)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值