C++57个入门知识点_34_虚函数的模拟实现-理解(利用函数指针替代virtual的虚函数功能;虚函数的本质即为函数的覆盖,子类一旦对父类同名成员函数重载,对象在调用时使用的是子类的函数)

上篇C++57个入门知识点_33_深入理解虚函数的原理-重点(间接调用:先查虚表地址,再查虚表中的虚函数指针;编译器先取对象的前4个字节地址,再取对应地址下函数指针;查看内存、反汇编的方法;成员函数指针)介绍了虚函数的原理,本篇将会介绍虚函数的c语言的模拟实现,对虚函数有一个更深的理解。

虚函数本质上是函数,虚表本质上是函数指针的数组,此处完全将virtual关键字去掉,看是否可以得到与上篇一样的效果。

1. 虚函数调用对象虚表中函数的步骤总结

(1)找到对象头部四个字节–虚表地址
(2)根据虚函数的位置(在cpp中定义的顺序),确定该虚函数在虚表的下标
(3)找到对应的虚函数地址,直接调用该地址
在这里插入图片描述

用下图也可以看出
在这里插入图片描述

2. 整体代码

2.1 整体结构如下图

在这里插入图片描述

2.2 Chinese.h/cpp

  • Chinese.h
#pragma once
#include <iostream>
#include "Person.h"
class CChinese :public CPerson {
public:
	CChinese() {
	    //将每个类对应的虚表地址赋给虚表指针
		m_pVirTable = m_ChsTable;
	}
	~CChinese() {

	}
	 void speak() {
		printf("Speak Chinese!");
	}

	 //每一个类有自己的虚表,需要重新定义
	 static PEN_VIRTUAL m_ChsTable[2];
};
  • Chinese.cpp
#include "Chinese.h"
//函数覆盖
PEN_VIRTUAL CChinese::m_ChsTable[2] = { (PEN_VIRTUAL)&CChinese::speak, (PEN_VIRTUAL)&CPerson::eat };

2.3 English.h/cpp

  • English.h
#pragma once
#include <iostream>
#include "Person.h"
class CEnglish :public CPerson {
public:
	CEnglish() {
	    //将每个类对应的虚表地址赋给虚表指针
		m_pVirTable = m_EngTable;
	}
	~CEnglish() {

	}

	void speak() {
		printf("Speak English");
	}
	void eat() {
		printf("Eat English");
	}

	//每一个类有自己的虚表,需要重新定义
	static PEN_VIRTUAL m_EngTable[2];
};
  • English.cpp
#include "English.h"
//函数覆盖
PEN_VIRTUAL CEnglish::m_EngTable[2] = { (PEN_VIRTUAL)&CEnglish::speak, (PEN_VIRTUAL)&CEnglish::eat };

2.4 Person.h/cpp

  • Person.h
#pragma once
#include <iostream>

class CPerson;//向前声明

//定义一个函数指针
typedef void(CPerson::*PEN_VIRTUAL)();

class CPerson
{
public:
	CPerson() {
	   //将每个类对应的虚表地址赋给虚表指针
		m_pVirTable = m_Table;
	}
	~CPerson() {

	}

	 void speak() {
		printf("Just Speak!");
	}

	 void eat() {
		 printf("Just Eat!");
	 }

	 //模拟虚表指针(数组的指针)
	 PEN_VIRTUAL* m_pVirTable;//PEN_VIRTUAL代表函数指针

	 //静态成员来表示真正的虚表,用来表示同一个类的虚表是一样的
	 static PEN_VIRTUAL m_Table[2];
};
  • Person.cpp
#include "Person.h"
//静态成员的初始化
PEN_VIRTUAL CPerson::m_Table[2] = {&CPerson::speak,&CPerson::eat};

2.5 testCPP.cpp


#include <iostream>
#include "Person.h"
#include "English.h"
#include "Chinese.h"


int main(int argc,char* argv[])
{
	CPerson* pPer[2];
	CPerson per;

	CChinese chs;
	int nChsLength = sizeof(chs);

	CEnglish eng;
	int nEngLength = sizeof(chs);

	//把子类指针转给父类
	pPer[0] = &chs;
	pPer[1] = &eng;

	for (int i = 0; i < 2; i++)
	{
		//(pPer[i]->*pPer[i]->m_pVirTable[0])();//获取虚表内第0项指针,等效为下面几步
		//等价于:
		//(1)找到对象头部四个字节--虚表
		PEN_VIRTUAL* pVirTable = pPer[i]->m_pVirTable;
		//(2)根据虚函数的位置(在cpp中定义的顺序),确定该虚函数在虚表的下标
		PEN_VIRTUAL pfn = pVirTable[1];
		//(3)找到对应的虚函数地址,直接调用该地址
		//pPer[i]->speak();//间接调用
		(pPer[i]->*pfn)();

		printf("\r\n");
	}

	return 0;
}

上述代码运行结果:实现了虚函数功能
在这里插入图片描述

3. 子类中的函数重载

  • Person.cpp
#include "Person.h"
//静态成员的初始化
PEN_VIRTUAL CPerson::m_Table[2] = {&CPerson::speak,&CPerson::eat};
  • Chinese.cpp
#include "Chinese.h"
//函数覆盖
PEN_VIRTUAL CChinese::m_ChsTable[2] = { (PEN_VIRTUAL)&CChinese::speak, (PEN_VIRTUAL)&CPerson::eat };
  • English.cpp
#include "English.h"
//函数覆盖
PEN_VIRTUAL CEnglish::m_EngTable[2] = { (PEN_VIRTUAL)&CEnglish::speak, (PEN_VIRTUAL)&CEnglish::eat };

(1)当main函数中:类对象调用虚表中的第1个函数

for (int i = 0; i < 2; i++)
	{
		//(pPer[i]->*pPer[i]->m_pVirTable[0])();//获取虚表内第0项指针,等效为下面几步
		//等价于:
		//(1)找到对象头部四个字节--虚表
		PEN_VIRTUAL* pVirTable = pPer[i]->m_pVirTable;
		//(2)根据虚函数的位置(在cpp中定义的顺序),确定该虚函数在虚表的下标
		PEN_VIRTUAL pfn = pVirTable[0];
		//(3)找到对应的虚函数地址,直接调用该地址
		//pPer[i]->speak();//间接调用
		(pPer[i]->*pfn)();

		printf("\r\n");
	}

运行结果为:在2个子类中对speak()都进行了重载,因此执行的为对应类对应的函数
在这里插入图片描述
(2)当main函数中:类对象调用虚表中的第2个函数

for (int i = 0; i < 2; i++)
	{
		//(pPer[i]->*pPer[i]->m_pVirTable[0])();//获取虚表内第0项指针,等效为下面几步
		//等价于:
		//(1)找到对象头部四个字节--虚表
		PEN_VIRTUAL* pVirTable = pPer[i]->m_pVirTable;
		//(2)根据虚函数的位置(在cpp中定义的顺序),确定该虚函数在虚表的下标
		PEN_VIRTUAL pfn = pVirTable[1];
		//(3)找到对应的虚函数地址,直接调用该地址
		//pPer[i]->speak();//间接调用
		(pPer[i]->*pfn)();

		printf("\r\n");
	}

运行结果:Chinese类对eat()并未重载,English类对eat()进行了重载,因此Chinese对象仍执行Person类的eat()函数,English对象指向对应类的函数
在这里插入图片描述

4.学习视频地址:C++57个入门知识点_34_虚函数的模拟实现