Chinaunix首页 | 论坛 | 博客
  • 博客访问: 507981
  • 博文数量: 95
  • 博客积分: 5168
  • 博客等级: 大校
  • 技术积分: 1271
  • 用 户 组: 普通用户
  • 注册时间: 2008-12-28 23:31
文章分类

全部博文(95)

文章存档

2013年(2)

2012年(3)

2011年(1)

2010年(8)

2009年(81)

分类:

2009-10-14 21:41:13

-------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处:
http://sjj0412.cublog.cn
-------------------------------------------

虚函数是面向对象编程语言里一个很重要的机制,下面我们以一个c++例子,分析其对应的c语言程序来说明虚函数的机制。

面向对象有了一个重要的概念就是对象的实例,对象的实例代表一个具体的对象,故其肯定有一个数据结构保存这实例的数据,这一数据包括变量,接口函数指针,如果是虚函数,则有相应的虚函数指针,其他函数指针不包括。

要讲虚函数机制,必须讲继承,因为只有继承才有虚函数的动态绑定功能,先讲下c++继承对象实例内存分配基础知识:

 

c++继承分为两种,普通继承和虚拟继承(virtual)。具体的继承又根据父类中的函数是否virtual而不同。
下面就单继承分为几种情况阐述:
1.普通继承+父类无virtual函数
    若子类没有新定义virtual函数  此时子类的布局是 : 由低地址->高地址  为父类的元素(没有vptr),子类的元素(没有vptr).
   若子类有新定义virtual函数  此时子类的布局是 : 由低地址->高地址  为父类的元素(没有vptr),子类的元素(包含vptr,指向vtable.)

2. 普通继承+父类有virtual函数
    不管子类没有新定义virtual函数  此时子类的布局是 : 由低地址->高地址  为父类的元素(包含vptr), 子类的元素.
    如果子类有新定义的virtual函数,那么在父类的vptr(也就是第一个vptr)对应的vtable中添加一个函数指针.

3.virtual继承
  
若子类没有新定义virtual函数  此时子类的布局是 : 由低地址->高地址  子类的元素(vptr),虚基类的元素.为什么这里会出现vptr,因为虚基类派生出来的类中,虚类的对象不在固定位置(猜测应该是在内存的尾部), 要一个中介才能访问虚类的对象.所以虽然没有virtual函数,子类也需要有一个vptr,对应的vtable中需要有一项指向 虚基类.
  
若子类有新定义virtual函数  此时子类的布局是与没有定义新virtual函数内存布局一致.但是在vtable中会多出新增的虚函数的指针.


4.
多重继承
   
此时子类的布局是 : 由低地址->高地址  为父类p1的元素(p1按照实际情况确定元素中是否包含vptr), 父类p2的元素(p2按照实际情况确定元素中是否包含vptr),子类的元素.
  
如果所有父类都没有vptr,那么如果子类定义了新的virtual function,那么子类的元素中会有vptr,对应的vtable会有相应的函数指针.
 
如果有的父类存在vptr.如果子类定义了新的virtual function,会生成一个子类的vtable,这个子类的vtable是,在它的父类的vtable中后添加这个新的虚函数指针生成的.因为子类分配的空间显示并没有新增加一个4字节的指针空间,其实不管子类增加了多少新的虚函数,其空间大小不变,因为其和虚函数相关的分配的空间就是一个vptr,是一个指针,也就是4字节,不变,要变是变在vtable.

 

比如如下一个类:

Class test1()  {};

      fun1()  {};

public Virtual   a(){println(“test1:a”);};

         public Virtual  b(int b){println(“test1:b”);};

int a;

}

Class test2 extends test1{

           fun2(){};

           public Virtual  bint b{this->b++;println(“test2:b”) } ;

           public  Vitrual  c(){println(“test2:c}”)}

int b;

}

Int main(){

           test1  a=new test2();

     a.b();

}

 

首先我们看看下:类test1,test2实例大小及其内存分配图:

test1的实例数据大小是:虚函数表指针(4+iaptr接口指针+int变量大小(4=12

test2的实例数据大小是:test1大小+其变量b大小=12+4=16

注意这是上面的提到的虚类继承,子类新增的虚函数不增加子类大小,只是在其虚函数表中体现。

大家注意上面的test1,test2的构造函数,析构函数,fun1,fun2都没加进去。

下面看下实例数据内存分布图:  

下面看下其对应的c语言伪代码;

1.       已实现的函数:

test1.b(Sturct test1 *this ,b){println(“test1”);};

test2.bSturct test2 *this ,b{println(“test2:a”);

test2.cSturct test2 *this{println(“test2:b}”)}

//上面是虚函数实现

test1.fun1(Sturct test1 *this,){}

test2.fun2(Sturct test2 *this){}

//这个是普通函数,就是上面的,只不过变了名字而已。

 

2.会生成类对应的结构体:

struct test1{

Stuct Test1_vtbl * vtbl;

Int a;

}test1;

Struct test2{

Stuct Test2_vtbl * vtbl;

Int a;

Int b;

}test2;

3.会生成两个虚函数表结构体:

Struct Test1_vtbl{

        (Void *)(test1 *this) a;

        (Void *)(test1 *this,int b) b;

     (Void *)(test1 *this) dispose;

}

Struct Test1_vtbl  test1_vtb1={test1.a,test1.b,test1.~test1};

//父类test1虚函数表。

Struct Test2_vtbl{

        (Void *)(test2 *this) a;

        (Void *)(test2 *this,int b) b;

     (Void *)(test2 *this) dispose;

(void *)(test2 *this) c;         

}

Struct Test2_vtbl  test2_vtb1={test1.a,test2.b,test2.~test2,test2.c};

//子类test2虚函数表。

     //注意虚函数a还是父类的a,因为其没有重载,而b重载了就是test2b,同时析构函数也是虚函数,是自动加的。

4.编译器自动生成的一些函数,构造函数,析构函数: 

test1.test1(Sturct test1 *this ){

this->vtbl=&test1_vtbl;

}

test1.~test1(Sturct test2 *this){

free(this);

…………..

};

 

test2.test2(Sturct test2 *this){

test1.test1(this);//调用父类的构造函数,这里也不考虑类型转化,其实编译器会帮我们做好。

this->vtbl=&test2_vtbl;

}

test2.~test2(Struct test2 *this){

test1.test1(this);

//……….

};

 

5.main函数对应的代码:

Int main(){

            test1  a=new test2();

     a.b();

}

对应c伪代码:

Int main(){

       Struct test2 *tmp=malloc(sizeof(Struct test2));               ………………1

       test2.test2(tmp);                                       ..…………….2

       Struct test1 *a=tmp; //不考虑转化错误,这个是编译器做的   ……………… 3

       a->vtbl->b(a,1);                                        ……………….4

    a->vtbl->dispose(a);                                     ……………….5

}

我们现在分析a->vtbl->b(a,1) 是如何调用到test2.b()函数的。

执行1后,虚函数表是空的,即为null;

执行2   --test2.test2(tmp)时会先执行test1.test1(this),这样首先tmpvtbl是指向

test1_vtbl的,后来又回到test2.test2执行了this->vtbl=&test2_vtbl;

就把test2_vtbl赋给tmp2,然后

Struct test1 *a=tmp;

这个只是指针赋值,可见a还是指向tmp的首地址。

所以a->vtbl->b()执行的是test2.b

同时a->vtbl->dispose();执行的也是test2的析构函数,为什么呢,因为尽管现在是一个test1对象,但是他本身是一个test2对象,所以结束时要调用其真正的析构函数。

上面也可知道,构造函数不能是虚函数,因为构造函数本身就是赋值虚函数表的,如果自己就是,析构函数必须是析构函数(这个当然还有其他方面考虑).

   面向对象语言里的类里面的函数一个重要特征是-----函数的参数被自动的添加了一个,这个参数就是大名鼎鼎的this参数,这个参数就是这个函数所属类的实例指针,可见this是实例,而类中的普通函数是公用的,只不过多了一个this参数来表明要执行哪个实例,当然this也可以不是参数,而是放到特定的寄存器,比如ecx。不过说到底,其实这个就可以看成一个参数,毕竟汇编级了,参数不是放在寄存器,就是放在堆栈,都是一样的效果,不过由于this指针使用频率高,放到寄存器是首选,因为寄存器的速度快啊,不过为了好说明,上面的例子,我还是以参数的形式好说明些。

  附带一些关键词的:

 比如显式带有thissuper关键词的,不受虚函数影响,比如

如果我在fun1()  {this->b();b();};

Int main(){

            test1  a=new test2();

a.       fun1();

 

}

 

其对应于fun1(tes1 this){test1.b();this->vtbl->b();};

如果一个x函数是虚函数,执行x才会调用this虚函数表中的x,如果是this.x,就直接绑定test.x函数了。

阅读(2643) | 评论(0) | 转发(0) |
0

上一篇:项目整理

下一篇:davinc架构--开始篇

给主人留下些什么吧!~~