今天学习虚函数时看到代码中的两个强制类型转换十分不解,查了蛮多资料,终于知道了原因,因此写下自己的见解。
简单说一下虚表的概念:在一个类中如果有虚函数,那么此类的实例中就有一个虚表指针指向虚表,这个虚表是一块儿专门存放类的虚函数地址的内存。
通过sizeof运算符我们可以查看一个对象所占的空间,对于一个只有函数(无虚函数)没有属性的空类对象,它的空间长度为1,而对于有虚函数、没有属性的空类对象,无论虚函数的数量为多少,空间长度都为4,这是因为它包含一个指向虚表的指针,指针内存放的是虚表的地址。(不同环境下空间长度或许不同,但是它的空间长度一定是指针所占的空间长度)
定义一个对象后,构造器为其分配空间,而这个指向虚表的指针始终都是在第一个,也就是说这个指针的地址就是对象地址。这样我们就轻松获取到了这个指向虚表的指针的地址。
我们可以把虚表想象为一个存放许多地址的指针数组,它的内容就是虚函数的地址。
因此,我们就找到了获取虚表地址和虚函数地址的方法。
实例化一个对象,指向虚表的指针的地址就是对象的首地址,指针的内容就是对象的前四个字节,也就是虚表的地址,剩下的就当作一个指针数组处理就可以了。
#include<iostream>
using namespace std;
class base
{
public:
int a;
public:
virtual void showa()
{
cout<<"base::a"<<endl;
}
virtual void showb()
{
cout<<"base::b"<<endl;
}
virtual void showc()
{
cout<<"base::c"<<endl;
}
};
int main()
{
base b;
cout<<sizeof(b)<<endl;
cout<<&b<<endl; //对象首地址
cout<<(int*)&b<<endl; //指针首地址,(int*)强制类型转换是为了取四个字节的内容
cout<<*(int*)&b<<endl; //指针的内容,即虚表的地址
cout<<*((int*)*(int*)&b)<<endl; //虚表第一个元素即虚函数showa()的地址
cout<<*((int*)(*(int*)&f)+1)<<endl; //第二个元素的地址
/*
*这里可能有人奇怪虚表的地址不就是第一个元素的地址,为什么这里要加(int*)?
*代码的后面会有解释
*/
typedef void(*Fun)(void); //重命名函数指针类型
Fun pfun;
pfun=(Fun)*(int*)*(int*)&b; //强制类型转换为该函数指针类型
b.showa();
pfun();
return 0;
}
加(int*)的原因:
其实它(int*)的作用是一样的,都是为了取四个字节的内容,至于为什么要这样做就涉及到了指针的基础知识。
我们都知道定义指针变量时一定要声明类型,不同于其他基础类型,定义int变量就是4个字节,定义char变量是1个字节,定义short变量就是2个字节,我们都知道无论是什么类型的指针都占4个字节,那么声明类型到底有什么用呢?
它就是为了限定指针在内存上取变量的长度,这样说可能不太清楚,我们来看代码:
#include<stdio.h>
int main()
{
int a=134480385;
int* pa=&a;
char* pb=&a;
short* pc=&a;
printf("*pb=%d *pc=%d *pa=%d\n",*pb,*pc,*pa);
}
a的二进制表示为:0000 1000 0000 0100 0000 0010 0000 0001
这样大家看的应该就比较清楚了,char类型的指针pb只会取一个字节,因此取出的数为0000 0001,即1
short类型的指针pc会取两个字节,因此取出的数为0000 0010 0000 0001,即513
同理,int类型指针取四个字节,会全部取出。
好了,现在回到虚表中的(int*)中,现在大家是不是能够理解为什么了呢。如果不加(int*)编译器就不知道应该取的长度
在取虚表地址的代码中,我们可以看到这个虚表指针是没有名字的,因此就无法像我们平常使用指针那样直接调用变量来引用内容,只能通过*(int*)&b的方式获取指针的内容,*(int*)&b就是获取对象b空间上的前四个字节的内容。
同理,虚表虽然像一个指针数组,但是我们不知道它的名字,也就无法像原来使用数组那样直接使用数组名和下标的方式使用,只能通过地址来调用内容。*(int*)*(int*)&b,就是在虚表的空间上取四个字节,它就是第一个虚函数的地址。(int*)(*(int*)&f)+1是第二个元素的地址,加*取它的内容,即第二个虚函数的地址。