【C++】多态:虚函数、重写、抽象类、虚函数表

概述

多态的概念:通俗讲就是多种形态,具体来说就是当需要完成某个行为时,由不同的对象去完成会产生不同的结果。
比如乘车买票,普通人必须买全票,学生可以买半票。

实现

在C++中实现多态需要满足的条件

1、前提:发生继承
2、父类中要有虚函数
3、调用虚函数的类型必须是指针或者引用
4、虚函数需要在子类中被重写
5、一般是通过父类的指针或者引用来调用虚函数 (可以将子类对象的地址或引用赋值给父类的指针或引用—切片)

代码示例:

#include<iostream>
using namespace std;
class Person {
public:
	virtual void BuyTicket()
	{
		cout << "全票" << endl;
	}
};
class Student :public Person {
public:
	void BuyTicket()
	{
		cout << "半票" << endl;
	}
};
void test()
{
	Person p;
	Person* p1 = &p;
	Person& p2 = p;
	//p.BuyTicket();	//用对象来调用虚函数时,不会产生多态

	p1->BuyTicket();	
	p2.BuyTicket();

	Student s;
	p1 = &s;			//切片
	Person& p3 = s;		//切片
	p1->BuyTicket();
	p3.BuyTicket();
}
int main()
{
	test();
	return 0;
}

运行结果:
在这里插入图片描述

虚函数重写

在继承体系中,父类定义了一个虚函数,在子类中重新定义了一个函数名参数列表返回值和父类虚函数完全相同的函数,这叫做虚函数的重写。
注意:子类重写父类虚函数时可以不加virtual关键字

重写的特例:
协变:重写的虚函数返回值可以和父类虚函数不同,但必须是具有继承关系的指针或者引用

#include<iostream>
using namespace std;
class Person {
public:
	virtual void BuyTicket()
	{
		cout << "全票" << endl;
	}
	virtual Person* RedPapper()
	{
		cout << "100块" << endl;
			return new Person;
	}
};
class Student :public Person {
public:
	//重写:函数名,参数列表、返回值与父类虚函数完全相同
	void BuyTicket()
	{
		cout << "半票" << endl;
	}
	//协变:返回值为具有继承关系的类指针或引用
	Student* RedPapper()
	{
		cout << "1毛" << endl;
		return new Student;
	}
};

void test()
{
	Person p;
	Person* p1 = &p;
	Person& p2 = p;
	//p.BuyTicket();	//用对象来调用虚函数时,不会产生多态

	p1->BuyTicket();	
	p2.BuyTicket();
	p1->RedPapper();
	p2.RedPapper();

	Student s;
	p1 = &s;
	Person& p3 = s;
	p1->BuyTicket();
	p3.BuyTicket();

	p1->RedPapper();
	p3.RedPapper();
}
int main()
{
	test();
	return 0;
}

运行结果:
在这里插入图片描述
协变除了上述定义方法外,其返回值还可以是自定义的具有继承关系的类引用或指针,例如:

#include<iostream>
using namespace std;

class A{};

class B :public A {};


class Person {
public:
	virtual A* RedPapper()
	{
		cout << "100块" << endl;
		return new A;
	}
};

class Student :public Person {
public:
	//协变:返回值为具有继承关系的指针或引用
	B* RedPapper()
	{
		cout << "1毛" << endl;
		return new B;
	}
};

void test()
{
	Person p;
	Person* rp=&p;
	rp->RedPapper();

	rp = (Person*)new Student;
	rp->RedPapper();
}

int main()
{
	test();
	return 0;
}

运行结果:
在这里插入图片描述

override关键字 final关键字

override关键字的作用:
用于修饰子类中的函数,自动检查这个函数是否重写父类的虚函数,如果没有重写则报错,即强制重写。

//强制重写
virtual void fun() override
{}

final关键字的作用:
1、修饰类:表示该类无法被继承

//表示类A不能被继承
class A final
{};

2、修饰父类虚函数:表示该虚函数不能被重写

//无法被重写
virtual void fun() final
{}

抽象类

纯虚函数:在虚函数后面写上=0,表示该函数是一个纯虚函数,例如:

virtual void fun() =0;

抽象类:包含纯虚函数的类叫做抽象类(也叫接口类)。

抽象类不能实例化出对象
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
如果不实现多态,不要把函数定义成虚函数。

虚函数表

阅读代码:
在这里插入图片描述

显然,类A中只有一个int型的数据成员,那为什么这里对象a却占了8个字节的空间呢?

调试:
在这里插入图片描述

通过调试我们发先对象a中多了一个名为_vfptr的指针,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function),虚函数表指针指向的就是该类的虚函数表。
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
虚函数表其实是一个指针数组。

下面再深入理解一下:

#include<iostream>
using namespace std;
class A {
public:
	A() {}
	virtual void fun1() {}
	virtual void fun2() {}
	void fun3() {}
	int _a;
 };
class B :public A {
public:
	void fun1(){}
	int _b;
};
int main()
{
	A a;
	B b;
	cout << sizeof(a) << endl;
	return 0;
}

在这里插入图片描述

说明:
1、派生类对象b中也有一个虚表指针,这个虚表指针中也有一部分继承自基类
2、基类a对象和派生类b对象虚表是不一样的,这里我们发现fun1完成了重写,所以b的虚表中存的是重写的B::fun1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。
3、fun2继承下来后是虚函数,所以放进了虚表,fun3也继承下来了,但不是虚函数,所以不会放进虚表。
4、虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
5、 总结一下派生类的虚表生成
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值