zoukankan      html  css  js  c++  java
  • Data 语义学(2)

    四、"继承"与 Data Member

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

      先来看个例子  

     1 #include <iostream>
     2 
     3 using namespace std;
     4 
     5 class A
     6 {
     7 public:
     8     //
     9 private:
    10     int val;
    11     char c1;
    12     char c2;
    13     char c3;
    14 };
    15 
    16 int main()
    17 {
    18     cout << sizeof(A) << endl;
    19     return 0;
    20 }

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

        a. val 占用 4 bytes。

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

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

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

      

     1 #include <iostream>
     2 
     3 using namespace std;
     4 
     5 class A
     6 {
     7 public:
     8     //
     9 private:
    10     int val;
    11     char c1;
    12 };
    13 
    14 class B: public A
    15 {
    16 public:
    17     //
    18 private:
    19     char c2;
    20 };
    21 
    22 class C: public B
    23 {
    24 public:
    25     //
    26 private:
    27     char c3;
    28 };
    29 
    30 int main()
    31 {
    32     cout << sizeof(C) << endl;
    33     return 0;
    34 }

      这下输出结果是多少呢?可以肯定的是,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。

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

     1 class A 
     2 {
     3 public:
     4     int num;
     5     virtual void fun(){};  
     6 }
     7 
     8 class B
     9 {
    10 public:
    11     int num;
    12     int num1;
    13     virtual void fun(){}
    14 }

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

       也有可能是这样   

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

      该写如下:

      

     1 class A 
     2 {
     3 public:
     4     int num;
     5     virtual void fun(){};  
     6 }
     7 
     8 class B: public A
     9 {
    10 public:
    11     int num1;
    12     virtual void fun(){}
    13 }

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

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

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

      (3). 多重继承

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

     1 class base1
     2 {
     3 public:
     4     // something
     5 protected:
     6     float fb1;
     7 };
     8 
     9 class dbase1: public base
    10 {
    11 public :
    12     // something
    13 protected:
    14     float fdb1;
    15 };
    16 
    17 class base2
    18 {
    19 public:
    20     // something
    21 protected:
    22     float fb2;
    23 };
    24 
    25 class Topclass :public dbase1,public base2
    26 {
    27 public:
    28     // something
    29 private:
    30     float muble;
    31 };

      他们的关系图如下

        图 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 那这些东西到底是什么样子呢?今天将详细的展示给你们看:

     1 #include <iostream>
     2 
     3 using namespace std;
     4 
     5 class A
     6 {
     7 public:
     8     //
     9     virtual void foo(){};
    10 private:
    11     int val;
    12 };
    13 
    14 class B:  public A
    15 {
    16 public:
    17     //virtual void foo(){}
    18 private:
    19 
    20 };
    21 
    22 
    23 int main()
    24 {
    25     cout << sizeof(A) << endl;
    26     cout << sizeof(B) << endl;
    27     return 0;
    28 }

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

     1 #include <iostream>
     2 
     3 using namespace std;
     4 
     5 class A
     6 {
     7 public:
     8     //
     9     virtual void foo(){};
    10 private:
    11     int val;
    12 };
    13 
    14 class B: virtual public A  //这里将继承改成了虚拟继承
    15 {
    16 public:
    17     //virtual void foo(){}
    18 private:
    19 
    20 };
    21 
    22 
    23 int main()
    24 {
    25     cout << sizeof(A) << endl;
    26     cout << sizeof(B) << endl;
    27     return 0;
    28 }

    这个代码将输出什么呢?结果却变成了 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!

  • 相关阅读:
    Java-对象数组排序
    aoj 0118 Property Distribution
    poj 3009 Curling 2.0
    poj 1979 Red and Black
    AtCoder Regular Contest E
    AtCoder Beginner Contest 102
    AtCoder Beginner Contest 104
    SoundHound Inc. Programming Contest 2018
    poj 3233 Matrix Power Series
    poj 3734 Blocks
  • 原文地址:https://www.cnblogs.com/zhuwbox/p/3426127.html
Copyright © 2011-2022 走看看