文章详情

  • 游戏榜单
  • 软件榜单
关闭导航
热搜榜
热门下载
热门标签
php爱好者> php文档>C++继承多态下的内存分布

C++继承多态下的内存分布

时间:2010-05-15  来源:Codeplayer

-------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处: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  b(int 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.b(Sturct test2 *this ,b){println(“test2:a”);

test2.c(Sturct 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重载了就是test2的b了,同时析构函数也是虚函数,是自动加的。

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),这样首先tmp的vtbl是指向

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指针使用 频率高,放到寄存器是首选,因为寄存器的速度快啊,不过为了好说明,上面的例子,我还是以参数的形式好说明些。

  附带一些关键词的:

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

如果我在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函数了。
相关阅读 更多 +
排行榜 更多 +
辰域智控app

辰域智控app

系统工具 下载
网医联盟app

网医联盟app

运动健身 下载
汇丰汇选App

汇丰汇选App

金融理财 下载