虚表的学习

虚表

首先要明白虚表的概念:

a—每个有虚函数的类或者虚继承的子类,编译器都会为它生成一个虚表,虚表中的每一个元素都指向一个虚函数的地址(虚表是从属于类的)

》》》》》》虚表存放的是虚函数的地址。只有含有虚函数的类才有虚表。 不同的类具有不同的虚表(父类有一份,子类也有一份)

》》》》》》父类中声明为虚函数,子类中重写的函数也自动变成虚函数(也就是说子类中可以省略virtual关键字),所以子类也有虚表

b–在对象中 对象的大小 不会包含虚函数大小,此时cat 对象的大小为 一个虚表指针 一个继承父类的 m_age ,一个整型变量m_life 的大小,

c–在类中 类的大小包含 虚表指针的大小 继承的变量大小,本类变量的大小,

d-- 虚表指针指向了一个数组,也就是指向了虚表的地址。

e–编译器会为包含虚函数的类加上一个成员变量,是一个指向该虚函数表的指针,虚表指针是从属于对象的,也就是说,如果一个类含有虚表,那么类的每个对象都含有虚表指针

f–虚表的内容是依据类中的虚函数声明次序—填入函数指针。

g-- 派生类对基类中那些虚函数进行重写,如果重写了,会将虚表中相同偏移位置的函数的地址进行

  • 子类完全重写父类的虚函数
class Animal
{
    public:
    int m_age;
    virtual void speak(){
        cout<<"Animal::speak()"<<endl;
    }
    virtual void run(){
        cout<<"Animal::run()"<<endl;
    }
};
class Cat:public Animal
{
    public:
    int m_life;
    void speak(){
        cout<<"Cat::speak()"<<endl;
    }
    void run()
    {
        cout<<"Cat::run()"<<endl;
    }
    
};
Animal *cat=new Cat();
cat->m_age=20;
cat->speak();
cat->run();

运行结果

Cat::speak()
Cat::run()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iDtsISQT-1658400621645)(/home/ding-jin-xing/图片/2022-01-20 14-18-20 的屏幕截图.png)]

所有的Cat对象(不管在全局区、栈、堆)共用同一份虚表

  • 子类重写父类部分虚函数
class Animal
{
    public:
    int m_age;
    virtual void speak(){
        cout<<"Animal::speak()"<<endl;
    }
    virtual void run(){
        cout<<"Animal::run()"<<endl;
    }
};
class Cat:public Animal
{
    public:
    int m_life;

    void run()
    {
        cout<<"Cat::run()"<<endl;
    }
    
};
Animal *cat=new Cat();
cat->m_age=20;
cat->speak();
cat->run();

运行结果

Animal::speak()
Cat::run()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pwYdCPpm-1658400621646)(/home/ding-jin-xing/图片/2022-01-20 14-29-42 的屏幕截图.png)]

继承的是父类的speak函数 而且没有进行重写,所以虚表中就不会有对父类的speak函数 进行重写的操作


其次要懂得调出虚表中的地址,虚表中的地址也就是函数的地址,所以可以间接调出函数。

观察上面所列的两个内存图

虚表指针是一个二级指针,指向的是一块内存的地址。int **point =&虚表的地址;这个二级指针占的内存是4个字节,因为这个二级指针位于对象的首部,所以这个二级指针占的地址也就是对象的地址,所以取对象的地址,就得到了这个二级指针的地址(也就是point)。 然后我们对这个二级指针解第一层引用,就得到了虚表的地址(也就是*point) 然后进行第二次解引用,就得到了虚函数的地址。

下面举例子说明一下怎么调出虚表地址和函数地址

只有基类时:


#include <iostream>
using namespace std;
class Base {

public:

	virtual void f() { cout << "Base::f" << endl; }

	virtual void g() { cout << "Base::g" << endl; }

	virtual void h() { cout << "Base::h" << endl; }

};
int main()
{
	
void(*pFUN)();//定义一个函数指针
	Base b;

	Base * p = &b;

	cout << "该对象的地址:" << p << endl;

	cout << "虚表指针地址"<< (int*)(&b) endl;
    cout<<"********************************************************************"<<endl;

	cout << "虚函数表的指针指向的地址10进制:" << *(int*)(&b) << "即虚函数表的指针存的内容"<<endl;

	cout << "即虚函数表的地址:" << (int*)*(int*)(&b) << endl << endl;
    cout<<"*****************************************************************"<<endl;

	pFun = (void(*) ())*(int*)*(int*)(&b);//第一个虚函数的指针

	cout << "第一个虚函数的地址:" << pFun << endl;

	pFun();//调用第一个虚函数

	pFun gFun = NULL;
	gFun = (void(*) ())*((int*)*(int*)(&b) + 1);//第二个虚函数的指针

	pFun hFun = NULL;
	hFun = (void(*) ())*((int*)*(int*)(&b) + 2);//第三个虚函数的指针
}


解释一下这里的强制转换。

在计算机中,内存是没有任何含义的,计算机并不知道地址的意义。

本身是个地址 是你肉眼看上去它是个地址,你脑子里知道它是个地址而已。。但是电脑不知道

同样一个数 97. 它到底是自然数97呢,还是表示 字符‘a’呢,还是表示指针地址呢?这些都要靠类型决定。

类型决定了一个变量所代表的(二进制数字)的含义,计算机都是二进制的,所以一切都是数字。
数字代表什么,由它的类型决定。

举一个简单的例子,

 int a=0int*p=&a;    

把a的地址赋值给变量p,但是计算机并不知道 变量p存储的这块地址是什么含义,所以前面加了强制转换 int* ,以表示 p是指向整型数据的地址。

b返回Base类型的对象。

&b返回Base *类型的指针。

(int *)(&b)将Base *类型的指针转换为int *类型的指针,转换后指针指向地址没变,但指向对象的类型变了。 表明 b是指向整型数据的指针

*(int *)(&b)int *类型的指针解引用,从地址开始的那个字节开始,取出sizeof(int)个字节,赋值给一个int对象(因为指针认为自己指向一个int对象)。
    由于这是二级指针,解完第一层引用后,依旧为地址,所以作为地址需要强制转换说明含义,

(int *)*(int *)(&b)相当于 (int *)后接一个int值,返回一个int指针,将这个int值作为该指针指向的地址值。表明这个地址值指向int 型的数据

*(int *)*(int *)(&b)int *类型的指针解引用,返回int值。解最后一层引用得到的是虚函数的地址,需要说明这个地址代表的是void(*)()这种类型的虚函数的地址,所以加了强制类型转换。
所以虚函数的地址就表示为
    
(void(*)())*(int *)*(int *)(&b)

虚函数的地址其实就可以看作是对二级指针变量取了两次地址, 第一次解引用就得到了虚表地址
   虚表可以看成是一维数组,所以虚表地址相当于数组名,一维数组名+1 代表一维数组下一个数据,也就是这里的下一个虚函数。
    

当然我们也可以采用typedef的方式,对void(*)() 进行重定义

#include "pch.h"
#include <iostream>
using namespace std;
class Base {

public:

	virtual void f() { cout << "Base::f" << endl; }

	virtual void g() { cout << "Base::g" << endl; }

	virtual void h() { cout << "Base::h" << endl; }

};
int main()
{
	typedef void(*Fun)(void);//这样,Fun就可以作为 void(*)(void); 这种类型的别名

	Base b;

	Fun pFun = NULL;

	Base * p = &b;

	cout << "该对象的地址:" << p << endl;

	cout << "虚函数表的指针也是从这个地址"<< (int*)(&b) <<"开始存的" << endl << endl;

	cout << "虚函数表的指针指向的地址10进制:" << *(int*)(&b) << "即虚函数表的指针存的内容"<<endl;

	cout << "即虚函数表的地址:" << (int*)*(int*)(&b) << endl << endl;

	pFun = (Fun)*(int*)*(int*)(&b);//第一个虚函数的指针

	cout << "第一个虚函数的地址:" << pFun << endl;

	pFun();

	Fun gFun = NULL;
	gFun = (Fun)*((int*)*(int*)(&b) + 1);//第二个虚函数的指针

	Fun hFun = NULL;
	hFun = (Fun)*((int*)*(int*)(&b) + 2);//第三个虚函数的指针
}


  1. 在有虚函数的基类对象中,肯定至少有三块不同的内存存储区域。

  2. 首先是对象内存空间,其开始区域,存了虚函数表的指针。(所以虚指针地址就是对象地址)第一次解引用得到虚表地址,第二次得到虚表中虚函数地址

  3. 虚函数表实际是一个指针的数组,这些指针就是虚函数的函数指针。

  4. 最后是各个虚函数的存储区域。


    //之前说过,对象中存在一个二级指针,所以我们可以直接定义一个二级指针,将二级指针的地址强制转换然后赋值给二级指针,强制转换是因为地址的含义不明确。
    //然后我们利用二级指针和二维数组的关系,直接取出虚函数
    
    #include <iostream>
    using namespace std;
    class Animal {
    public:
    	virtual  void speak() {
    		cout << "Animal::speak()" << endl;
    	}
     virtual  	void run() {
    		cout << "Animal::run()" << endl;
    	}
    };
    class Cat : public Animal {
    public:
    	void speak() {
    		cout << "Cat::speak()" << endl;
    	}
    	void run() {
    		cout << "Cat::run()" << endl;
    	}
    };
    int main() {
    	Animal *cat=new Cat;
    
    long int**ptr=(  long int **)cat;
    typedef void (*PF)();
    PF func =(PF)ptr[0][0];//第一个虚函数地址
    func ();
    PF fund =(PF)ptr[0][1];//第二个虚函数地址
    fund();
    
    }
    
    
    
    
    Cat::speak()
    Cat::run()
    

    运行结果如上

    虚函数表的结束标志

    在上面例子中还需要讲一个细节,在虚函数表最后位置有一个字节用来标志虚函数表的结束。

加入如上代码便可以得到结束标志,((int*)*(int*)(&b) + 3)这里指向了虚函数表即指针数组的第四个元素,但实际上数组里只有三个指针,所以这里便刚好指向了结束标志。再通过(char*)转换指针类型,代表指向的是一个字节。


单继承(无虚函数覆盖)

在此例中,基类有三个虚函数,派生类也有三个虚函数,但派生类一个虚函数也没有去重写。


#include <iostream>
using namespace std;
class Base {
public:

	virtual void f() { cout << "Base::f" << endl; }

	virtual void g() { cout << "Base::g" << endl; }

	virtual void h() { cout << "Base::h" << endl; }

};
class Derive : public Base {
public:

	virtual void f1() { cout << "Derive::f" << endl; }

	virtual void g1() { cout << "Derive::g" << endl; }

	virtual void h1() { cout << "Derive::h" << endl; }

};
int main()
{
	typedef void(*Fun)(void);
	Derive d;
	Base *p = &d;//父类指针指向子类对象  ,子类含有六个虚函数,

	Fun fFun = NULL;
	fFun = (Fun)*((int*)*(int*)(&d) + 0);//第一个虚函数的指针

	Fun gFun = NULL;
	gFun = (Fun)*((int*)*(int*)(&d) + 1);//第二个虚函数的指针

	Fun hFun = NULL;
	hFun = (Fun)*((int*)*(int*)(&d) + 2);//第三个虚函数的指针

	Fun f1 = NULL;
	f1 = (Fun)*((int*)*(int*)(&d) + 3);
	Fun g1 = NULL;
	g1 = (Fun)*((int*)*(int*)(&d) + 4);
	Fun h1 = NULL;
	h1 = (Fun)*((int*)*(int*)(&d) + 5);

	char* end = NULL;
	end = (char*)((int*)*(int*)(&d) + 6);
}

int main()
{
	typedef void(*Fun)(void);
	Derive d;

	int *vTable = (int *)*(int *)(&d);//vTable是虚函数表的地址,也就相当于一维数组名
	for (int i = 0; i<6; ++i)//判断条件写成vTable[i] != 0,有可能会报异常
	{
		printf("function : %d :0X%x->", i, vTable[i]);
		Fun f = (Fun)(vTable[i]);
		f();         //访问虚函数
	}
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uP47Kxcz-1658400621647)(/home/ding-jin-xing/图片/2022-01-21 00-17-02 的屏幕截图.png)]


单继承(有虚函数覆盖)

#include "pch.h"
#include <iostream>
using namespace std;
class Base {
public:

	virtual void f() { cout << "Base::f" << endl; }

	virtual void g() { cout << "Base::g" << endl; }

	virtual void h() { cout << "Base::h" << endl; }

};
class Derive : public Base {
public:

	virtual void f() { cout << "Derive::f" << endl; }

	virtual void g1() { cout << "Derive::g" << endl; }

	virtual void h1() { cout << "Derive::h" << endl; }

};
int main()
{
	typedef void(*Fun)(void);
	Derive d;

	int *vTable = (int *)*(int *)(&d);
	for (int i = 0; i<5; ++i)
	{
		printf("function : %d :0X%x->", i, vTable[i]);
		Fun f = (Fun)(vTable[i]);
		f();         //访问虚函数
	}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n36CSTD6-1658400621647)(/home/ding-jin-xing/图片/2022-01-21 00-22-47 的屏幕截图.png)]

运行结果如图上:

注意子类的重写虚函数是覆盖掉了原父类虚函数的位置

『原本的运行情况是6个虚函数,由于子类的 f()函数 重写了父类的f()函数,所以子类的f()函数覆盖掉了子类虚表中父类虚函数的位置』

最后如果代码报错:warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]

这是指针长度和地址改变前后不一致,这时把每次进行强制类型转换的(int*)换成*

// p = (void ()())(ptrdiff_t)(ptrdiff_t)(cat); //第一个虚函数地址

或者

p = (void ()())(long int*)(long int)(cat); //第一个虚函数地址

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁金金

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值