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

news/2024/7/5 2:23:14

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

  (2012-05-08 23:36:58)
http://blog.sina.com.cn/s/blog_62690ab901013cql.html
虚函数是面向对象编程语言里一个很重要的机制,下面我们以一个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函数了。

本篇文章来源于:开发学院 http://edu.codepub.com   原文链接:http://edu.codepub.com/2010/0518/22776.php


http://www.niftyadmin.cn/n/4645243.html

相关文章

我的家装日记15

终于快到最后阶段了,等欧亚达的家具到位,就可以铺地板,然后就可以进家电,搬家了。 因为是订做的家具,所以时间比较长,4月份订货的,要6月中旬才能做好。盼星星,盼月亮,终于…

继承与动态内存分配——《c++primer plus第五版》

2008-04-09 00:25 继承与动态内存分配——《cprimer plus第五版》 http://hi.baidu.com/wangxiaoliblog/item/e334fcba483c95d684dd7992 继承是怎样与动态内存分配进行互动的呢?例如,如果基类使用动态内存分配,并重新定义赋值和复制构造函数,…

今天去交行还款花了两个小时

昨天运气不好,晚上跑去交通银行还信用卡账单,发现能存款的机器全部宕机,无奈之下今天早上去了,昨晚玩了一个通宵的游戏,早上起来后感觉有点疲惫,加上房东阿姨是一个难缠的主,相当的耗费精力&…

c++ 内存分布(一)

c 内存分布(一)2011-08-31 10:58http://hi.baidu.com/kuhntoria/blog/item/d92ec865a54242c48cb10d4e.html(1)单继承,无虚函数覆盖,无成员变量,无虚继承...1 (2继承无虚函数覆盖 有成员变量,无虚继承...3 (3)单继承有虚…

c++内存分布(二)--虚函数和虚继承

c内存分布(二)--虚函数和虚继承http://hi.baidu.com/kuhntoria/blog/item/5872c1fe9bfd5d0d6d22eb22.html2011-08-31 16:20一.多重继承 首先我们先来考虑一个很简单(non-virtual)的多重继承。看看下面这个C类层次结构。 1 class Top 2 { 3 pu…

关于内核符号表

关于内核符号表 http://soft-app.iteye.com/blog/920312 在编写驱动的过程中,常会使用到EXPORT_SYMBOL宏来将定义的函数名导出到内核符号表。以前只是简单的知道如果一个模块中定义的函数要提供给其他模块调用,就必须进行导出。这段时间在编译单个模块…

内核符号表详解

内核符号表详解 http://hi.baidu.com/apollon2010/blog/item/ecbe1d5a3bd2ec8a800a18dc.html 关键词: Kernel Symbol Table、/proc/ksyms、system.map、Oops、LKM 这应该是一个很基本的内核概念,和模块、系统调用等一样基础,但牵涉的东…

如何读取被禁用的网卡信息

http://topic.csdn.net/u/20080310/19/5fa9b49d-c7f5-42be-986f-4cb46fb4e0b0.html?2079547956 比如,有两张网卡,一张启用,一张禁用,如何获取被禁用的网卡的信息,如网卡MAC,网卡名称等。GetAdaptersInfo…