四、"继承"与 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. IOS—通过ChildViewController实现view的切换

    IOS-通过ChildViewController实现view的切换 在以前,一个UIViewController的View可能有很多小的子view.这些子view很多时候被盖在最后,我们在最外层Vi ...

  2. ARCGIS二维三维放大缩小

    private void ULZoomPan() { ESRI.ArcGIS.SystemUI.ICommand com = new ControlsGlobeFixedZoomOutCommand( ...

  3. Android Activity横竖屏转换的生命周期

    新创建一个Activity,用来此次测试. 先贴代码 package com.hugo.apj.activitylifetest; import android.support.v7.app.AppC ...

  4. Image路径问题

    BitmapImage image = new BitmapImage(new Uri("/Pictures/log.png", UriKind.Relative)); evaIm ...

  5. Linux sed命令常用方法

    sed也成stream editor,流编辑器,是Linux上常用的文本处理工具. 通用格式:sed  行范围  模式/RegExp/  文件 模式: d 删除 p 打印符合条件的行 a \strin ...

  6. Maven Integration for Eclipse 正确地址

    m2eclipse has moved from sonatype to eclipse. The correct update site is http://download.eclipse.org ...

  7. C#超时处理(转载)

    /// <summary>    /// 超时处理    ///    ///    /// </summary>    public class TimeoutChecker ...

  8. 开通博客第一天 (先发一些android(java)常见异常信息

    常见异常: java.lang.AbstractMethodError抽象方法错误.当应用试图调用抽象方法时抛出. java.lang.AssertionError断言错.用来指示一个断言失败的情况. ...

  9. Delphi动态创建组件,并释放内存

    开发所用delphi版本是xe2,效果图如下: 代码如下: ---------------------------------------------------------------------- ...

  10. VirtualBox 中的UBUNTU和java环境的配置以及各种常用说明

    本来是要ubuntu下学习C++的,但是又懒的用高手们推荐的各种记事本级的操作,所以要用IDE.(我用IDE我自豪,人类的进化就是建立在工具的使用这个基础之上的.)我选用了oracle的netbean ...