虚函数的作用、原理等具体内容可以看我的另一篇文章 C++ 虚函数详解
这里讨论如何通过对象找到他的虚函数表还有相应的虚函数。
相关概念:
变量:不管是用int
还是int*
声明的变量,本质上都是一样的,都存储在内存中,都具有两个要素:地址和内容。
地址:指变量存储在内存中的地址,一般用16进制表示,如0x61febc。可以用 & 对一个变量进行取址,返回的就是变量地址。
内容:指存储在内存中的内容,如char
大小为一个字节,那么他就可以表示2的8次方个字符。(ASCII码决定了什么数位表示什么字符)。
指针:指针和普通变量“不一样”的地方在于指针存储的内容是“地址”。因此可以对指针进行解引用(*)操作,就会返回存储在这个地址的变量的值。不同类型的指针,存储的都是地址,他们的差别在于解引用(*)的时候根据不同类型读取长度不同的内容,int*
类型会从地址开始读取四个字节的内容,而long long*
会从地址开始读取八个字节的内容。
用指针获取虚函数表指针和虚函数表内容:
class Drive{
public:
virtual void g(){
cout << "Drive g()" << endl;
}
virtual void h(){
cout << "Drive h()" << endl;
}
int m_Drive = 9;
};
int main(){
typedef void (*func)(void);
func f;
Drive* d = new Drive();
//找到虚函数表并执行其中的虚函数
int* pDrive = (int*)*(int*)d;
f = (func)*(pDrive + 0);
f();
f = (func)*(pDrive + 1);
f();
//通过内存访问成员变量
pDrive = (int*)d + 1;
cout << *pDrive << endl;
return 0;
}
//输出
Drive g()
Drive h()
9
访问虚函数解析:
1、Drive* d = new Drive();
变量d是一个指针,存储的是对象的地址。
2、(int*)d;
目标是求出虚函数表指针。虚函数表指针存储在对象的起始地址,并且函数指针大小为四个字节,所以可以转化为整型指针 int*,这样在对(int*)d
解引用(*)的时候,可以正确获得虚函数表指针。
指针的类型,指的是这个地址上存储的变量的类型。使用正确的指针类型,在对指针解引用或者加减操作的时候,才能有正确的表现。
3、*(int*)d;
解引用,此时获得虚函数表的地址(虚函数表中顺序存储着所有虚函数的地址(函数指针))。
4、int* pDrive = (int*)*(int*)d;
要通过虚函数表地址获得他的内容,需要解引用(*),要先转化成指针类型,因为虚函数表的内容是函数指针(四个字节大小),所以转化成整型指针。
5、f = (func)*(pDrive + 0)
解引用获得第一个虚函数的地址,再转化成对应的(func)
函数形式,供调用。
6、f()
根据函数地址,调用函数。
7、*(pDrive + 1)
指针 + 1,因为是整型指针,向下移动四个字节,也就是第二个虚函数地址的位置。重复5、6就可以调用函数。
8.pDrive = (int*)d + 1;
在对象实例的内存中,虚函数指针在首四个字节,成员变量排在他的后面,所以先把d转成整形指针,再+1,指针就会向下移动四个字节,直接解引用*pDrive
就可求出变量。
拓展:
如果成员变量由 int 变为long long,那么要怎么得出来呢?我想到的是:
//答案A
int* pDrive = (int*)d + 1; //(1)
cout << *(long long*)pDrive << endl; //(2)
因为通过操作(1),指针已经指向第四个字节的位置,而这就是成员变量的起始位置(我认为),变量大小为八个字节,所以先转成long long* 再解引用就可以得出答案。但是事实并非如此。
解答:
gcc编译默认内存对齐单位是4,也就是#pragma pack(4)
但是下面这种情况:
class T{
public:
int n;
long long m;
};
//sizeof(T) = 16
如果内存对齐单位为4的话,大小应该为12而不是16,这反而像是内存对齐为 8,我也不知道为什么TAT,可能是编译器自己的优化?
在显式设定#pragma pack(4)
后,结构体大小是12,符合内存对齐单位为4了,这时,答案A可用的。