C++(刨根问底)_虚函数_多态_抽象类_多态的原理

文章目录

    • 1.C++多态的类型(静态/动态)
    • 2.虚函数与多态
    • 虚函数的定义
    • 虚函数的重写,通过基类的指针或引用实现多态
    • 多态的条件
    • ①虚函数重写的例外:协变
    • ②虚函数重写的特例:析构函数的重写
    • ③虚函数重写易错点(不会重写默认值,只会重写方法)
    • ④基类成员函数声明虚函数,派生类重写这个函数时可以不加virtual
    • 3.C++11 final override关键字
    • 4.抽象类(更好的表示抽象的数据成员)
    • 纯虚函数(在虚函数的后面写上 =0)
    • 5.多态的原理
    • 虚函数表(虚函数表指针_vfptr)
    • 为什么对象不能实现多态(切片)
    • 确定虚函数表在进程地址空间中储存位置
    • 打印虚函数表
    • 多继承下的虚函数表
    • 虚继承(菱形继承)下的虚函数表

1.C++多态的类型(静态/动态)

1.静态的多态
静态多态:也称为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
静态多态有两种实现方式:

①函数重载:
包括普通函数的重载和成员函数的重载

注意:
函数重载需要在同一作用域中。
两个在同一作用域的函数返回值不同不构成函数重载

②函数模板的使用
2.动态的多态
动态多态(动态绑定):即运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。(父类指针或引用调用重写虚函数)

2.虚函数与多态

虚函数的定义

虚函数:即被virtual修饰的类非静态成员函数称为虚函数
注意:①虚函数是类的成员函数,一般函数无法声明为虚函数。
②这里的virtual与虚继承的virtual没有任何关系,注意不要搞混了

虚继承的virtual是为了解决C++菱形继承产生的数据冗余和二义性。
虚函数是为了实现多态

③只有类的非静态成员才可以为虚函数

④构造函数无法定义为虚函数
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第1张图片

虚函数的重写,通过基类的指针或引用实现多态

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

动态多态:
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第2张图片
实现多态:
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第3张图片
基类指针指向基类和指向派生类所执行的函数不同

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第4张图片
基类引用分别引用基类和派生类执行的函数不同
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第5张图片
如果不是用指针和引用而是通过对象是无法实现多态的

多态的条件

由上图可知:
① 必须通过基类的指针或者引用调用虚函数
②. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

①虚函数重写的例外:协变

我们要求虚函数的重写条件为:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

但存在例外(协变):
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时称为协变
这种情况就导致了虚函数的返回值不同,但仍然构成虚函数的重写
eg:
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第6张图片
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第7张图片
由上图,构成动态多态

注意:上图如果Fruits和Apple不是继承关系,不能构成协变。不满足虚函数重写的条件,会报错

②虚函数重写的特例:析构函数的重写

再回到定义上,派生类虚函数的重写要求与基类虚函数的返回值类型、函数名字、参数列表完全相同,
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。

首先:
C++为了保证派生类与基类的析构顺序为先析构派生类再析构基类,在派生类的析构函数结束时会自动调用基类的析构函数。

其次:
如果我们动态开辟了派生类对象。而选择用基类指针调用析构函数,如果派生类与基类的析构函数没有重写,编译器会调用基类的析构函数。这就导致了对于动态开辟的派生类对象释放时仅仅释放了继承来的基类数据,造成内存泄漏。

根据上面的分析:在释放动态开辟的空间时,当基类指针指向基类的时候调用基类的析构函数,当基类指针指向派生类时调用派生类的析构函数(此时编译器在调用完派生类的析构函数时还会调用基类的析构函数,这样就达到了完全释放的目的)

eg:
析构函数不是虚函数时:
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第8张图片
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第9张图片
析构函数是虚函数时
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第10张图片
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第11张图片
这种情况子类析构函数和父类的析构函数编译器会处理成同名函数(在继承中编译器同样也会做这样的处理)
在VS编译器上编译器为了使析构函数构成重写,将基类与派生类的析构函数统一命名为destructor

③虚函数重写易错点(不会重写默认值,只会重写方法)

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第12张图片
当基类ptr指针访问派生类函数时构成多态,b中的Test函数重写,但重写不会重写默认值value,所以value的值还是父类的1

运行结果:
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第13张图片

④基类成员函数声明虚函数,派生类重写这个函数时可以不加virtual

3.C++11 final override关键字

final:
禁止这个虚函数重写,如果重写这个虚函数会报错

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第14张图片
override
检查虚派生类是否完成了对基类虚函数的重写,如果不重写会报错

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第15张图片

4.抽象类(更好的表示抽象的数据成员)

纯虚函数(在虚函数的后面写上 =0)

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第16张图片
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数使派生类必须重写,另外纯虚函数更体现出了接口继承

注意:
1.虽然抽象类不能实例化对象,但还是可以创建对应的指针来实现多态
2.普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数或纯虚函数。

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第17张图片
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第18张图片

5.多态的原理

虚函数表(虚函数表指针_vfptr)

当类中存在虚函数时,在生成对象时会在对象中添加虚表指针,这个指针指向对象的虚函数,以此来实现多态。
虚表指针指向一个数组,这个数组的每个元素为函数指针

虚函数表是在编译阶段生成的,构造函数完成对虚函数表的初始化

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第19张图片

注意:
这里要与虚继承相区分,虚继承也会在对象里生成个指针,但虚继承中生成的指针指向的是派生类距离基类同名数据的偏移量,为了解决二义性。叫做虚基表。二者没有关系

在指针或引用调用虚函数时,不是编译时确定的,而是在运行时通过对象的虚函数指针来找要执行的函数。所以指向的是父类对象调用的就是父类的虚函数,指向子类调用子类的虚函数。

为什么对象不能实现多态(切片)

当基类指针指向派生类时,基类指针只会指向派生类中基类的那一部分(切片),这时后通过指针就可以找到派生类的虚表

#include

using namespace std;

class A
{
     
public:
	virtual void Test()
	{
     
		cout << "A " << endl;
	}
private:
	int Date = 10;
};

class B :public A
{
     
public:
	virtual void Test()
	{
     
		cout << "B " << endl;
	}
private:
	int BDate = 12;
};

void fun(A* p)
{
     
	p->Test();
}

void fun(A p)
{
     
	p.Test();
}

int main()
{
     
	A a;
	B b;
	fun(&b);
}

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第20张图片
如果将派生类赋值给基类,此时是将派生类中基类的部分通过调用拷贝构造函数赋值给基类对象(切片),但此时因为这个对象最后是基类对象,所以这个虚函数表还是基类的虚函数表,并不是派生类虚函数表构不成多态。
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第21张图片
调试信息

基类指针和引用派生类,虚函数表可以找到派生类
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第22张图片
派生类对象赋值给基类对象仍然是基类的虚函数表

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第23张图片

确定虚函数表在进程地址空间中储存位置

在32位下指针大小为4字节。我们要打印的不是对象的地址,所以&对象不可取
因为对象的开始四个字节保存的是虚表指针,虚表指针大小为4个字节,所以我们将对象地址强制转化为(int*)在解引用就访问到对象的前4个字节,就可以打印虚表地址了

#include

using namespace std;

class A
{
     
public:
	virtual void Test()
	{
     
		cout << "A " << endl;
	}
private:
	int Date = 10;
};

class B :public A
{
     
public:
	virtual void Test()
	{
     
		cout << "B " << endl;
	}
private:
	int BDate = 12;
};
int j = 0;//数据段
int main()
{
     
	int a = 0;//栈区
	int* n = new int(0);//堆区
	const char* str = "Hello Word";//代码段
	B b;
	B* ptr = &b;//对象的前4个字节为虚表地址
	printf("虚函数表地址%p\n",ptr);
	printf("数据段地址%p\n",&j);
	printf("栈区地址%p\n",&a);
	printf("堆区地址%p\n",n);
	printf("代码段地址%p\n", str);
	return 0;
}

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第24张图片

发现对象的虚表地址最接近代码段,所以对象的虚表保存在代码段上

打印虚函数表

在VS编译器下虚函数表最后以NULL来标识结束

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第25张图片

注意:如果检查发现虚函数表末不是用NULL重新生成解决方案

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第26张图片
分析上面的代码,Teacher类重写了基类的PrintAge函数和fun1()函数,基类的fun2函数并没有重写,并且Teacher类新添加了fun3的虚函数。这里将通过函数指针的方式来访问虚函数表里的函数。

#include

using namespace std;

class People
{
     
public:
	People(int _age=18)
		:age(_age)
	{
     }
	 virtual void PrintAge() {
     
		cout << "Fruits Peole Age:" << endl;
	}
	virtual void fun1 ()
	{
     
		cout << "People fun1()" << endl;
	}
	virtual void fun2()
	{
     
		cout << "People fin2()" << endl;
	}
private:
	int age;
};

class Teacher :public People
{
     
public:
	Teacher()
		:People(35)
	{
     }
	virtual void PrintAge()
	{
     
		cout << "Apple Teacher Age:" << endl;
	}
	virtual void fun1()
	{
     
		cout << "Teacher fun1()" << endl;
	}
	virtual void fun3()
	{
     
		cout << "Teacher fun3()" << endl;
	}
};

typedef void(*VFPtr)();//虚函数表中的函数指针类型

void PrintVFPtr(VFPtr* ptr)//存函数指针的数组传参时退化为指针
{
     
	for (int i = 0; ptr[i] != nullptr; i++)
	{
     
		printf("_vfptr[%d]:%p\n", i, ptr[i]);//打印虚函数地址
		ptr[i]();//调用这个地址上的函数
	}
}

int main()
{
     
	Teacher T;
	PrintVFPtr((VFPtr*)(*(int*)&T));//*(int*)&T访问的是虚函数数组,再强制转化为参数类型传参。
	return 0;
}

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第27张图片

注意:因为这时虚函数的函数类型相同,都是不带参数的函数,所以可以这样调用。如果虚函数参数不同可以打印地址,但不能通过地址调用函数了。因为无参虚函数和有参数虚函数指针大小相同,所以可以打印地址

多继承下的虚函数表

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第28张图片

上面代码中Professor继承了两个类,其中重写了fun1()函数,没有重写fun2()函数,添加了新的虚函数fun3(),多继承下派生类有两个虚表,且这两个虚表挨着存放

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第29张图片

其中因为派生类先继承People类,先存放People类的虚函数表,所以这两个虚表地址之间相差了People类的大小个字节,所以我们可以用代码打印虚函数来观察多继承下的虚表

#include

using namespace std;

class People
{
     
public:
	virtual void fun1 ()
	{
     
		cout << "People fun1()" << endl;
	}
	virtual void fun2()
	{
     
		cout << "People fin2()" << endl;
	}
};

class Teacher
{
     
public:
	virtual void fun1()
	{
     
		cout << "Teacher fun1()" << endl;
	}
	virtual void fun2()
	{
     
		cout << "Teacher fun2()" << endl;
	}
};

class Professor :public People, Teacher
{
     
	virtual void fun1()
	{
     
		cout << "Professor fun1()" << endl;
	}
	virtual void fun3()
	{
     
		cout << "Professor fun3()" << endl;
	}
};

typedef void(*VFPtr)();//虚函数表中的函数指针类型

void PrintVFPtr(VFPtr* ptr)//存函数指针的数组传参时退化为指针
{
     
	for (int i = 0; ptr[i] != nullptr; i++)
	{
     
		printf("_vfptr[%d]:%p\n", i, ptr[i]);//打印虚函数地址
		ptr[i]();//调用这个地址上的函数
	}
}

int main()
{
     
	Professor P;
	PrintVFPtr((VFPtr*)(*(int*)&P));//打印第一张虚表
	//因为两张虚表之间相差People个字节,而char*类指针+Z就是跳过Z个字节,所以这里将&P强制转化为字符类指针
	PrintVFPtr((VFPtr*)(*(int*)((char*)&P + sizeof(People))));//打印第二张虚表
	return 0;
}

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第30张图片

根据打印结果可以看出,多继承下的重写,两个基类的同名虚函数都会重写,新添加的虚函数保存在第一个虚表中

虚继承(菱形继承)下的虚函数表

为了解决菱形继承引入了虚继承。但虚继承和虚函数虽然关键字相同,但实际没有关系。
C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第31张图片

B类虚继承了A重写了Test函数自己有虚函数fun
C类虚继承了A重写了Test函数自己有虚函数fun
D类继承了B和C类,重写了函数Test

其内存规则如下:

C++(刨根问底)_虚函数_多态_抽象类_多态的原理_第32张图片

你可能感兴趣的:(C++,c++,开发语言,后端)