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函数了。