C++虚函数表的问题!(看一段代码,高分悬赏)
这个问题比较复杂,但不涉及算法,只是对内存模型理解的比较深一点吧
大侠们耐心看下去,我都有注释的,只有了解虚函数机制才能解决 ,
如有大侠肯给出合理的解释,小弟感激不尽!!!悬赏80分,回答满意再加20分!
#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是一个函数指针的类型声明
Base b;
Fun pFun = NULL;
cout << "虚函数表地址:" << (int*)(&b) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;//先得到vptr即虚表数组首地址,然后解引用得到vtbl数组中的第一个函数地址
pFun = (Fun)*((int*)*(int*)(&b));//将第一个函数地址赋值给pfun
pFun();
return 0;
}
小弟对虚函数实现机制有基本的了解,知道虚函数表即vptr + vtable这些玩意!
上面的代码大概了解它的意思
小弟迷惑的是:
问题一:第二个注释行,可以这样(int*)*(int*)(&b),小弟感觉是对一个指针取地址再解引用。看下面代码
#include <stdio.h>
void main()
{
int a =6;
int *p = &a;
printf("%p\n", *&p);
printf("%p\n", p); //打出来结果都是一样
}
(int*)*(int*)(&b)这个代码好像对b取地址再解引用,int*只不过是类型转换而已!!!岂不是和这段代码一样一下代码
cout << "虚函数表 — 第一个函数地址:" << b << endl;我知道这样理解显然是错误的,但大侠能否指点哪儿理解错了???
问题二:(int*)*(int*)(&b)这段代码有很多int*强制转换,而cout << "虚函数表地址:" << (int*)(&b) << endl; 这条语句中的int*我却可以
删除或者改成void *都可以,(int*)*(int*)(&b)这里面的int*貌似很多不能去掉,甚至不能用void *替换等等~~~~,比如说改成(void*)*(int*)(&b)
是可以输出一样的地址,而换成 *(void*)(&b)却报错了.大侠能否解释下这里面的规则在哪里!!!
问题三:第三个注释行,是函数指针的赋值。看一段简单的代码
#include <stdio.h>
void fun()
{
printf("Hello,world!\n");
}
void main()
{
void (*p)(void);
p = fun;// p = &fun也可以,呵呵,why?
p();
}
p = fun;只要类型匹配就可以赋值,而 pFun = (Fun)*((int*)*(int*)(&b));前面的(Fun)*不知道是什么意思!!!
((int*)*(int*)(&b))已经是第一个虚函数的地址,赋值给类型匹配的pFun变量很好 啊,可是为什么非要加(Fun)*才能编译通过 啊!!!
其中有什么规则在里面吗!!!
附有VC6.0下运行结果!!! 展开
首先我们应当清楚:
为了得到正确的虚函数偏移量,C++编译器要将虚函数表的指针存在于对象实例中最前面的位置。
还有多层指针要从右往左一层一层拨剥开
问题一:当然不一样,首先p本身是一指针类型,是一种简单类型,*&p:先得到p在物理内存中的地址,再取出该地址中的数据,这个数据依然是指针类型,例如:p的物理地址是0x10,而0x10所在位置存的是0x20,那么输出就是0x20,而0x20依然是个地址。p:过程是相同的,只是写起来比较简单。
其次b是一对象类型,是非简单类型,插入符“<<”无法识别,除非你写一个重载该运算符的函数
&b是取出对象的地址,(int*)(&b)是将该地址转换为整形指针,就是对象虚表数组的首地址,*(int*)(&b)是将取出虚表数组的第一项,就是第一个虚函数的首地址,(int*)*(int*)(&b)就是将该地址转换为整形指针。
问题二:void*不是一个指向对象类型的指针(int*,long*都是),所以你向他取东西肯定是不行的,例如:void* p = NULL;
cout << *p << endl;
就不可以;
问题三:你这个之所以可以 是因为,你的函数碰巧没有参数,如果有的话,就不行了,编译器会说类似 从void()()转换成void()(char*)失败。
而Fun* 类型正好定义了参数序列。
一个类创建的实例它的前4个字节(即一个指针的长度)就指向该函数的虚表,前提是该类有虚表。所以:
*(int*)*(int*)(&b),
(int*)(&b),是先将对象的地址取出,转换成int*,这样就转成了一个int的指针。然后再用*取这个地址上的值,因为指针是int类型,所以就取出了4个字节的值,再把这个值用(int*)转成一个int指针,这个指针就指向vtable了。这个vtable还是个指针,它的值才是第一个函数的地址,所以还要再用*取次值。
用图说明大概就是:
对象的前4个字节 > vtable > 第一个虚函数。
所以必须要有那个*。
第二个问题:
void是个空类型,没有长度。而*是取地址上的值,必须要知道类型的长度,比如a是int*类型,那么*a编译器就知道从这个地址指向的位置上取4个字节。虽然void*变量本身有长度,但编译器无法知道其指向的地址有多长,所以用*取一个void*类型地址的值在编译时就会报错。
void*指针一般配合强转用作转换成任何指针类型,不会也不能参与表达式运算。
第三个问题:
void fun();
这么一个函数,函数名代表函数入口地址,加个&还是函数地址,在表达式中返回的是存有该函数入口地址的函数指针,这是编译器规定的。
像你写的
void (*p)(void);
这个p就是一个返回值为void,参数列表为void的函数指针,而fun返回一个函数指针,跟p的类型匹配,所以不需要强转。
为什么光有int*转换来的地址不能调用函数,
因为操作系统调用一个函数除了函数地址外,还需要它的参数列表,所以你光一个地址是无法调用函数的。
这个时候就要用到函数指针。函数指针除了可以存储函数地址外还能描述出函数的参数列表。
所以你用int*取到的地址,需要强转成FUN类型的函数指针,这样编译器就知道这个函数的参数和返回值,就能调用函数了。
(int *) * (int *)(&b)和后面的*&p是不一样的。
(int *) * (int *)(&b)首先通过 (int *)(&b)一个指针,这个指针指向虚函数表。
然后“*(int *)(&b)”是指用*操作符间接引用这个地址,得到了虚函数表的首地址,也就是得到这么一个指针,这个指针指向了第一个虚函数的地址。
最后的那个(int *)做了个类型转换,这样才能调用cout <<;
这么理解更好:(int *) (* ((int*)(&b)) )。
并不是你理解的cout << b <<endl,关键就在于第二个 *起了间接引用的作用。
问题二
这个比较简单,根据c++语法规则,指向void的指针比较特殊,是不能间接引用的。
一个指向任何对象类型的指针都可以赋值给类型为void *的变量,void *可以显式转换到另一个类型,其他操作是不安全的,因为编译器不知道实际被指的的是哪种对象,要想使用void *,必须显式地将它转换成某个指向特定类型的指针。
例如
int i;
void *p = &i;
*p = 10; //错误,不能这么做。
p++;//错误
int *pi=static_cast<int *>(p);//正确
*pi = 10;
所以,你(void *)*(int *)(&b)没错,而(int *)*(void *)(&b)却错了,就在于第二个*做了间接引用。
问题三
p = fun;// p = &fun也可以,呵呵,why?
因为对一个函数无非就是两种操作,调用和取地址。所以可以省略从函数指针得到函数的间接运算符&,也可以省略取得函数地址的运算符&。
就是说
p=fun;//正确
p=&fun;//正确
p();//正确
(*p)();//正确
pFun = (Fun)*((int*)*(int*)(&b));前面的(Fun)*不知道是什么意思!!!
Fun是一个函数指针类型, (Fun)作用是做类型转换, *是间接引用运算符。
前面已经知道(int*)*(int*)(&b)是一个指针,这个指针指向了第一个虚函数的地址。
那么*((int*)*(int*)(&b))就是得到了第一个虚函数的地址,但是还不能直接使用,因为这里编译器会把它当成一个int型的数,必须加上(Fun)类型转换后,才可以赋值给pFun,否则编译器是会提示类型不匹配的。
同理触类旁通,举一反三,可以知道
pfun = (Fun)*(((int*)*((int*)(&b)))+1);//将第二个函数地址赋值给pfun
pfun = (Fun)*(((int*)*((int*)(&b)))+2);//将第三个函数地址赋值给pfun
我很羡慕你的学习态度,钻研的精神很好,以后如果有什么有趣的东西,大家一起探讨,共同进步。
void*有可能不是32位。vc6.0我很久不用了,我也不能确定在vc6.0里void是否等于int长度,不过看一下错误信息应该就知道是什么问题了。
需要(Fun)*实际上可以看成((*p)(void))和*两部分
不过这个问题比较难说清楚,自己试验更能理解。
分都是浮云你还是给其他回答这个的人吧。。。