关于虚函数的问题

时间:2021-12-28 18:38:26
小弟在了解虚函数的机理时遇到一些问题,以下是我在网上找到的源代码:

/******************************************************************/
#include <iostream>
#include <iomanip>
using namespace std;

class base
{
public :
virtual void f() {cout<<"this is 'base'"<<endl;}
};

class inher1:public base
{
public :
virtual void f() {cout<<"this is 'inher1'"<<endl;}
};

void main()
{
    base *b_ptr=new inher2;//产生一个对象
    void (*p)(base *)=0;   //声明一个函数指针
    long tlb_ptr=0;        //一个长整形变量,用于储存32位的地址
    
    memcpy(&tlb_ptr,b_ptr,sizeof(long));  
    memcpy(&p,reinterpret_cast<void *>(tlb_ptr),sizeof(long));

    p(b_ptr);

    delete b_ptr;
}
/******************************************************************/

在这里,小弟想问的是
    我把:
    memcpy(&tlb_ptr,b_ptr,sizeof(long));  
    memcpy(&p,reinterpret_cast<void *>(tlb_ptr),sizeof(long));
    改为:
    memcpy(&p,b_ptr,sizeof(long)); 
    后为什么出错了?

我尝试打印出 tlb_ptr,b_ptr,p 的“值”,发现三者互不相同,也是不明白,请各位大侠指教!

13 个解决方案

#1


函数指针比较特别,因为它们之间没有必然的联系,所以就不会产生隐式的指针转换。
故而不能用static_cast、dynamic_cast及const_cast来进行转换,只能使用reinterpret_cast强制实现转换。
b_ptr的指针类型比较隐晦,因为它是由编译器做出来的,所以其类型不可知。推测应该是
(void *)[]而不是void *。

#2


tlb_ptr的值是对象里的vptr的值,即vtbl的地址
b_ptr的值是对象的地址
p的值是虚拟函数的真实地址

如有不明白,我再想办法画个图来说明,留言,或者加QQ 510857

#3


将 memcpy(&p,b_ptr,sizeof(long)); 改为:
memcpy(&p,reinterpret_cast<void *>((*reinterpret_cast<long*>(b_ptr))),sizeof(long));
因为:memcpy(&p,b_ptr,sizeof(long)); 是将类对象的地址值解释为一个新的地址!
那肯定使 p 得不到正确指向!

#4


to :BluntBlade(无锋之刃)
 
 能讲解一下在
    memcpy(&tlb_ptr,b_ptr,sizeof(long));  
    memcpy(&p,reinterpret_cast<void *>(tlb_ptr),sizeof(long));
 中指针的转换过程吗?

 主要是不明白 ‘类对象’, ‘函数’,‘普通数据’ 的‘指针’ 有何区别, 为何要通过long类型变量作为中间量,才能取得虚函数表中真正的虚函数的地址?
 
to inline(虚函数):
 为何说 memcpy(&p,b_ptr,sizeof(long)); 是将类对象的地址值解释为一个新的地址?
 新的地址 指的是怎么样的地址呢?

#5


b_ptr          base: A           base's vbtl
                  vptr               ptr to f()
+-------+       +-------+          +-------+
| &A    |  ->   | &vbtl |    ->    | &f()  |
+-------+       +-------+          +-------+

&表示内存空间中的值为某个对象的地址

memcpy的函数原型是memcpy(void *,void *,int)

先看memcpy(&tlb_ptr,b_ptr,sizeof(long));
memcpy得到了目标地址(tbl_ptr的地址,long *类型,可以隐式转换为void *)
和源地址(b_ptr的值,即A的地址,base *类型,可以隐式转换为void *)。
因此得以调用memcpy。

执行memcpy后tlb_ptr开始的四字节内存中储存了&A开始的四字节内存原来的值,
即对象A的vptr的值,也就是vbtl的地址(因为base中没有定义数据成员,而定义
有虚函数,所以A中只有vptr)。

再看memcpy(&p,reinterpret_cast<void *>(tlb_ptr),sizeof(long));
memcpy得到了目标地址(p的地址, void (*)(base *),可以转换为void *)和源
地址(tbl_ptr的值,long类型,不能隐式转换为void *),故而不能调用memcpy。

于是必须通过reinterpret_cast<void *>(tlb_ptr)将tlb_ptr的值的类型long转化
为void *,这样就满足了memcpy的调用要求。

由上面的分析也可以得到:
tlb_ptr的值是对象里的vptr的值,即vtbl的地址
b_ptr的值是对象的地址
p的值是虚拟函数的真实地址

#6


inline兄的说法是:
b_ptr的值是对象的地址,*b_ptr的值就是vptr的值,即vbtl的地址。

memcpy(&p,b_ptr,sizeof(long)); 
可以通过编译,因为base *可以隐式地转换为void *。
这样p中的值就是vptr的值,即vbtl的地址,而不是函数的地址了。

单继承已经足够复杂,如果是多继承就更头大了。
如果楼主有心情的话可以去看《Inside The C++ Object Model》,里面有详尽的说明。

请指教。

#7


非常感谢 BluntBlade(无锋之刃) 。

现在终于明白原来传给 tlb_ptr 的是虚函数表的头地址,而不是那实际虚函数的地址。
看来我对指针的理解还不够深啊!

#8


让我来解释一下你的行为吧:
memcpy(&tlb_ptr,b_ptr,sizeof(long)); 
将基类指针 b_ptr 所指对象 4 个单元内容复制到 long 型对象 tlb_ptr 中,
现在的 tlb_ptr 与 b_ptr 所指的对象其实完全相同,不过 tlb_ptr 需要重新解释
才能当作类对象使用,否则使用 tlb_ptr 只能当一个值来用!
memcpy(&p,reinterpret_cast<void *>(tlb_ptr),sizeof(long));
reinterpret_cast<void *>(tlb_ptr) 将 tlb_ptr 的 值解释成地址!
乍一看好象这个地址是由类对象的位域"凑"成的!怎么能赋值给 p 呢?
但实际上并没有错,因为你的类中有虚函数,类中有一个"虚函数表",
这个虚函数表是一个指针数组,而 p 得到的是这个指针数组的首址!
总之,没有特别需要不要使用 reinterpret_cast 
即类中声明的第一个虚函数的地址,所以下面的调用才能进行!
假设类中没有虚函数,那么无论你怎么重新解释后面的调用都将是没有定义的!!!

#9


指针这玩意儿,够呛。楼主好好研究一下咯。
不过老实说,指针就是C/C++乃至计算机的灵魂级事物。

#10


谢谢各位老大。

不过,小弟还想问一个问题,就是我将我所给出的原码修改了一下,就是在base和inher1类中
加入其它的成员(私有,公有)数据与函数,在vc6.0下发觉程序依然正常运行,而且结果不变。这是否意味着某个类若有虚函数表,则表的头地址必定在类对象指针所指的内存地址
上(即“最开头”)???

#11


若某个类中有虚函数则会生成一个函数指针数组即虚函数表!
且在这个类对象的开头生成一个指针指向这个虚函数表的首址!
这些虚函数名被解释成为虚函数表的下标!
转换顺序先为从基类继承的虚函数,倘若本类中重新定义了则
本类中的覆盖从基类继承的!然后是在本类中顺序声明的虚函数!
但这个虚函数表不一定在类对象的体内!
你加一些非虚函数成员不影响结果!你在基类和派生类中加一些
虚函数试试!然后改变一下声明的顺序试试!再试一下派生类中
不重新定义基类的虚函数!你会明白的!

#12


vptr的位置是由编译器厂商决定的。不同的编译器有不同的现实方法,当然效率也不同。

#13


多谢各位大侠!

#1


函数指针比较特别,因为它们之间没有必然的联系,所以就不会产生隐式的指针转换。
故而不能用static_cast、dynamic_cast及const_cast来进行转换,只能使用reinterpret_cast强制实现转换。
b_ptr的指针类型比较隐晦,因为它是由编译器做出来的,所以其类型不可知。推测应该是
(void *)[]而不是void *。

#2


tlb_ptr的值是对象里的vptr的值,即vtbl的地址
b_ptr的值是对象的地址
p的值是虚拟函数的真实地址

如有不明白,我再想办法画个图来说明,留言,或者加QQ 510857

#3


将 memcpy(&p,b_ptr,sizeof(long)); 改为:
memcpy(&p,reinterpret_cast<void *>((*reinterpret_cast<long*>(b_ptr))),sizeof(long));
因为:memcpy(&p,b_ptr,sizeof(long)); 是将类对象的地址值解释为一个新的地址!
那肯定使 p 得不到正确指向!

#4


to :BluntBlade(无锋之刃)
 
 能讲解一下在
    memcpy(&tlb_ptr,b_ptr,sizeof(long));  
    memcpy(&p,reinterpret_cast<void *>(tlb_ptr),sizeof(long));
 中指针的转换过程吗?

 主要是不明白 ‘类对象’, ‘函数’,‘普通数据’ 的‘指针’ 有何区别, 为何要通过long类型变量作为中间量,才能取得虚函数表中真正的虚函数的地址?
 
to inline(虚函数):
 为何说 memcpy(&p,b_ptr,sizeof(long)); 是将类对象的地址值解释为一个新的地址?
 新的地址 指的是怎么样的地址呢?

#5


b_ptr          base: A           base's vbtl
                  vptr               ptr to f()
+-------+       +-------+          +-------+
| &A    |  ->   | &vbtl |    ->    | &f()  |
+-------+       +-------+          +-------+

&表示内存空间中的值为某个对象的地址

memcpy的函数原型是memcpy(void *,void *,int)

先看memcpy(&tlb_ptr,b_ptr,sizeof(long));
memcpy得到了目标地址(tbl_ptr的地址,long *类型,可以隐式转换为void *)
和源地址(b_ptr的值,即A的地址,base *类型,可以隐式转换为void *)。
因此得以调用memcpy。

执行memcpy后tlb_ptr开始的四字节内存中储存了&A开始的四字节内存原来的值,
即对象A的vptr的值,也就是vbtl的地址(因为base中没有定义数据成员,而定义
有虚函数,所以A中只有vptr)。

再看memcpy(&p,reinterpret_cast<void *>(tlb_ptr),sizeof(long));
memcpy得到了目标地址(p的地址, void (*)(base *),可以转换为void *)和源
地址(tbl_ptr的值,long类型,不能隐式转换为void *),故而不能调用memcpy。

于是必须通过reinterpret_cast<void *>(tlb_ptr)将tlb_ptr的值的类型long转化
为void *,这样就满足了memcpy的调用要求。

由上面的分析也可以得到:
tlb_ptr的值是对象里的vptr的值,即vtbl的地址
b_ptr的值是对象的地址
p的值是虚拟函数的真实地址

#6


inline兄的说法是:
b_ptr的值是对象的地址,*b_ptr的值就是vptr的值,即vbtl的地址。

memcpy(&p,b_ptr,sizeof(long)); 
可以通过编译,因为base *可以隐式地转换为void *。
这样p中的值就是vptr的值,即vbtl的地址,而不是函数的地址了。

单继承已经足够复杂,如果是多继承就更头大了。
如果楼主有心情的话可以去看《Inside The C++ Object Model》,里面有详尽的说明。

请指教。

#7


非常感谢 BluntBlade(无锋之刃) 。

现在终于明白原来传给 tlb_ptr 的是虚函数表的头地址,而不是那实际虚函数的地址。
看来我对指针的理解还不够深啊!

#8


让我来解释一下你的行为吧:
memcpy(&tlb_ptr,b_ptr,sizeof(long)); 
将基类指针 b_ptr 所指对象 4 个单元内容复制到 long 型对象 tlb_ptr 中,
现在的 tlb_ptr 与 b_ptr 所指的对象其实完全相同,不过 tlb_ptr 需要重新解释
才能当作类对象使用,否则使用 tlb_ptr 只能当一个值来用!
memcpy(&p,reinterpret_cast<void *>(tlb_ptr),sizeof(long));
reinterpret_cast<void *>(tlb_ptr) 将 tlb_ptr 的 值解释成地址!
乍一看好象这个地址是由类对象的位域"凑"成的!怎么能赋值给 p 呢?
但实际上并没有错,因为你的类中有虚函数,类中有一个"虚函数表",
这个虚函数表是一个指针数组,而 p 得到的是这个指针数组的首址!
总之,没有特别需要不要使用 reinterpret_cast 
即类中声明的第一个虚函数的地址,所以下面的调用才能进行!
假设类中没有虚函数,那么无论你怎么重新解释后面的调用都将是没有定义的!!!

#9


指针这玩意儿,够呛。楼主好好研究一下咯。
不过老实说,指针就是C/C++乃至计算机的灵魂级事物。

#10


谢谢各位老大。

不过,小弟还想问一个问题,就是我将我所给出的原码修改了一下,就是在base和inher1类中
加入其它的成员(私有,公有)数据与函数,在vc6.0下发觉程序依然正常运行,而且结果不变。这是否意味着某个类若有虚函数表,则表的头地址必定在类对象指针所指的内存地址
上(即“最开头”)???

#11


若某个类中有虚函数则会生成一个函数指针数组即虚函数表!
且在这个类对象的开头生成一个指针指向这个虚函数表的首址!
这些虚函数名被解释成为虚函数表的下标!
转换顺序先为从基类继承的虚函数,倘若本类中重新定义了则
本类中的覆盖从基类继承的!然后是在本类中顺序声明的虚函数!
但这个虚函数表不一定在类对象的体内!
你加一些非虚函数成员不影响结果!你在基类和派生类中加一些
虚函数试试!然后改变一下声明的顺序试试!再试一下派生类中
不重新定义基类的虚函数!你会明白的!

#12


vptr的位置是由编译器厂商决定的。不同的编译器有不同的现实方法,当然效率也不同。

#13


多谢各位大侠!