今天是清明节假期的第二天,天气阴沉,无心于游玩,遂决定宅于实验室。
现在来说每天拜读一下大牛的博客已成生活中不可或缺之乐趣!但是俗话说的好:”光说不练假把式!“,今天拜读了浩哥的博客,感触颇多,于是就产生了本篇博文!
目的主要还是总结一下自己看到和想到的一些东西,以及遇到的一些问题,关于文中提出的问题受本人水平所限,可能略显拙劣,还望各位大牛莫要见笑,不吝赐教!
个人认为c++语言的出现是面向对象程序设计的一个里程碑,在今天看来面向对象对于大家来说都不陌生,几乎现在新近流行的每一种编程语言都包含有面向对象的特性。在面向对象中很重要的一个行为就是
继承
和
多态
,有关于他们的定义这里不再赘述!继承的确为我们的程序设计提供了很大的便利,最重要的一点就是它极大的提高了我们代码的复用性,我们可以将多个对象中共有的部分提取出来放在一个基类里面。但是c++中继承形式的多样性也极大增加代码的复杂性和不确定性,其中尤以多重继承为甚,它使得对象的关系不再是单纯的线性的继承关系而是随着代码功能的增加渐渐的演变成一种不可控的网状的关系,这样极大的降低了代码的可读性!也成为了c++经常为人所诟病的一个槽点,当然为了解决这个问题,也催生了另外一种面向对象的编程语言的产生,那就是现如今来说大名鼎鼎的JAVA语言!
当然任何事物都有其两面性,虽然多重继承存在极大的隐患但是无可否认它也为我们的设计带了极大的便利!成魔成佛还是最关键还是在于它的使用者,与此同时c++自身对于多重继承存在问题也引入了很多解决的办法,其中很重要的一个方面就是虚函数和虚继承!关于虚函数大家相比也都不陌生,它是面向对象的多态中不可或缺的一件神兵利器!为了使用这件神兵,我们的子类对象中都会存放着一张虚函数表(有点类似于“辟邪剑谱”),当我们用一个基类的指针来指向子类的对象,并用该指针进行函数调用时就会通过查阅这张虚函数表来实现动态绑定!
现在问题就来了:
1、这样的一张虚表它放在内存的什么地方呢?
2、在多重继承当中可能存在多张虚表他们的分布是怎么样的?
3、有关于虚继承中虚表的分布又是一个怎样的情况呢!
假如您很清楚上面的问题,那么你完全可以X掉这个网页了。假如您还有一点的困惑,那么准备好零食扫几眼,说不定会对你有点帮助!下面我们就开始吧!
还是先上例子:
- /*************************************************************************
- > File Name: memRank.cpp
- > Author: dongdaoxiang
- > Mail: dongdaoxiang@ncic.ac.cn
- > Virtual Table
- > Created Time: 2013年04月04日 星期四 16时57分13秒
- ************************************************************************/
-
- #include<iostream>
- using namespace std;
- class Parent
- {
- public:
- int age;
- Parent():age(10){}
- virtual void f(){cout << "Parent::f()" << endl;}
- virtual void g(){cout << "Parent::g()" << endl;}
- };
- class Child : public Parent
- {
- public:
- int num;
- Child():num(6){}
- virtual void f(){cout << "Child::f()" << endl;}
- virtual void h(){cout << "Child::h()" << endl;}
- virtual void gc(){cout << "Child::gc()" << endl;}
- };
- class GrandChild : public Child
- {
- public:
- int gpa;
- GrandChild() : gpa(4){}
- virtual void f(){cout << "GrandChild::f()" << endl;}
- virtual void ggc(){cout << "GrandChild::ggc()" << endl;}
- virtual void gc(){cout << "GrandChild::gc()" << endl;}
- };
-
- int main()
- {
- typedef void (*Func)(void);
- GrandChild temp;
- Parent *p = new GrandChild();
- int ** ptVtab = (int **)(&temp); //这里我们用一个二级指针指向虚表内存储的虚函数的入口地址,为什么用int呢?跟机器相关32位的机器一个指针
- int i = 0; //占用4个字节的存储空间!
- Func f;
- for( i= 0; (Func)ptVtab[0][i] != NULL; i++)//对象内存空间的起始位置
- {
- f = (Func)ptVtab[0][i];
- f();
- }
- cout << (int) ptVtab[1] << endl;//跳过四个字节之后的位置!
- cout << (int) ptVtab[2] << endl;
- cout << (int) ptVtab[3] << endl;
- p->f();
- return 0;
- }
大家可以先想象一下这个输出:
- mini@mini-ThinkPad-T420:~/unixtest$ ./a.out
- GrandChild::f()
- Parent::g()
- Child::h()
- GrandChild::gc()
- GrandChild::ggc()
- 10
- 6
- 4
- GrandChild::f()
纳尼,这都是什么玩意?不要着急我们先来看上述的代码,在代码中我们定义了一个基类Parent,其后一个线性的继承关系。当我们在main函数中定义了一个GrandChild的对象之后,取这个对象所在内存地址的起始位置会获得我们之前提到那张虚函数表!由于我们在GrandChild中重新定义了由父类定义来的f()函数,所以这样虚表的中其实位置指向了GrandChild中定义的f()的入口地址,所以当我们定义一个Parent的指针指向一个GrandChild的对象来调用f()函数时,他将从虚函数表中拉取GrandChild的入口地址,从而实现动态绑定!
虚表中函数的分布关系:
首先是由子类对象重新定义的虚函数的入口地址;
基类对象中其他虚函数的入口地址;
子类对象中其他虚函数的入口地址;
然后跳过这样虚表的地址之后:
首先是基类对象中定义的属性值
然后子类对象中定义的属性值
我们可以看到在这种线性的继承关系中子类对象中仅仅维护一张虚表!
我们再来看下面的一个例子!
我们会看到如下的执行结果:
- mini@mini-ThinkPad-T420:~/unixtest$ ./a.out
- Derive::f()
- Base1::g()
- Base1::h()
- Derive::g1()
- 10
- ===============================================
- Derive::f()
- Base2::g()
- Base2::h()
- 20
- ===============================================
- Derive::f()
- Base3::g()
- Base3::h()
- 30
- 100
为了便于说明我就拿来浩哥画的这样图:
图画已经很清晰了,当我们申请一个Derive的对象时,由于该对象存在三个父类于是在它的地址空间中就存放了三张虚表,但是我们会发现对于Drive本身的虚函数就放在第一张虚表内!
(
上图中有点小错误就是第三张虚表内的函数前缀改为Base3
)
分布情况如下:
base1的虚表;
继承自base1的属性值;
base2的虚表;
继承自base2的属性值;
base3的虚表;
继承自base3的属性值;
子类自身的属性值;
好了这是这种情况,别着急我们接着向下看下面的例子:
根据上面的例子大家可以尽情的猜测一下本例的结果?看看自己有没有猜对:
- mini@mini-ThinkPad-T420:~/unixtest$ ./a.out
- +++++++++++++++++++B1_vTable+++++++++++++++++
- D::f()
- B::Bf()
- D::f1()
- B1::Bf1()
- D::f2()
- D::Df()
- 0
- B
- 11
- 1
- ===================B2_vTable=================
- D::f()
- B::Bf()
- D::f2()
- B2::Bf2()
- 0
- B
- 12
- 2
- 100
- D
这样的执行结果说明了什么问题:
1、首先子类继承了拥有同一个基类的父类时,他将保有两个父类继承来的两份基类的数据副本!
2、类空间内部采取了对齐,char的数据成员其实占用了int型的数据空间!
内存空间分布:
1、B1的虚函数表空间(其中存放了重定义的基类的虚函数、
基类的虚函数、自己内部的虚函数的入口地址和来自父类的B1虚函数地址)
2、基类的数据成员
3、父类的数据成员
4、B2的虚函数表空间(自己重定义的虚函数、基类的虚函数、来自父类B2的虚函数地址)
5、基类的数据成员
6、父类的数据成员
7、自己的数据成员(放在了整个数据空间的末尾
)
画成一张图如下(浩哥出品)
如果我们采用虚继承结果会是怎么样的呢?
结果又变成了这样:
- mini@mini-ThinkPad-T420:~/unixtest$ ./a.out
- +++++++++++++++++++B1_vTable+++++++++++++++++
- D::f()
- D::f1()
- B1::Bf1()
- D::f2()
- D::Df()
- 11
- 1
- ===================B2_vTable=================
- D::f()
- D::f2()
- B2::Bf2()
- 12
- 2
- 100
- D
- ------------------B_vTable-------------------
- D::f()
- B::Bf()
- 0
- B
我想从结果上来看大家已经很清楚了
假如我们在继承的时候采用虚继承的方式,就会解决在一般继承之中,存在的基类多副本的问题,但是虚继承中的子类多了一张基类的虚函数表,由次其实可以衍生出很多比较有意思的问题,例如:在一般的多重继承和虚继承的情况下子类的内存空间有多大?我们在回答诸如此类的问题的时候,就要考虑到基类多副本和虚表的地址占用的空间大小了?
在虚继承的情况下内存空间分布:
1、B1的虚函数表(子类重定义的基类的虚函数、重定义的B1的虚函数、子类自己的虚函数)
2、继承自B1的数据成员
3、B2的虚函数表(子类重定义的基类的数据成员、重定义的B2的虚函数)
4、继承自B2的数据成员
5、子类自己的数据成员
6、基类的虚函数表(子类重定义的虚函数、基类的虚函数)
7、基类的数据成员
wow,终于把这些字码完了,不知道你有没有耐心的看完哦!
最后附上浩的博客原文的地址:
http://blog.csdn.net/haoel/article/details/3081328