四、"继承"与 Data Member

  (1). 只有继承没有多态

  先来看个例子  

 #include <iostream>

 using namespace std;

 class A
{
public:
//
private:
int val;
char c1;
char c2;
char c3;
}; int main()
{
cout << sizeof(A) << endl;
return ;
}

  输出的结果是 8 ,这个比较容易理解:

    a. val 占用 4 bytes。

    b. c1,c2,c3各占 1 byte,3 个 bytes。

    c. 边界调整(alignment)---调整到 word 需要 1 byte

  一共 8 个!那么在看下面这个例子:

  

 #include <iostream>

 using namespace std;

 class A
{
public:
//
private:
int val;
char c1;
}; class B: public A
{
public:
//
private:
char c2;
}; class C: public B
{
public:
//
private:
char c3;
}; int main()
{
cout << sizeof(C) << endl;
return ;
}

  这下输出结果是多少呢?可以肯定的是,C中也含有 val、c1、c2 和  c3,那么输出结果是不是也是 8呢?当然不是,是 16 !为什么呢?解释如下:

  C++ 规定出现在 derived class 中的 ”base class subobject“ 有其完整原样性。我们从下至上看看各个类在内存中的布局!

  class A 中含有两个变量 val 和 c1,其中 val 占有 4 个 bytes,c1 占有 1 个byte加起来 5 个 bytes。再加上边界调整填补 3 bte,实际会占用 8 byte。

  class B 继承了 class A 并增加了一个自己的 nonstatic data member c2,类型时 char,占有 1 byte,轻率的程序员会以为:c2 会 c1 放到一起构成 2 bytes,这样内存补齐只需补 2 bytes,class B的大小应该还是 8 bytes。然而,class B 的 c2 是被放在 class A 所填补的 3 bytes 的后面,此时 class B 的大小是 9 bytes ,在进行边界调整填补 3 bytes,实际会占用 12 bytes。同样 class C 的大小是 16 bytes ,class B 的大小 + c3 + 边界调整!下图会让你一目了然:

   图 1

  很多人会认为这种做法很愚蠢,在 class B 继承 A 的时候,将 c1 和 c2 放在一起,填补 2 bytes,在 class C 继承 class B 的时候,将 c1、c2 和 c3放到一起,填补 1 byte,这样 class A、class B、class C 的大小都是 8 bytes,比之前那样省很多空间!!!事实上是如此吗?这种做法会破坏C++中规定的 “出现在 derived class 中的 ”base class subobject“ 有其完整原样性”,会是一些代码导致致命的错误,不信你看:

  加入我们声明以下一组指针:

    B *pb;

    A *pa1,*pa2;// 这两个指针可以用来指向 class A,B,C。

  如果执行操作:

    *pa1 = *pa2;

  应该执行一个默认的“memberwise”复制操作(复制一个个的 members)对象是值得 class A 的那部分哦。如果 pa1指向一个 class B 或 class C ,那么就会将 pa2 所指的内容中 class A 的部分复制给 class B 或 class C 的 class A 的部分!如果将 A、B 和 C 的c1、c2 和 c3 捆版到一起,去填补空间,上述那些语义就没办法保留了!由于捆绑而导致的内存复制出现错误,请看下图,会更容易理解:

  

  所以知道,C++设计者们为什么宁愿浪费空间而不那么做的原因了吧?

  (2) 加上多态

  加上多态之后会使很多操作起来更有方便性,使动作在运行期决定。这使得 class 更加完美,但我们必须维持付出代价:

    a. 导入一个相关的 virtual table,用来存放他 class 所声明的每一个 virtual function 的地址。这个 table 元素的个数 = virtual function 的数目 + 1 或 2(用以支持 runtime identification)

    b. 在每一个 class object 中导入一个 vptr ,提供执行期的连接,使每个 object 能够找到相应的 virtual table。

    c. 加强 destructor,使它能够销毁那个被安插进去的 vptr。

  那这个指针到底放在哪呢?看如下代码: 

 class A
{
public:
int num;
virtual void fun(){};
} class B
{
public:
int num;
int num1;
virtual void fun(){}
}

  这时 class A 和 class B 的布局可能是这样:

   也有可能是这样   

  如果我对 class A 和 class B 进行如下该写,那么 A 和 B 的布局又会是什么样子呢?

  该写如下:

  

  class A
{
public:
int num;
virtual void fun(){};
} class B: public A
{
public:
int num1;
virtual void fun(){}
}

  那么 class A 和 class B 的布局会如下:

    图2 还有可能。。。另外一种就不画了,只是把vptr 移到 num 上去罢了!

  从这个图中可以发现什么?对无论继承多少次 vptr 在所有的 base class 或者 derived class 中都只有一份!并且位置都相同,但是他们(vptr)指向的 virtual table 不相同哦!都指向内存中各自的 virual table 调用时分别从各自的 virtual table 中找 virtual functions 的地址进行调用!实现了多态的机制哦!

  (3). 多重继承

  还是先来看几个类的定义:    

 class base1
{
public:
// something
protected:
float fb1;
}; class dbase1: public base
{
public :
// something
protected:
float fdb1;
}; class base2
{
public:
// something
protected:
float fb2;
}; class Topclass :public dbase1,public base2
{
public:
// something
private:
float muble;
};

  他们的关系图如下

    图 3

  他们在内存中的布局图如下(假设他们都有虚函数哈,给他们画了 vptr 指针):

   图4

  由他们的内存布局图可知,在class Topclass 中取 class base1 和 class dbase1 的时候可以直接取到,而取class base2就必需要做相应的转化了。请看下面的例子:

    Toplass tc;

    base1   *b1;

    dbase1 *db;

    base2  *b2;

  如果是 b1 = &tc或 db = &tc都是和单继承一样的;但如果是 b2 = &tc的操作就需要相应的转化咯,如下:

    pv = ((Topclass *)((char *) &tc)) + sizeof( dbase2);

  而如果将 tc定义成 Topclass *tc;呢?那么相应的操作是否是 pv = ((Topclass *)((char *) tc)) + sizeof( dbase2);呢?只能说你这样对了一半,应该是这样:

    pv = tc ? ((Topclass *)((char *) &tc)) + sizeof( dbase2) : 0;

  因为 tc 有可能是空指针哦。至于 reference 就不会这样了,因为不可能有一个空的引用!

  (4). 虚拟继承

  //一下可能有误,回头看

  之前我们就对虚拟继承的内存比较好奇,而且在我的博文中未讲,我们只知道会生成一个指针和一个 virtual base classs table 那这些东西到底是什么样子呢?今天将详细的展示给你们看:

 #include <iostream>

 using namespace std;

 class A
{
public:
//
virtual void foo(){};
private:
int val;
}; class B: public A
{
public:
//virtual void foo(){}
private: }; int main()
{
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
return ;
}

  以上是一个一般继承的代码,程序输出的结果大家也知道是什么了,是 8 和 8,他们内存布局图也就是我们"(2)加上多态"图 2 所画的那样。那么我稍作如下修改:

 #include <iostream>

 using namespace std;

 class A
{
public:
//
virtual void foo(){};
private:
int val;
}; class B: virtual public A  //这里将继承改成了虚拟继承
{
public:
//virtual void foo(){}
private: }; int main()
{
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
return ;
}

这个代码将输出什么呢?结果却变成了 8 和 12 啦,为什么呢?我画一下虚拟继承后的他们的内存布局:

   图 5

  你将这幅图和上面"(2)加上多态"那里画的图 2 对比一下,会发现这里会多了一个 _vptr_B 指针啦(我将上面的 _vptr 后面 加上了 _A 或 _B 以区别他们,这个没影响)。这也就是为什么上面第二个代码输出的结果是 8 和 12 了。那为什么要这么做呢?以下是我通过 vc 调试和书上所述的方式总结出来的:

  我们之前都有说过 virtual base class 是虚基类它在 derived class 中访问并不能像其他,而是另外安插了一个指针,该指针指向一个 virtual base class table 这个表放的是 virtual base class 在 derived class 中的 offset !这个指针就是上面所说的 _vptr_B。(在我的 VC 下)这个 _vptr_B 有两个作用:第一、用来指向 class B 的 virtual function table 。第二、用来指向 class B 的 virtual base class。有人说一个指针怎么会指向两个表呢?请看下图:

   图 6

  看上图明白了吧?virtual function table 和 virtual base class table 首地址是同一个。两个表合到一起了,我们就叫 virtual function table ,表的首地址在分界处,当取正索引时,取的是 VFT slots ,当取负索引时,取的是 VBC offset。这样就可以方便的访问虚基类和实现多态了!VBC offset 是指virtual base class 在 derived class中的偏移量哦!---不过大家注意了,这只是 VC 下的一个用来共享虚基类的一种方法,别的编译器可能不是采用这种方法。但是都大同小异了,都是为了能够方便的在 derived class 中访问共享的 virtual base class!

Data 语义学(2)的更多相关文章

  1. Data 语义学(1)

    一.Data Member 的绑定(The binding of Data Member) extern float x; class Point3d { public: Point3d( float ...

  2. C++对象模型(五):The Semantics of Data Data语义学

    本文是<Inside the C++ Object Model>第三章的读书笔记.主要讨论C++ data member的内存布局.这里的data member 包含了class有虚函数时 ...

  3. 【C++对象模型】第三章 Data语义学

    1. Data Member 的布局 同一个Access Section(private, public等)中,data member的顺序按照声明顺序排列,但是没有规定需要连续排序.同时编译器可能会 ...

  4. 【深入探索c++对象模型】data语义学二

    单一继承中,base class 和derived class的对象都是从相同的地址开始,其间差异只在于derived class比较大,用以容纳自己的nonstatic members. 若vptr ...

  5. 【深度探索C++对象模型】data语义学

    class X{}; class Y :public virtual X{}; class Z :public virtual X{}; class A :public Y, public Z{}; ...

  6. 深入探索C++对象模型(三)

    Data 语义学 一个class的data members,一般而言,可以表现这个class在程序执行时的某种状态.Nonstatic data members放置的是"个别的class o ...

  7. Linux Debugging(四): 使用GDB来理解C++ 对象的内存布局(多重继承,虚继承)

    前一段时间再次拜读<Inside the C++ Object Model> 深入探索C++对象模型,有了进一步的理解,因此我也写了四篇博文算是读书笔记: Program Transfor ...

  8. CYQ.Data、ASP.NET Aries 百家企业使用名单

    如果您或您所在的公司正在使用此框架,请联系左侧的扣扣,告知我信息,我将为您添加链接: 以下内容为已反馈的用户,(收集始于:2016-08-08),仅展示99家: 序号 企业名称 企业网址 备注 1 山 ...

  9. 终于等到你:CYQ.Data V5系列 (ORM数据层)最新版本开源了

    前言: 不要问我框架为什么从收费授权转到免费开源,人生没有那么多为什么,这些年我开源的东西并不少,虽然这个是最核心的,看淡了就也没什么了. 群里的网友:太平说: 记得一年前你开源另一个项目的时候我就说 ...

随机推荐

  1. Understanding JTS--reference

    Part I-An introduction to transactions If you look at any introductory article or book on J2EE, you' ...

  2. iOS--为视图添加阴影

    iOS–为视图添加阴影 情况一:视图添加圆角,在添加阴影 //阴影视图 self.viewShadow = [[UIView alloc]initWithFrame:CGRectMake(0, 0, ...

  3. css考核点整理(六)-水平居中定位的几种方式

     定宽    text-align: center 父容器position:relative:子容器 position:absolute;left:50%; margin-left: 宽度/2 .Ce ...

  4. javascript操作json方法

    /*新增json的对象属性*/ var json = {}; json["subjectName"] = "ddd"; json.teacherlist = & ...

  5. Oracle11g数据库导入到oracle10g的解决方法

    我想有很多人在工作和学习中遇到这样的一个问题,Oracle数据库服务器版本和本机版本不一致问题,你的本机要是比服务器的版本要高的话还好,如果你本机是10g服务器是11g的话,从11g导出来的数据库是导 ...

  6. 转 sqlserver字段描述相关操作sql

    可以自己查询系统表: SELECT o.name AS tableName, c.name AS columnName, p.[value] AS Description FROM sysproper ...

  7. HBase0.98.1 通过协调器进行表的行数统计

    1. 启用表aggregation,只对特定的表生效.通过HBase Shell 来实现 (1)disable指定表.hbase> disable 'student' (2)添加aggregat ...

  8. opencar二次开发常用代码

    <?php //创建Registry对象 //注册所有公共类 //创建Front类对象,作为请求分发器(Dispatcher) //根据用户请求(url)创建控制器对象及其动作. // 在Fro ...

  9. TOM大师脚本-show space 多个版本,谢谢大牛们

    示例一 该脚本需区分 对象的管理方式是 自动还是 手动, 对手动管理方式 的表显示很全面 SQL> exec show_space_old('MAN_TAB','DEV','TABLE'); F ...

  10. wpf 自定义RadioButton控件样式

    实现的效果为: 我感觉来自定义RadioButton样式和定义button空间的样式差不多,只是类型不同而已. 接下来分析一下样式代码: <!--自定义单选按钮样式-->        & ...