C++虚函数与多态记录(面试级整理)

一、原理

1.1 什么是多态?

  • 多态就是不同对象对同一行为会有不同的状态。(举例 : 学生和成人都去买票时,学生会打折,成人不会)
  • 实现多态有两个条件:
    1) 一是虚函数重写,重写就是用来设置不同状态的
    2)二是对象调用虚函数时必须是指针或者引用

ps:没有这两个条件无法构成多态,很多笔试题都会利用这个陷阱让你上当!

实际上,代码上体现(动态)多态就是当父类指针指向子类对象,然后通过父类指针能调用子类的成员函数。

1.2 什么是虚函数?什么是重写?

虚函数是带有 virtual 关键字的成员函数

子类有个和父类完全相同(函数名,形参,返回值都相同,协变和析构函数除外)的虚函数,就称子类虚函数重写父类虚函数

总结:

在父类的函数前加上virtual关键字,在子类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数,如果对象类型是子类,就调用子类的函数,如果对象类型是父类,就调用父类的函数。

  • 虚函数机制(virtual function) , 用以支持执行期绑定,实现多态。
  • 虚基类 (virtual base class) ,虚继承关系产生虚基类,用于在多重继承下保证基类在子类中拥有唯一实例。

1.3 多态的原理?

多态是用虚函数表实现的。

有虚函数的类都会生成一个虚函数表,这个表在编译时生成。

虚函数表是一个存储虚函数地址的数组,以 NULL 结尾。

如果要生成子类虚表,就要经过三个步骤:

  • 第一步,将父类虚表内容拷贝到子类虚表上;
  • 第二步,将子类重写的虚函数覆盖掉表中父类的虚函数;
  • 第三步,如果子类有新增加的虚函数,按声明次序加到最后

1.4 多态如何调用?

  • 满足多态的函数调用,程序运行起来后,根据对象中的虚表指针来找实际应该调用的函数;
  • 不满足多态的函数在函数编译时就确定函数地址了。

1.5 动态绑定与静态绑定?

  • 静态绑定是程序编译时确定程序行为。
  • 动态绑定是程序运行时根据具体的对象确定程序行为

1.6 继承中的多态

  1. 继承无虚函数覆盖:
    虚函数按声明顺序存放,父类虚函数在子类虚函数前面.
    C++虚函数与多态记录(面试级整理)_第1张图片

  2. 继承有虚函数覆盖:
    覆盖的 f ( ) f() f() 替代原有父类虚函数位置, 没覆盖的不变
    C++虚函数与多态记录(面试级整理)_第2张图片

  3. 继承无虚函数覆盖:
    每个父类都有自己的虚表,子类成员函数被放入第一个父类表中
    C++虚函数与多态记录(面试级整理)_第3张图片

  4. 继承有虚函数覆盖:
    三个父类虚表中的 f ( ) f() f() 都会被子类函数指针覆盖
    C++虚函数与多态记录(面试级整理)_第4张图片

  5. 多继承规则:
    多继承子类未重写的虚函数放在第一个继承父类部分的虚函数表中,继承的虚表都会覆盖

  6. 重复继承: B类数据重复,具有二义性.
    C++虚函数与多态记录(面试级整理)_第5张图片

二、面试常考

  1. inline函数可以是虚函数吗?
    答案:不能,因为inline函数没有地址,无法放到虚函数表中

  2. 静态成员可以是虚函数吗?
    答案:不能, 因为静态成员函数没有 this 指针, 因为有 this 指针才能访问到虚表指针,有虚表指针才能找到虚表从而调用实际应该调用的函数。

  3. 构造函数可以是虚函数吗? 虚函数指针在什么时候生成的的?
    答案:不能,因为对象中的虚表指针是在构造函数初始化列表阶段才初始化的

  4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
    答案:可以, 并且最好把基类的析构函数定义成虚函数。当父类指针指向子类对象时,如果析构函数不是虚函数,析构就只会释放父类对象,造成内存泄漏。(因为析构重名,只能调用一个,调用默认的父类析构函数)。 定义成虚函数后,调用析构时就会取出虚表指针找到实际应该调用的函数。(指针虽然都是父类类型,但是指针内取出的虚表是不一样的,所以析构能调用子类析构)

  5. 对象访问普通函数快还是虚函数更快?
    答案:首先如果是普通对象,是一样快的,如果是指针对象或者是引用对象,则调用的普通函数快,因为普通对象在编译时就确定地址了,虚函数构成多态,运行时调用虚函数需要到虚函数表中去查找

参考:

  1. C++多态详解及面试题
  2. C++ 多态与虚函数

三、代码

// 虚函数和多态
/*
 基类成员函数用virtual修饰
 1. 基类大小多出来4个字节
	指针 _vfptr
	_vfptr 指向一个  函数指针数组
	数组中保存所有虚函数的地址
2. 派生类继承基类, 会继承基类的函数指针数组里的元素
3. 如果派生类有重写,即在派生类里面有与基类重名的函数, 那么重写后的函数地址会覆盖函数指针
数组里的元素
4. 调用函数时, 会去虚函数表中找函数
*/

#include "pch.h"

#include 

using namespace std;

class A
{
	int n;
public:
	virtual void show() { cout << "A show" << endl; }
	virtual void show2() { cout << "A show2" << endl; }

};
class B :public A
{
public:
	void show() { cout << "B show" << endl; }
};

// 给没有返回值,没有参数的函数指针取个别名,叫做pFUNC
// 那么pFUNC 就是指向这样的函数的指针类型
typedef void(*pFUNC)();



class C :public A
{
public:
	void show() { cout << "C show" << endl; }
};
int main()
{
	int num = 100;
	int *po = &num;
	cout << "Hello============== World!\n";
	A a;
	B b;
	C c;

	/*
	A* p = NULL;
	p = &b;
	p->show();

	p = &c;
	p->show();

	cout << "====================================" << endl;
	*/
	// pvfptr 指向 B类的对象b, 获得虚函数表地址 pvfptr
	int* pvfptr;
	pvfptr = (int*)&b;                         // 强行把&b(B*)转成(int*)类型 ,取得虚函数表的地址
	cout << "&b:     " << &b << endl;        // 
	cout << "pvfptr: " << pvfptr << endl;    // 
	// 获得对象b中的虚函数表的地址,就是虚函数表这个数组的首地址
	int* _vfptr;
	_vfptr = (int*)*pvfptr;                   // 然后,再次取值就可以得到第一个虚函数的地址了

	cout << "*pvfptr: " << *pvfptr << endl;
	cout << "(int*)*pvfptr: " << (int*)*pvfptr << endl;

	//int** _vfptr;
	//_vfptr = &pvfptr;




	cout << "_vfptr: " << _vfptr << endl;           // 获得虚函数表首地址
	int* pfunc1 = (int*)*_vfptr;
	cout << "pfunc1: " << pfunc1 << endl;          // 获得虚函数表第一个函数地址
	int* pfunc2 = (int*)*(_vfptr + 1);
	cout << "pfunc2: " << pfunc2 << endl;          // 获得虚函数表第二个函数地址

	

	pFUNC pf1 = (pFUNC)pfunc1;
	pf1();
	pFUNC pf2 = (pFUNC)pfunc2;
	pf2();

	pFUNC pf3 = (pFUNC)_vfptr;
	pf3();
	pFUNC pf4 = (pFUNC)_vfptr;
	pf4();

	return 0;

}

/*

_vfptr: 0046F084
pfunc1: 00401096
pfunc2: 00401285
B show
A show2

*/



VS 内存地址

C++虚函数与多态记录(面试级整理)_第6张图片

代码对应的图示

C++虚函数与多态记录(面试级整理)_第7张图片

你可能感兴趣的:(C++,算法,数据结构,多态,指针,面试,c++)