+ -
当前位置:首页 → 问答吧 → [中级扫盲贴]关于虚函数表vtable

[中级扫盲贴]关于虚函数表vtable

时间:2010-06-28

来源:互联网

要理解vtable,首先要理解多态。

先举一个关于多态的示例程序,观察多态的效果
  1. class Base {
  2. public:
  3.     // 注:用virtual修饰
  4.     virtual void who_am_i(void) {
  5.         std::cout<<"I am Base."<<std::endl;
  6.     };
  7. };

  8. class Derived {
  9. public:
  10.     void who_am_i(void) {
  11.        std::cout<<"I am Derived."<<std::endl;
  12.     };
  13. };

  14. int main(void) {
  15.     Derived obj;
  16.     Base *ptr = &obj;
  17.     ptr->who_am_i();
  18. };
复制代码
运行结果(如无意外)应是:
I am Derived.

可是,如果Base::who_am_i 取消 virtual修饰,运行结果就会变成
I am Base.

这是因为vtable的作用,你可以大致上这样理解,实际情况因应不同编译器而略有差异:

1. 首先,父类中所有被 virtual 修饰的成员方法,都会被编译器关联到一个特定数组的下标值,这个值无需程序员关注

    譬如 Base::who_am_i() 和 Derived::who_am_i 都会被赋予 4
    如果还有一个 Base::foo 和 Derived::foo 都会被赋予 5

    如果 Derived 增加了一个新的virtual bar 方法,而父类没有,那么 Derived::bar 以及在其子类中 会被赋予 更大的下标值

2. 所有Base 和 Derived,以及其后的更多的子类的对象的内存布局中,会被额外安插多一个指针项
    这个指针项就是指向所谓的 vtable,你可以理解为一个数组(只是可以这样理解,实际情况请自己动手做实验)
    然后 vtable 中就会存放各个 virtual 成员方法的入口地址
   上述的 who_am_i 方法的入口地址,无论是 Base 的 vtable 还是 Derived 的 vtable,总是被放在下标元素为4 的数组元素中。
    尽管 Base::who_am_i 和 Derived::who_am_i 的入口地址是不一样的(所以其实每个类的vtable都是不一样的),
    但是在各自的类中,都可以用 vtable[4]取到他们各自的入口地址。

3. 剩下来的事情就很简单了,
    将所有对 virtual 方法的调用,由原来的直接 jump 到一个确定的地址
    全部改为 jump 到 vtable[n] 的调用,
    至于使用哪一个vtable,由指针或引用所指的对象的 vtable 指向所决定,也就是实际类型的 vtable

所以实际情况是这样的,在有virtual 修饰的情况下, ptr->who_am_i(); 这句

会变成类似于:  (*ptr->vtable[4])() 这样的代码。

作者: xyfree   发布时间: 2010-06-28

要么都virtual,要么都别virtual,不过这仅仅是个写法问题……语法上,只要继承的类中有一个函数写了virtual,那么从这里开始这一条线儿上就都是virtual了…………

作者: starwing83   发布时间: 2010-06-28

本帖最后由 xyfree 于 2010-06-28 22:45 编辑

回复 starwing83


    不是的,有些时候你的private的方法不会想用virtual的,这样子类就不会无意中修改了理想的行为。

    所以Java 里面默认 全部 virtual 是一个很笨的 idea,因为其实 virtual 的成员方法会污染子类的成员方法的命名空间。
    不过Java自身也意料到这个问题,所以他们有一个很搞笑的解救办法。
  1. // java code

  2. // Base.java
  3. public class Base {

  4.     // 注意,这里的private 改成 public 的效果完全不一样
  5.     private void print() {
  6.         System.out.println("I am Base");
  7.     }
  8.     public void who_am_i() {
  9.         print();
  10.     }   
  11. }

  12. // Derived.java
  13. public class Derived extends Base {
  14.     private void print() {
  15.         System.out.println("I am Derived");
  16.     }
  17. }

  18. // Test.java
  19. class Test {
  20.     static public void main(String args[]) {
  21.         Base obj = new Derived();
  22.         obj.who_am_i();
  23.     }
  24. }
复制代码
试一下吧,非常搞笑

作者: xyfree   发布时间: 2010-06-28

这些问题《深度探索C++对象模型》里面讲得很清楚。

作者: 没本   发布时间: 2010-06-28

支持楼主做C++扫盲,最好出一个系列的, 让我们有点C基础的看起来省力点

作者: peijue   发布时间: 2010-06-28

回复 没本


    是啊,可是有人就看不进去怎么办,我觉得我也不会说得比那本书好……虽说那里比较长篇,郁闷。
    等这帖沉了之后,又会有更多新手重复问这些问题。
    社会进步就是这么慢呐.........

作者: xyfree   发布时间: 2010-06-28

回复 xyfree


    呵呵是啊。我的办法就是看到问的就回点,慢慢赚分。。。。。。

作者: 没本   发布时间: 2010-06-28

等全写好了楼主做个合集吧。把你发的这些帖子全串起来。

作者: 没本   发布时间: 2010-06-28

说实话
我认为这些可能多数人都能看得懂
他们的问题恐怕是看不出这些东西如何才能恰倒好处地应用并且强于C

作者: pmerofc   发布时间: 2010-06-28



QUOTE:
回复  starwing83


    不是的,有些时候你的private的方法不会想用virtual的,这样子类就不会无意中修 ...
xyfree 发表于 2010-06-28 21:25




    你真是……………………


第一,我说的是在一条继承树枝上面,谁跟你扯单个类了?

第二,你的代码有误,class Derived : public Base


第三,0x出来前拒绝讨论C++,光那个复制语义的stl就够呛。

作者: starwing83   发布时间: 2010-06-28



QUOTE:
要么都virtual,要么都别virtual,
starwing83 发表于 2010-06-28 21:15



我以为你说,一个类里面所有的方法,要么都virtual,要么都别virtual……

谢谢提醒,已改正。
你也写错了, java里面的继承用 关键字 extends 哈哈哈哈

作者: xyfree   发布时间: 2010-06-28

  1. (*ptr->vtable[4])()
复制代码
看见这个我笑了

作者: OwnWaterloo   发布时间: 2010-06-28

回复 OwnWaterloo


    这个.....没错吧?错了也不管了,反正意思表达到就算了。

作者: xyfree   发布时间: 2010-06-28

回复 xyfree

我写一个更generic的, 不可能参数全为空吧?
  1. (int* (*)(int))(p->vtable[0])( 1212 );
  2. (void (*)(double))(p->vtable[0])( 3.26 );
复制代码
这是很麻烦的。


我猜你也是从汇编看出来的吧? 我也被汇编骗过……

其实用"函数指针的结构体" 更容易表达。
最终生成的代码是一样的, 是按offset寻址。
因为所有函数指针的表示相同, 所以从汇编上看, 就像一个数组。
但用结构体有更好的静态类型检测, 而且书写没这么麻烦……

作者: OwnWaterloo   发布时间: 2010-06-28

回复 OwnWaterloo

你的写法非常精确。除了
  一 你都漏了this参数
  二 那些方法除了this之外确实是无其他参数的,也不返回,故意的,就因为我知道展开会非常麻烦

我是实在不想在这中情况下考虑类型系统,
只会给思考带来羁绊......哈哈哈哈

反正是编译器替我写那些代码
我就不管了,哈哈

作者: xyfree   发布时间: 2010-06-28

回复 xyfree

this是从你那里开始漏的……
我就是在给你介绍一种展开不麻烦的方法。

作者: OwnWaterloo   发布时间: 2010-06-29

回复 OwnWaterloo


    我大致了解你的意思,你的想法是不是说,将整个函数参数列表视作一个结构体,然后用这个结构体的指针取代原来的参数列表?
    那我是不是还要先交待我这样的想法呢?毕竟这不是顺其自然的事情。
    所以,与其引入更多的变换信息,不如还是用最传神的表达方式简化掉算了。
    这个题外话。

    话说,你们那个VimE,写到哪个部分有可能会接纳C++  >_<

作者: xyfree   发布时间: 2010-06-29

回复 xyfree
  1. typedef struct I_ I;
  2. struct I_ {
  3.     int (*f1)(I** self, int p1);
  4.     void (*f2)(I** self, double p1, int p2);
  5.     ...
  6. };

  7. typedef struct {
  8.     I* vptr_;
  9.     ...
  10. } C;

  11. int C_f1(I** self, int p1);
  12. int C_f2(I** self, double p1, int p2);

  13. I C_vtbl = { &C_f1, &C_f2, ... };

  14. C c;
  15. I** i = &c.vptr_;

  16. (*i)->f1(i, a1);
  17. (*i)->f2(i, a1, a2);
复制代码

作者: OwnWaterloo   发布时间: 2010-06-29

回复 xyfree

vime由sw负责~_~  直接向他询问~

作者: OwnWaterloo   发布时间: 2010-06-29