您好,欢迎来到锐游网。
搜索
您的当前位置:首页【C++】多态之详细介绍虚函数指针和虚函数表

【C++】多态之详细介绍虚函数指针和虚函数表

来源:锐游网

一、面试题:

分析如下代码,选择正确答案:

二、虚函数表和虚函数表指针

1、本类虚函数表:

我们观察一个现象:

class Person
{
protected:
	int _a;
public:
	virtual void  fun1()
	{
		cout << "调用父类的fun" << endl;
	}

	virtual void  fun2()
	{
		cout << "虚表的第二个值" << endl;
	}

	 ~Person()
	{
		cout << "父类析构" << endl;
	}
};

int main()
{
	Person P;
	return 0;
}

问题:

我们知道一个类对象里面只会存储成员变量,不会存储成员函数,成员函数是放在一个公共区。那我们看上述Person里面只有一个成员变量 _a ,为什么监视窗口会多显示一个vfptr变量呐?

解答:

(1)、这个vfptr就是虚函数表指针(简称虚表指针),当类里面有虚函数时,实例化对象后就会自动多出这个变量,名字叫vfptr是“virtual function pointer”的缩写,这是一个函数指针数组

(3)、并且虚表里面的存储值的顺序就是虚函数从上往下声明的顺序。

2、子类的虚函数表及其虚表的相关规则

首先我们看如下代码:

class Person
{
protected:
	int _a = 0;
public:
	virtual void  fun1()
	{
		cout << "调用父类的fun" << endl;
	}

	virtual void  fun2()
	{
		cout << "虚表的第二个值" << endl;
	}
};

class Student :public Person
{
protected:
	int _s = 1;
public:
	virtual void fun1()
	{
		cout << "调用子类fun" << endl;
	}
};


int main()
{
	Person P;
	Student S;

	return 0;
}

我们Student类继承了Person类,并且重写了fun1函数,继承了fun2函数

我们会观察到以下几点现象:

(1)、子类对象S中也有一个虚表指针,S对象由两部分构成,一部分是自己的成员,一部分是父类继承下来的成员。

所以虚函数的重写也可以叫覆盖:覆盖就是指虚表中虚函数的覆盖

重写是语法的叫法,覆盖是原理层的叫法。
(3)、虚函数表本质是一个存虚函数指针的指针数组,一般情况(如vs编译器中)这个数组最后面放了一个nullptr,可以通过内存窗口查看
这样设置,我们可以用来打印虚函数表,作为循环判断条件。
(4)、注意若我们子类自己定义了虚函数,也会放进继承父类那部分的虚表里面,只是vs编译器的监视窗口看不见,但可以使用内存窗口看见。
(5)、通常虚表指针是设置在对象的前四个/八个字节,或者最后四个/八个字节,若放在前面,我们想要拿到这个虚表指针的话,就可以用int*指针,运用截断机制拿到虚表指针的值。
int main()
{
	Student S;
	Person P;
	Person* PP = &P;
	int* ptr = (int*)PP;
	printf("取到虚函数指针的值:%p", *ptr);
	return 0;
}

(6)、满足多态以后的函数调用是在运行起来以后到对象中去找的 不满足多态的函数调用时编译时确认好的(即普通函数调用,去符号表里面找)。
(7)、同一个类的不同对象,有不同的虚表指针,但指向的都是同一个虚表。
(8)、虚表是编译时生成的,对象里面的虚表指针是在构造函数的初始化列表最开始就赋值的。

3、虚表和虚表指针的相关问题:

(1)、虚函数存在哪的?虚表存在哪的?虚表指针存在哪?

1、虚函数和普通函数一样,因为函数都会编译成对应的指令,所以都存储在代码段区域

3、虚表指针存储在实例对象本身的内存空间中(对象的前四个字节或最后四个字节)。

三、动态绑定与静态绑定

(1)、静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如: 函数重载
(2)、动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为 动态多态 。(指向谁调用谁的函数)。

四、抽象类

1、概念:

(1)、在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做 抽象类 (也叫接口类),抽象类不能实例化出对象。
(2)、子类继承后若没有重写纯虚函数的话,子类也叫抽象类,也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

2、接口继承和实现继承

(1)、普通函数的继承是一种实现继承,子类继承了父类函数,可以使用函数,继承的是函数的实现。
(2)、虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口(函数声明与父类一样,这就是开头面试题缺省值的问题)。所以如果不实现多态,不要把函数定义成虚函数。

五、多继承的虚函数表:

首先看一段代码:

class A
{
public:
	virtual void fun1()
	{
		cout << "A的fun1" << endl;
	}

	virtual void fun2()
	{
		cout << "A的fun2" << endl;
	}
protected:
	int _a;
};

class B
{
protected:
	int _b;
public:
	virtual void fun1()
	{
		cout << "B的fun1" << endl;
	}

	virtual void fun2()
	{
		cout << "B的fun2" << endl;
	}
};

class C:public A,public B
{
protected:
	int _c;
public:
	virtual void fun1()
	{
		cout << "C的fun1" << endl;
	}
	virtual void fun3()
	{
		cout << "C的fun3" << endl;
	}
};

int main()
{

	return 0;
}

类A和类B都分别有两个虚函数和一个成员变量,类C多继承的类A和类B,然后重写了fun1函数,自己定义了fun3虚函数。

我们观察到如下现象:

(1)、计算类C对象的大小:

(2)、查看C类对象的虚表:

发现C类对象包含了从A继承的部分和从B继承的部分,并且每部分都有一个虚表。

(3)、切片问题:

首先我们要知道上述中:

1、ptr1ptr2是不相等的:因为兼容赋值只会切继承父类的那部分。

(4)、子类自身定义的虚函数:

子类自身定义的虚函数,要么放在继承列表的第一个类的虚表中,要么放在继承列表所有类的虚表,如vs2019是放在继承列表第一个类的虚表中。

六、关于多态章节超经典的面试问答题:

1、什么是多态?
答:参考上述内容。
2. 什么是重载、重写(覆盖)、重定义(隐藏)?
答:参考上一篇文章。
3. 多态的实现原理?
答:参考虚表的相关内容
4. inline函数可以是虚函数吗?
答:可以,这涉及内联函数的双属性,我们要知道普通内联函数是没有地址的,只在调用位置展开,但如果是多态调用,就会忽略内联属性,此时也会有地址,此时就是作为虚函数。这个函数就不再是 inline,虚函数要放到虚表中去。
5. 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。使用场景参考上一篇文章。
8. 对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
10. C++菱形继承的问题?虚继承的原理?
答:菱形继承的问题参考底层分析,虚基表。注意这里不要把虚函数表和虚基表搞混了。
11. 什么是抽象类?抽象类的作用?
答:参考上述内容。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- ryyc.cn 版权所有 湘ICP备2023022495号-3

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务