Bootstrap

C++中的多态

目录

前言

一.多态的概念

        1.1 概念

        1.2 多态的构成条件

        1.3 虚函数

        1.4 虚函数的重写

        1.4.1 协变

        1.4.2 析构函数的重写

        1.4.3 C++11里的override和final关键字

         1.5 抽象类

         1.6 接口继承和实现继承

 二.多态的原理

 2.1 虚函数表

2.2 派生类中的虚表指针

2.3 虚表保存在哪

 2.4 多态原理

 2.5 动态绑定和静态绑定

三.单继承和多继承的虚函数表

3.1 单继承中的虚表

3.2 多继承中的虚表


前言

        我认为多态是因为有了虚函数,通过继承,派生类可以重写虚函数,才有了C++的多态。所以要了解多态的原理,需要重点了解虚函数和虚基表。

一.多态的概念

        1.1 概念

        多态就是当要完成某个行为,当不同的对象去完成时会产生不同的效果。或者是说,不同的对象处理某一件事有不同的方法。

        比如:在火车站买票,普通成年人,需要全价买票,学生可以半价买票,军人可以优先买票。

        1.2 多态的构成条件

        多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。

        条件:

  • 被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写。
  • 必须通过基类的指针或者引用调用虚函数

第一点注意点是:调用的函数只能是虚函数,并且派生类必须对基类的虚函数进行重写。如果不重写,只是继承,实现方法一样。如果基类的不是虚函数,重写函数只是构成隐藏。

第二个注意点就是:必须通过基类的指针或者引用调用虚函数。

满足多态:跟对象的类型无关,跟指向的对象有关,指向哪个对象调用就是它的虚函数。

不满足多态:跟调用对象的类型有关,类型是什么就调用谁的虚函数。

如果不满足多态条件:

1.不重写虚函数

2.不是虚函数,重写

3.不用指针或者引用调用

        1.3 虚函数

        虚函数:被virtual修饰的成员函数称为虚函数。注意:修饰的是成员函数。

class Person
{
public:
	//虚函数
	virtual void BuyTicket()
	{
		cout << "全价买票" << endl;
	}
protected:

};

         1.4 虚函数的重写

        虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类的虚函数的返回值类型,函数名字,参数列表完全相同),但是函数的实现不同,称派生类重写了基类的虚函数。

class Person
{
public:
	//虚函数
	virtual void BuyTicket()
	{
		cout << "全价买票" << endl;
	}
protected:

};

class Student :public Person
{
public:
	//虚函数重写
	virtual void BuyTicket()
	{
		cout << "半价买票" << endl;
	}
    //注意:在在虚函数重写时,派生类的虚函数不加virtual关键字,也可以构成重写
    //因为继承,将基类的虚函数继承了下来,在派生类依旧保持虚函数的属性。
    //但是这种写法不规范,不建议这样写。
    //void BuyTicket()
	//{
		//cout << "半价买票" << endl;
	//}
protected:

};

 注意:

        虚函数的重写,需要函数名,参数,返回值类型一样。但是派生类只是继承了函数的接口,接口就是函数名,参数,返回值类型。派生类重写只是实现不同。

        1.4.1 协变

        派生类重写基类虚函数,与基类虚函数的返回值不同。并且基类虚函数的返回值基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。

 也可以是返回别的类的指针或者引用

class A{};
class B : public A {};

class Person {
public:
    //返回A的
    virtual A* f() {return new A;}
};
class Student : public Person {
public:
    //返回B的
    virtual B* f() {return new B;}
};

         1.4.2 析构函数的重写

问题:

 所以析构函数需要定义成虚函数,来构成多态:

这里是一个注意点,申请空间为派生类,但是赋值给基类时,需要将析构函数写成虚函数,构成多态。

         1.4.3 C++11里的override和final关键字

        1. final:修饰虚函数,表示该虚函数不能被继承,不能进行重写。

 修饰类,类不能被继承

 在C++98中,为了不让类被继承,可以将基类的构造函数私有化(private),于是派生类就不能构造属于基类的成员。

        2.override 检查派生类虚函数是否重写了基类的虚函数,如果没有编译错误。

这个只能修饰派生类的虚函数,不能修饰基类虚函数

 override关键字最好用来检查派生类虚函数接口(函数名,参数,返回值)是否写错

 1.5 抽象类

在虚函数后面写上=0,这个函数称为纯虚函数。

包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承之后也不能实例化出对象,只有重写的纯虚函数,派生类才能实例化出对象。

只有派生类经过纯虚函数重写才能实例化出对象。但是基类还是抽象类,不能实例化对象。

纯虚函数的作用:

        1.一定程度上强制了派生类对纯虚函数的重写,如果不重写,派生类就不能实例化对象。

        2.表示抽象的类型

应用场景:

        比如:比如,只说一辆车,车是抽象的,是一个很笼统的概念。因为有多车,不知道具体什么车,并且你不会拿车实例化对象,车这个类就可以定义成抽象类,里可以写成员函数,但是不写具体实现。

        但是某个品牌的车,比如奔驰,可以继承车这个抽象类,只需要重写纯虚函数,就可以实例化对象。

        具体的继承抽象的。

1.6 接口继承和实现继承

        普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。只是可以有隐藏。

        虚函数的继承是一种接口继承,派生类只是继承了基类虚函数的接口,目的是为了重写,达成多态。

        所以如果不实现多态,不要定义成虚函数。

 二.多态的原理

        2.1 虚函数表

 通过观察我们发现,对象b是8个字节,除了_num外,还有_vfptr指针。这个指针是我们叫做虚函数表指针,简称虚表指针。一个含有虚函数的类中至少有一个这样的指针。

_vfptr指针变量保存的是虚函数表的起始地址。

虚函数表实际是一个函数指针数组,虚函数表简称虚表。虚表里面保存的都是虚函数的地址。        

2.2 派生类中的虚表指针

 派生类不重写基类的虚函数。

 通过上面现象说明一个结论:

        派生类会继承基类的虚函数,会继承基类的虚表。但是派生类和基类的_vfptr变量内容不相等,说明两个虚表不是同一种虚表,只是虚表里的内容相同,所以会调用同一个函数。

派生类重写虚函数

 通过上面现象说明一个结论:

        派生类重写基类虚函数,会重写派生类虚函数表里的内容,将对应位置覆盖层重写虚函数的指针。

派生类增加虚函数

 注意:

        1.类中有虚函数只是这个类中多了虚函数表指针,不是将虚函数表保存到类中。

        2.虚函数表最后会以nullptr结尾。

        3.同类型的对象共用一张虚表,可以理解成一个类的虚表属于这个类的,实例化的对象,都公用这一张虚表。

 总结派生类虚表的生成:

1.派生类会继承基类的虚表,当然两个虚表表不是一张虚表。派生类先将基表虚表的内容拷贝一份到派生类的虚表中

2.如果虚表重写虚函数,用派生类重写虚函数的地址覆盖掉虚表中对应虚函数的地址。

3.派生类增加虚函数,会在派生类虚表中声明次序增加到虚表的最后。

2.3 虚表保存在哪

        首先说明,一个具有虚函数的类_vfptr保存在前面还是后面是有平台决定的,根据上面的现象,我们平台_vfptr是保存在最前面的。

 2.4 多态原理

        多态是基于虚函数的虚函数表。构成多态,跟对象有关。如果是基类对象,会去基类的虚表中找要调用虚函数的地址,去执行虚函数的代码。如果是派生类对象,会去派生类类的虚表中找要调用虚函数的地址,去执行虚函数的代码。

        派生类虚函数重写之后,可以实现不同的对象,有不同的实现方法,展现不同的效果。

 再来段代码理解一下:

通过汇编分析,看出满足多态以后函数调用,不是在编译时确定的,是在运行起来后到对象的需表中找的。

不满足多态,是在编译时确定好的。

         2.5 动态绑定和静态绑定

  • 静态绑定又称为前期绑定,在程序编译期间确定了程序的行为,也称静态多态。比如函数重载。在编译的时候确定了调用的函数。
  • 动态绑定也称后期绑定,是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称动态多态。就像上面的,运行时在到虚表中找调用函数的地址。
  • 多态,多数都是动态绑定。

三.单继承和多继承的虚函数表

3.1 单继承中的虚表

 结论是否如上图所述:我们来打印一下虚表

打印代码

class Person 
{
public:
	virtual void func1(){ cout << "Person func1()" << endl; }
	virtual void func2(){ cout << "Person func2()" << endl; }

};

class Student :public Person
{
public:
	virtual void func1(){ cout << "Student func1()" << endl; }
	virtual void func3(){ cout << "Student func3()" << endl; }
protected:

};


typedef void(*VFPTR)();//声明一个函数指针即 typedef void (*)() VFPTR;
//打印代码
void PrintVfTable(VFPTR *vftable){
	for (int i = 0; vftable[i] != nullptr; i++){
		//打印虚表内容
		printf("vftable[%d]:%p\n", i, vftable[i]);
		//调用这个函数
		VFPTR fun = vftable[i];
		fun();
	}
	cout << endl;
}

int main()
{
	Person p;
	Student s;
	//要得到虚表指针的内容,由于这个平台,虚表指针是保存在开始的
	//先得到对象地址,强转成int *得到前四个字节,就是虚表指针的地址
	//再解引用,得到虚表指针的内容
	//再强转成函数二级指针
	PrintVfTable((VFPTR *)*(int *)&p);
	PrintVfTable((VFPTR *)*(int *)&s);

	getchar();
	return 0;
}

 结果和我们想的一样:

 3.2 多继承中的虚表

 根据上面的结论可以得到这样一张图:

#include<iostream>

using namespace std;

class Base1
{
public:
	virtual void func1(){
		cout << "Base1 : func1()" << endl;
	}
	virtual void func2(){
		cout << "Base1 : func2()" << endl;
	}
protected:
	int _a;
};

class Base2
{
public:
	virtual void func1(){
		cout << "Base2 : func1()" << endl;
	}
	virtual void func2(){
		cout << "Base2 : func2()" << endl;
	}
	virtual void func3(){
		cout << "Base2 : func3()" << endl;
	}
protected:
	int _b;
};
//多继承
class Deirve :public Base1, public Base2
{
public:
	virtual void func1(){
		cout << "Deirve : func1()" << endl;
	}
	virtual void func4(){
		cout << "Deirve : func4()" << endl;
	}
	virtual void func5(){
		cout << "Deirve : func5()" << endl;
	}
protected:
	int _c;
};


typedef void(*VFPTR)();//声明一个函数指针即 typedef void (*)() VFPTR;

void PrintVfTable(VFPTR *vftable){
	printf("虚表地址:%p\n", vftable);
	for (int i = 0; vftable[i] != nullptr; i++){
		//打印虚表内容
		printf("vftable[%d]:%p\n", i, vftable[i]);
		//调用这个函数
		VFPTR fun = vftable[i];
		fun();
	}
	cout << endl;
}

int main()
{
	Deirve d;
	//打印继承Base1的虚表
	PrintVfTable((VFPTR *)*(int *)&d);

	//打印继承Base2的虚表
	//加Base1大小的字节数,到Base2的虚表指针。
	//要加先强转成char *,步长为一个字节。不强转的话,步长为Deirve
	PrintVfTable((VFPTR *)*(int *)((char *)&d + sizeof(Base1)));
	getchar();
	return 0;
}

 画图表示为:

;