C++面向对象之----多态

多态

  • 多态
    • 多态构成条件
  • 虚函数
    • 虚函数的重写
    • C++11 override 和 final
    • 函数重载,虚函数的覆盖,继承中隐藏的区别
  • 抽象类
  • 多态的原理
    • 虚表的打印
    • 静态绑定与动态绑定
    • 多继承中的虚表
  • 总结

生活中我们是否会见到这样的场景,比如说我们想要去某个景区玩玩,再买票的时候,门口就有着学生半价,成人是全票,小孩免费什么的。对于这种场景,我们肯定不会对每一种人群写一个专门的函数,这样代码的复用性就变得很差。
C++面向对象之----多态_第1张图片
这个时候就需要使用多态来解决

多态

通俗的来说,多态就是一个事物的多个形态。当我们想要实现某个功能时,对于这个功能的不同对象都有着不同的状态。

这有个常见的例子,就是买票。经常会看的成人全票,学生证半价的情况。针对这种情况,定义两个多态的成员类

class People
{
public:
	virtual void BuyTicket()
	{
		cout << "成人,买全价票" << endl;
	}
};

class Student:public People
{
public:
	virtual void BuyTicket()
	{
		cout << "学生,买半价票" << endl;
	}
};

函数调用的方法

void Buy(People* Pobj)
{
	Pobj->BuyTicket();
}

void text1()
{
	People Pobj;
	Student Sobj;

	//成人
	Buy(&Pobj);

	//学生
	Buy(&Sobj);
}

结果
C++面向对象之----多态_第2张图片

多态构成条件

  1. 必须通过父类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数(virtual),并且子类必须对父类的虚函数进行重写

虚函数

virtual修饰的类成员函数称为虚函数

class People
{
public:
	virtual void BuyTicket()
	{
		cout << "成人,买全价票" << endl;
	}
};

虚函数的重写

虚函数的重写也叫做虚函数的覆盖,就是在子类中有一个跟父类完全相同的虚函数。

也就是子类的虚函数与父类的虚函数返回值类型,函数名字,参数列表完全相同,称子类的虚函数重写了父类的虚函数

class Student:public People
{
public:
	virtual void BuyTicket()
	{
		cout << "学生,买半价票" << endl;
	}
};

注意:子类在重写虚函数的时候,函数前是可以不用加virtual关键字,但是子类继承的父类是一定要加virtual关键字的。

虚函数重写的两个例外

  1. 协变(子类和父类虚函数的返回值类型不同)

子类在重写父类虚函数的时候,和父类虚函数返回值类型不同。也就是父类虚函数返回父类对象的指针或者引用,而子类虚函数返回子类对象的指针或者引用。

class A
{};

class B : public A 
{};

class People
{
public:
	virtual A* BuyTicket()
	{
		cout << "成人,买全价票" << endl;
		return nullptr;
	}
};

class Student :public People
{
public:
	virtual B* BuyTicket()
	{
		cout << "学生,买半价票" << endl;
		return nullptr;
	}
};

也就是子类与父类虚函数的返回值可以不相同,但是这个返回值必须也是一个继承的关系,父类返回父类,子类返回子类

C++面向对象之----多态_第3张图片
2. 析构函数的重写

C++面向对象之----多态_第4张图片
可以发现,当我们的父类是用子类强转过来的对象时,在进行析构函数的过程中,只是析构了切片后的父类,没有析构剩余子类部分。这里没有报错是因为类中并没有指针类型的成员变量。

这个时候就需要使用虚函数使析构函数构成多态,虽然析构函数中子类和父类的函数名看上去不同,不满足构成多态的条件,但实际上,编译后析构函数的名称统一都是destructor

C++面向对象之----多态_第5张图片
这个时候,只需要让子类与父类的析构函数也满足多态,等待调用析构函数的时候,其实是从虚表中找到各自的析构函数,然后调用。

C++11 override 和 final

如果我们定义一个虚函数,我们不想让他满足继承关系,这个时候就可以使用C++11中的这两个关键字

  1. final ,修饰虚函数,表示这个虚函数不能被继承

C++面向对象之----多态_第6张图片
2. override,检测子类虚函数是否重写了父类的某个虚函数,如果没有重写就会报错

C++面向对象之----多态_第7张图片
不加override,编译器没有报错,这时构成了隐藏,不是重写
C++面向对象之----多态_第8张图片

函数重载,虚函数的覆盖,继承中隐藏的区别

C++面向对象之----多态_第9张图片

抽象类

抽象,也就是那些看起来很奇怪的事情,很难理解的事物。

我们在虚函数的后面加一个=0,那么这个函数就构成了纯虚函数。包含纯虚函数的类叫做抽象类,也叫接口类。抽象类不能实例化出对象,他只是一个声明,没有定义

想要让抽象类发挥作用,只能在子类中重写该纯虚函数,利用子类来实例化出对象

//抽象类
class computer
{
public:
	virtual void Brand() = 0;
};

//子类进行实例化
class DELL :public computer
{
public:
	virtual void Brand()
	{
		cout << "Dell 戴尔" << endl;
	}
};

class Mac :public computer
{
public:
	virtual void Brand()
	{
		cout << "Mac" << endl;
	}
};

抽象类的实现可以看成一种接口继承

接口继承,就是纯虚函数的基础,子类继承的是父类的虚函数接口,子类的目的就是为了重写而达到多态,子类继承的是接口

实现继承,普通函数的基础都是实现的继承,子类继承了父类的函数,可以对继承的函数进行使用或者重写,他继承的是函数的实现。

多态的原理

C++面向对象之----多态_第10张图片
通过调试,可以看到在这个类的成员中,多了一个指针,而这个指针指向的是一个指针数组的地址。
C++面向对象之----多态_第11张图片
除了_P成员,还多一个__vfptr放在对象的前面,对象中的这个指针我们叫做虚函数表指针 (v代表virtual,f代表function)。

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表

虚表的打印

C++面向对象之----多态_第12张图片
我们可以用一个函数指针来完成对虚表的打印工作,利用int*来取对象的前四个字节,然后再进行解引用,最后把这个int型的数据转为函数指针的类型。

有时候,这个虚表的结尾可能不是一个0,所以可能会存在打印好多地址的情况。

C++面向对象之----多态_第13张图片
虚表中存储的是每个虚函数的地址,当我们让子类继承父类的时候,其实在子类的虚表中存储的是子类中虚函数的地址,如果发生了重新,那就换成重写后的虚函数的地址,没有重写,那么就是父类虚函数的地址。

如果子类中还有自己的虚函数,那么这个虚函数在虚表中的位置是在父类虚函数的后面
C++面向对象之----多态_第14张图片

而对于多态而言,当我们把子类对象的指针强转为父类对象时,同时被转过去的还有这个虚表,然后调用父类的对应虚函数时,其实调用的函数是虚表中子类重写的函数,这个时候就可以完成多态的功能。对于不同的情况,完成不同的操作。

虚函数时存在虚表中的,而虚表中存储的是函数的地址,在我们的程序中函数的地址都是存在代码段中的,所以虚表也是在代码段中的
C++面向对象之----多态_第15张图片
就像上面Show函数作为一个普通的函数,他的地址就是在代码段中,我们通过打印,发现Show函数的地址和虚表中函数的地址比较接近,所以可以判断出,虚表存在于代码段

静态绑定与动态绑定

  1. 静态绑定,这是在编译期间就确定了程序的行为,就像函数重载一样
  2. 动态绑定,是在程序运行期间,根据具体的类型决定程序的具体行为,然后调用具体的函数(所以不是模板,模板不是调用具体函数,而是推演),也就是多态了

多继承中的虚表

多继承设计的三个类

class Student
{
public:
	virtual void BuyTicket()
	{
		cout << "学生,买半价票" << endl;
	}
	virtual void StudentFun4()
	{
		cout << "StudentFun4()" << endl;
	}
protected:
	int _S;
};

class Teacher 
{
public:
	virtual void BuyTicket()
	{
		cout << " 教师, 三折" << endl;
	}

	virtual void TeacherFun4()
	{
		cout << "TeacherFun4()" << endl;
	}
};

class Grade : public Student, public Teacher
{
public:
	virtual void BuyTicket()
	{
		cout << "团体折扣" << endl;
	}

	virtual void GradeFun4()
	{
		cout << "GradeFun4()" << endl;
	}
};

C++面向对象之----多态_第16张图片
我们可以看到。多继承中,子类自己的虚函数在第一个继承的父类虚表的后边
C++面向对象之----多态_第17张图片

总结

·多态·就是一个事物的多个形态。当我们想要实现某个功能时,对于这个功能的不同对象都有着不同的状态

而对于inline函数和static函数,他是不能成为虚函数的

  • inline函数,由这个关键字声明的函数,在编译期间就会被编译器展开,所以说inline函数是没有地址的,也就不能被写到虚表中,因此不能构成多态
  • static函数,由static定义的函数,因为他是没有this指针的,所以使用类名::成员函数的方法是无法访问虚表的,因此也不能放在虚表中

至于为什么多态在调用的时候,必须是子类的指针或者引用类型,而不能是普通类型?
C++面向对象之----多态_第18张图片
我们构成多态,是利用了this指针中的虚表,但是当我们使用普通赋值的时候,却发现实际上是调用了父类的构造函数,重新构造了一个父类,并不是切片的过程。

所以说,如果使用普通的赋值,就会造成赋值过去的变量的虚表和原本的虚表不同
C++面向对象之----多态_第19张图片
所以说,使用指针类型或者引用类型,使得在强制类型转换的时候,使用切片的操作,就会完成多态

你可能感兴趣的:(C/C++,多态,虚表,C++)