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] = ŋ
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_虚函数的模拟实现