虚表
首先要明白虚表的概念:
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=0; int*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);//第三个虚函数的指针
}
-
在有虚函数的基类对象中,肯定至少有三块不同的内存存储区域。
-
首先是对象内存空间,其开始区域,存了虚函数表的指针。(所以虚指针地址就是对象地址)第一次解引用得到虚表地址,第二次得到虚表中虚函数地址
-
虚函数表实际是一个指针的数组,这些指针就是虚函数的函数指针。
-
最后是各个虚函数的存储区域。
//之前说过,对象中存在一个二级指针,所以我们可以直接定义一个二级指针,将二级指针的地址强制转换然后赋值给二级指针,强制转换是因为地址的含义不明确。 //然后我们利用二级指针和二维数组的关系,直接取出虚函数 #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); //第一个虚函数地址