多态

C++多态

问题引入

如果子类定义了与父类中原型相同的函数会发生什么?

  • 子类会覆盖父类的同名成员函数(子类重定义父类同名成员)
class Animal
{
public:
	void  cry()	//叫声
    {
        cout<<"Animal how to cay"<<endl;
    }
};
class Dog:public Animal
{
public:
    void cry()
    {
        cout<<"Dog:汪汪"<<endl;
    }
};
void animalCry(Animal* base)
{
    base->cry();
}
/*******main*******/
Animal *base = new Animal;
animalCry(base);	//调用父类的cry
base = new Dog;
animalCry(base);	//调用父类的cry

从上面可以看出,父类指针无论是指向自己,还是指向子类,调用的cry都是父类的,这不是我们所期望的,我们所期望的是:

  • 根据父类指针指向的对象的实际类型,调用不同类的cry函数
  • 父类指针指向父类,调用父类cry函数
  • 父类指针指向子类,调用子类cry函数

实现这种同一事物在不同场景下的不同表现形式,叫做多态

多态实现

在父类重写的函数的前面加上virtual关键字(子类可写可不写)

 virtual void  cry()	//叫声
 {
      cout << "Animal how to cay" << endl;
 }

多态成立的三要素:

  • 1,要有继承
  • 2,要有虚函数重写
  • 3,要有父类指针(引用)指向子类对象

多态是设计模式的基础,多态是框架的基石

多态的意义

为什么要使用多态呢?有什么好处?

案例:定义形状(Shape)类,从中派生矩形和圆形,利用多态求不同形状的面积

class Shape
{
public:
	Shape(int x = 0, int y = 0)
	{
		this->m_x = x;
		this->m_y = y;
	}
	void setPosition(int x, int y)
	{
		m_x = x;
		m_y = y;
	}
	virtual void getArea()
	{
		cout << "我是形状类,没有面积" << endl;
	}
	~Shape()
	{
		cout << "~Shape()" << endl;
	}
protected:
	int m_x;
	int m_y;
};
//矩形类,宽度,高度
class Rect :public Shape
{
public:
	Rect(int x = 0, int y = 0, int length = 0, int width = 0) :m_len(length), m_width(width), Shape(x, y)
	{
	}
	void getArea()
	{
		cout << "我是矩形,我的面积是:" << m_width * m_len << endl;
	}
	~Rect()
	{
		cout << "~Rect()" << endl;
	}
protected:
private:
	int m_width;	//宽度
	int m_len;	//长度
};
//矩形类,宽度,高度
class Circle :public Shape
{
public:
	Circle(int x = 0, int y = 0, int r = 0) :m_r(r), Shape(x, y)
	{
	}
	void getArea()
	{
		cout << "我是圆形,我的面积是:" << 3.14 * m_r * m_r << endl;
	}
	~Circle()
	{
		cout << "~Circle()" << endl;
	}
protected:
private:
	int m_r;//半径
};
//等边三角形
class Trilateral : public Shape
{
public:
	void getArea()
	{
		cout << "我是等边三角形" << endl;
	}
	~Trilateral()
	{
		cout << "~Trilateral()" << endl;
	}
private:
};
void show(Shape* shape)
{
	shape->getArea();
}
int main()
{
	Shape* base = new Rect(20, 50, 60, 30);
	base->getArea();
	delete base;

	base = new Circle(20, 20, 50);
	base->getArea();
	delete base;

	return 0;
}

虚析构函数

构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数

析构函数必须是虚函数,在有继承关系和父类指针指向子类对象时。如果析构函数不是虚函数,则对象析构时可能存在内在泄漏的问题。

本类指针指向本类对象

不管析构函数是否是虚函数(即是否加virtual关键词),delete时基类和子类都会被释放;

Rect* prect = new Rect(20, 50, 60, 30);
delete prect;

父类指针指向子类对象

  • 若析构函数是虚函数(即加上virtual关键词),delete时基类和子类都会被释放;
  • 若析构函数不是虚函数(即不加virtual关键词),delete时只释放基类,不释放子类;
Shape* base = new Rect(20, 50, 60, 30);
delete base;

函数重载、重写、重定义

class Parent
{
public:
	virtual void func()
	{
		cout << "Parent::func()" << endl;
	}
	virtual void func(int a,int b)
	{
		cout << "Parent::func(int a,int b) " <<a<<" "<<b<< endl;
	}
};
class Child:public Parent
{
public:
	virtual void func(int a, int b)
	{
		cout << "Child::func(int a, int b) " << a << " " << b << endl;
	}
	virtual void func(int a, int b, int c)
	{
		cout << "Child::func(int a, int b, int c) " << a << " " << b <<" "<<c<< endl;
	}
};
  • 函数重载:必须在同一个类(作用域)中进行;子类无法重载父类中的函数,只能进行重定义。
  • 函数重定义:一旦子类写了和父类同名的成员函数,父类的函数将被覆盖,无法直接访问
Child c;
c.func();			//无法访问
c.Parent::func();	//显式访问父类成员
  • 函数重写:重写发生在父类与子类之间,并且父类和子类中的函数具有完全相同的函数原型;使用virtual声明的函数重写,会在父类指针调用时,根据指针指向的实际对象的类型,形成多态特性;如果不加virtual,叫作重定义

final和override

final可以用来修饰父类的虚函数,表示在子类中禁止对改虚函数进行重写

override可以用来修饰子类的虚函数,表示是虚函数重写

动态联边和静态联编

联编是指一个程序模块、代码之间互相关联的过程。

静态联编:是程序的匹配、连接在编译阶段实现,也称为早期匹配。

  • 重载函数的使用就是静态联编

动态联编:是程序的匹配、连接在运行阶段实现,也称为迟绑定。(将函数体和函数调用关联起来,就叫绑定)

  • 分支语句就是动态联编,多态也是动态联编
多态中的动态联编如何实现的?

如果我们声明了类中的成员函数为虚函数,那么C++编译器会为类生成一个虚函数表,通过这个表即可实现动态联编。

虚函数表是什么?如何找到它?
  • 虚函数表是顺序存放虚函数地址的,虚表是顺序表(数组),依次存放着类里面的虚函数。

  • 虚函数表是由编译器自动生成与维护的,相同类的不同对象的虚函数表是一样的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d3qPJ5uX-1615733384101)(assets/image-20210311180154347.png)]

  • 既然虚函数表是一个顺序表,那么它的首地址存放在哪里呢?其实当我们在类中定义了virtual函数时,C++编译器会偷偷的给对象添加一个vptr指针,vptr指针就是存的虚函数表的首地址。
如何证明vptr指针的存在
  • 用sizeof计算Parent的大小,发现是4个字节,说明类里面有个隐藏vptr指针
sizeof(Parent);

示例:

class Parent
{
public:
	virtual void f() { cout << "Parent::f()" << endl; }
	virtual void g() { cout << "Parent::g()" << endl; }
	virtual void h() { cout << "Parent::h()" << endl; }
};

通过调试发现vptr指针是确实存在的,而且指向的数组存着所有的虚函数的地址。
在这里插入图片描述

如何找到vptr指针,去调用虚函数

操作比较繁琐,咱们逐步分析

  • vptr指针是类的第一个成员,所以只需要对对象取地址&p,即可得到vptr指针的首地址
  • 指针是四个字节,所以把&p强转为int*,即可把对象分为每个元素都是四个字节的数组,然后获得第一个元素,即vptr指针(*(int*)&p) 或者()(int*)&p)[0]。
  • 因为vptr指针存储的是虚函数表的首地址,所以先把vptr指针转成int*指针,然后就可以访问每个成员了(这里的每个成员都是函数指针了)*((int*)(*(int*)&p)+i)
  • 最后,把得到的结果转为函数指针即可调用了
typedef void(*FunType)();

Parent p;
FunType func = (FunType)*((int*)(*(int*)&p)+0);
func();

//还可以循环调个遍
for (int i = 0; i < 3; i++)
{
	FunType func = (FunType) * ((int*)(*(int*)&p) + i);
	func();
}

纯虚函数和抽象类

纯虚函数:纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本(如果不定义,那么派生类也是抽象类)

纯虚函数为各派生类提供一个公共界面(接口的封装和设计、软件的模块功能划分)

virtual 类型 函数名(参数列表) = 0;		//纯虚函数

抽象类:一个具有纯虚函数的基类称为抽象类。

  • 特点

    • 不能被实例化(定义对象)
    • 可以定义指针或引用
  • 案例:把求各种形状的面积,改为抽象类的方式

面向对象三大概念:

  • 封装:突破了C语言函数的概念
  • 继承:代码复用——可以用以前人写的代码
  • 多态:可以使用未来人写的代码

纯虚函数*:纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本(如果不定义,那么派生类也是抽象类)

纯虚函数为各派生类提供一个公共界面(接口的封装和设计、软件的模块功能划分)

virtual 类型 函数名(参数列表) = 0;		//纯虚函数

抽象类:一个具有纯虚函数的基类称为抽象类。

  • 特点

    • 不能被实例化(定义对象)
    • 可以定义指针或引用
  • 案例:把求各种形状的面积,改为抽象类的方式

面向对象三大概念:

  • 封装:突破了C语言函数的概念
  • 继承:代码复用——可以用以前人写的代码
  • 多态:可以使用未来人写的代码
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值