首页 > 编程语言 >【C++】多态之详细介绍虚函数指针和虚函数表

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

时间:2024-11-21 20:45:06浏览次数:3  
标签:虚表 函数 继承 多态 C++ 函数指针 重写 指针

一、面试题:

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

答案选:【B】

首先我们看到B继承了A,B的func函数重写了A的func函数,main函数里面,B对象p调用test函数,而test函数时继承A类的,所以test函数的形参this指针是A类的this指针,所以test函数里面调用func是A类this指针调用的func,所以满足父类指针调用的条件,又因为func函数又构成重写关系,所以这里是构成多态关系的,又因为test形参的A类指针是由B类对象p调用传参进行赋值的,所以该A类this指向的是B类this指针,所以根据多态的原理,test函数里面会调用B类的fun函数,又因为func函数时重写关系,重写是实现重写(即只会重写实现部分吧),函数声明部分是直接照搬父类的,所以val的缺省值还是父类的1,所以打印“B->1”,所以选B。

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

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”的缩写,这是一个函数指针数组

(2)、vfptr的值就是虚函数表(简称虚表)在内存中的存储地址,类里面有几个虚函数,该表里面就有几个值,如上述代码中只有一个虚函数,所以虚表里面只有一个值,就是该虚函数的地址。若我再增加一个虚函数,则虚表就会有两个值,如下:

(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对象由两部分构成,一部分是自己的成员,一部分是父类继承下来的成员。

(2)、我们可以发现,子类的虚表指针是继承父类那部分的虚表指针(但要注意:子类虚表指针不是父类的虚表指针,看值也可以知道)。如果没有重写父类的虚函数,那么虚表中对应的函数地址也是原来的地址,如果重写了父类的虚函数,那么就会把父类虚表中的对应的函数地址覆盖掉。

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

重写是语法的叫法,覆盖是原理层的叫法。 (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、虚函数和普通函数一样,因为函数都会编译成对应的指令,所以都存储在代码段区域;

2、vs2019中,虚表存储在常量区,可以通过比较地址的相似度进行判断;

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、ptr1和ptr2是不相等的:因为兼容赋值只会切继承父类的那部分。

2、但ptr1和&c相等,但含义不同,因为ptr1是类C继承列表的第一个类,所以起始地址相同。

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

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

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

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

标签:虚表,函数,继承,多态,C++,函数指针,重写,指针
From: https://blog.csdn.net/hffh123/article/details/143904570

相关文章

  • 阶乘之和 C++实现代码
    #include<bits/stdc++.h>usingnamespacestd;intmain(){ //求和的变量设置为0,阶乘的变量设置为1 longlongintsum1=0,temp_sum=1; intn; cin>>n; for(inti=1;i<=n;i++){ //每次开始不同数字的阶乘需要将值进行重置 temp_sum=1;......
  • C++:AVL树-模拟实现完整代码
    文章目录AVL树-模拟实现完整代码总结:查找错误的方式总结AVL树-模拟实现完整代码总结:#pragmaonce#include<iostream>usingnamespacestd;#include<assert.h>template<classK,classV>structAVLTreeNode{ pair<K,V>_kv;//数据的存储 AVLTreeNod......
  • C++:探索AVL树旋转的奥秘
    文章目录前言AVL树为什么要旋转?一、插入一个值的大概过程1.插入一个值的大致过程2.平衡因子更新原则3.旋转处理的目的二、左单旋1.左单旋旋转方式总处理图2.左单旋具体会遇到的情况3.左单旋代码总结三、右单旋1.右单旋旋转方式总处理图2.右单旋具体会遇到的......
  • C/C++中的const
    1.在C语言中 在C语言中,const 是一个关键字,用于修饰变量。它的主要作用是定义常量,即被 const 修饰的变量的值不能被修改。例如: constinta=10;//这里定义了一个整型常量 a ,尝试给 a 重新赋值(如 a=20; )会导致编译错误。const 也可以用于修饰指针。如果......
  • C++中的移动语义
    一、移动语义1.定义:在C++中,移动语义是一种优化技术。移动语义允许资源的“移动”而不是“拷贝”。在传统的C++中,当一个对象被赋值或传递给函数时,通常会发生拷贝操作,这会导致性能下降,尤其是在处理大型对象时。移动语义通过引入右值引用和移动构造函数、移动赋值运算符,允许......
  • 【C++学习笔记】一个先学了Java,Python,Csharp最后再来学C++的菜狗笔记
    1.字符串1.char数组charstr[]="helloworld";可以使用cstring库中的函数(如strlen,strcpy)。2.string类型#include<string>stringstr="helloworld";与csharp,java等语言不同的是动态分配内存,由标准库管理。支持操作符重载(如+,==等)。std::string是可变的,类似......
  • C++系统教程007-数据类型06(cin输入语句)
    练习:1.控制输出精确度本实例中,定义一个整型变量并赋值,定义一个双精度变量并赋值,利用cout输出这两个不同精度的格式。//控制精度#include<iostream>usingnamespacestd;intmain(){ intx=123; doubley=3.1415; cout<<"x="; cout.width(10);//设置输出域宽为10 ......
  • C++11-chrono时间库解析
    目录一、具体作用用途二、C++std::chrono时间库概述2.1、std::chrono命名空间的作用和用途2.2、基本组成部分:duration、time_point和clock三、duration的使用详解3.1、duration表示时间段的概念和使用方法3.2、duration的各种单位和精度选项3.3、使用示例四、time_p......
  • 【C++】类和对象-深度剖析默认成员函数-下
     >......
  • C++指针函数体内部初始化需要注意的地方
    有如下代码:voidchangePtr(int*p){*p=4;}intmain(){int*p=newint(5); changePtr(p);cout<<"*p:"<<*p<<endl;}以上代码我们都知道传递指针,函数改变了指针指向地址内的数据,函数体外部调用时p指向地址发生了改变,输出结果由5->4。但是在......