对象模型学习总结 (二) . 关于继承和多态

前言

  接着上文,现在谈一谈继承和多态。本来是想把继承和多态分开成两篇文章来写的。但是一想,多态的实现离不开继承。索性,把这两个面向对象比较重要的特性放在一起讲了。So,这篇文章会很长很长。OK,闲话少扯,进入正题。

关于继承  

  先看一段来自官方对继承的解释。

  继承(Inheritance)是指子类(subclass)继承超类型(superclass),会自动取得超类型除私有特质外的全部特质,同一类型的所有实例都会自动有该类型的全部特质,做到代码再用(reuse)。C++ 只支持类型构成式继承,虽然同一类型的所有实例都有该类型的全部特质,但是实例能够共享的实例成员只限成员函数,类型的任何实例数据成员乃每个实例独立一份,因此对象间并不能共享状态,除非特质为参考类型的属性,或使用指针来间接共享。C++ 同时支持公有继承(public inheritance)、使用保护继承(protected inheritance)、私有继承(private inheritance)。其中最常用的是公有继承,a... is a关系代表了在完全使用公有继承的对象类型之间的层次结构体系(hierarchy)。

  C++支持多重继承(multiple inheritanceMI)。多重继承(multiple inheritanceMI)的优缺点一直广为用户所争议,许多语言并不支持多重继承,而改以单一继承和接口继承(interface inheritance),而另一些语言改以单一继承和混入(mixin)。C++支持虚拟继承(Virtual Inheritance)用以解决多重继承的菱形问题(diamond problem)。(来自:wikipedia

理解继承

  如果您和我一样,是先学C语言然后再去学C++,那么在学C++的过程中难免会用C语言的思考方式去理解C++。先看一段我最初用C语言去理解C++的例子:

typedef struct base{
 //接口函数
     void (*base_construct)(struct base *this);
     void (*base_destruct)(struct base *this);
     void (*set)(struct base *this);
     void (*get)(struct base *this);
 
 //属性
     #define public_base 
     int base_val1;      
     char base_byte1;
     public_base}base;
 
typedef struct derived{ public_base //继承base的属性和方法
 
 //接口函数
     void (*derived_construct)(struct derived *this);
     void (*derived_destruct)(struct derived *this);
 
 //属性
     #define public_derived  
     int derived_val1;       
     char derived_byte1;
     public_derived
}derived;

  如果不考虑内存对齐和struct 中多出的函数指针,这和没有无多态的C++单一继承基本上没有太大的区别了。网上也有例子在使用C语言模拟C++ 继承机制的时候使用类似C++中组合的方法。不过我觉得组合是组合,继承是继承,不能混为一谈。(个人理解)

  上面还有不一样的地方就是,在C++base class 加上private 的成员在derived class中没办法访问,只能通过protectedpublic的接口间接访问private的成员,因为C语言没有类似access level这种东西,所以这里也没办法模拟出一个跟C++ class 很像的struct出来。而且在上面的例子中,derived并不能拥有一个和base名字一样的函数指针名称或数据成员名称。

 

无多态继承

无多态单一继承

内存对齐(关于内存对齐在继承中无论是无多态继承还是多态继承都是差不多的)

以下面的代码为例子:

class base{
 public:
     void set(){
         cout << "base set" << endl;
     }
     void get(){
         cout << "base get" << endl;
     }
 private:
     int base_val1;
     char base_byte1;
     char base_byte2;
     char base_byte3;
 };
 
 class derived_mid: public base{
 private:
     char derived_byte1;
 };
 
 class derived_down: public derived_mid{
 private:
     char derived_down_byte1;
 };

  上面的例子base class 中有四个数据成员, derived_min class 继承自base然后再添加一个char 类型的数据成员, derived_down为最底层的class,继承自derived_mid, 也多一个数据成员。按照《深入探索C++对象模型》的说法,这个三个类的内存分布情况大概是这样(图画得不标准见谅):

 

  在这里,我使用了两个编译器来测试验证这个结果。也许没有这个必要,但我还是想尽量弄清这两个编译器的区别。

  先看VS2012 + Win7 SP1 64Bit测试的结果,结果符合上面的要求:

  而在UBUNTU + GCC 4.7.1的环境测试下却得到一个诧异的结果。从下面截图的运行结果可以看到GCC没有对继承进行内存对齐的处理。

  撇开运行结果的差异不谈,先说说如果没有进行内存对齐的话会怎么样?如果继承时没有内存对齐,pdm->derived_byte1这个成员将得到base object 中被内存对齐的值,而这个值是什么,由编译器处理时决定的。因此这个值是一个不稳定的值(GCC 中内存对齐的值置为0)。如果pd所指的成员derivd_byte1有一个值,而且希望从pb所指对象中取得base_val1base_byte1base_byte2base_byte3的值的同时保持原有的值,那么GCC因为没有内存对齐的原因derivd_byte1将被赋与base object中内存对齐部分的值。简单点来说如下图所示(图画的不准请见谅)

  举个例子:

class base{
public:
  base ()
  {
    base_byte1 = 'b';
    base_byte2 = 'b';
    base_byte3 = 'b';
    base_val1 = 11;
  }
  void show()
  {
      printf("base_val1 is:%d
", base_val1);
      printf("base_byte1 is:%d
", base_byte1);
      printf("base_byte2 is:%d
", base_byte2);
      printf("base_byte3 is:%d
", base_byte3);
  }
private:
  int base_val1;
  char base_byte1;
  char base_byte2;
  char base_byte3;
};

class derived: public base{
public:
    derived()
    {
        derived_byte1 = 'd';
    }
    void show(){
        base::show();
        printf("derived_byte1 is:%d
", derived_byte1);
    }
private:
  char derived_byte1;
};

class derived_down:public derived{
public:
    char derived_down_byte1;
};

int main (void){
    cout << sizeof(base) <<endl;
    cout << sizeof(derived) << endl;
    cout << sizeof(derived_down) << endl;
    base *pb = new base;

    derived *pd = new derived;
    memcpy(pd, pb, sizeof(derived));
    pd->show();
    return 0;
}

   运行结果为(UBUNTU + GCC 4.7.1)

   在测试内存对齐的过程中还遇到过一个小插曲,顺便也提一下。看例子:

class base{
public:

public:
  int base_val1;
  char base_byte1;
  char base_byte2;
  char base_byte3;
};

class derived: public base{
public:
public:
  char derived_byte1;
};

class derived_down:public derived{
public:
    char derived_down_byte1;
};
int main (void) { cout << sizeof(base) <<endl; cout << sizeof(derived) << endl; cout << sizeof(derived_down) << endl; return 0; }

  运行结果:

  在这里我只是把base classprivate 的数据成员改为publicprivate没办法触发内存对齐),去掉base的构造函数(不去掉base的构造函数还是没办法触发内存对齐)。但是惊讶地发现继承后的derived class内存被对齐了。但是derived_down却没有对齐。

  而在VS2012中,因为有了内存对齐,所以但发生copy 的时候,base内存对齐部分也同样被copyderived_mid的内存对齐部分,所以derived_midderived_mid成员不会出现members object获得不确定的值。在这一点上,VS还是比较安全的。(个人理解)

  疑惑点在这里,我不明白GCC为什么在继承的时候不加上内存对齐?这样做除了节省内存空间外又有什么好处?是GCC太信任使用GCC的人?

构造函数

   "C++ default constructors...在需要的时候被编译器产生出来。", "default constrcutors 的构造的两种情况一个是程序需要,一个是编译器需要。" (摘自《深入探索C++对象模型》)

  这里主要讨论"带有 default constructorbase class"这种情况,内容也比较简单。看代码:

class base{
public:
      base()
      {
          cout << "I'm base construct" << endl;
      }
};
 
class derived: public base{
public:
private:
};

  当一个base class带有构造函数的时候,编译器将会在每一个继承了base class derived class 的所有构造函数中安插类似下面的代码(以无参构造函数为例)

derived::derived()
{
    base::base();      
}

  如果一个derived class中并没有构造函数, 那么编译器将帮你合成一个不带参的default constructor。(并不是任何情况编译器都会帮你合成一个构造函数,编译器只会在恰当的时候帮你合成构造函数)

无多态多继承

内存对齐

  直接看例子:

class base_left{
public:
    int a;
    char byte1;
};

class base_right{
public:
    int a;
    char byte2;
};

class derived: public base_left, public base_right{
public:
    char byte3;
};

int main(void)
{  
    cout << sizeof(base_left) << endl;
    cout << sizeof(base_right) << endl;
    cout << sizeof(derived) << endl;
    return 0;
}

  运行结果(UBUNTU + GCC 4.7.1)

 

  从中可以看到access levelpublic的,GCC将开启内存对齐。而VS好像默认都会开启内存对齐。同上面小节一样,我还是搞不明白GCC为什么会有这种奇怪的做法。

this指针的调整

  单一继承的时候并不需要this指针调整,因为为了兼容CstructC++编译器的做法大都会将derived object中的base subobject 放在内存的最上面。"Members are laid out in their declaration order, subject to implementation defined alignment padding."C++ under the Hood

  还是看例子:

class base_left{
public:
    base_left(){
        base_left_val = 11;
    }
    void show ()
    {
        cout << "base_left_val is:" << base_left_val << endl;
    }
protected:
    int base_left_val;
};

class base_right{
public:
    base_right(){
        base_right_val = 22;
    }
    void show ()
    {
        cout << "base_right_val is:" << base_right_val << endl;
    }
protected:
    int base_right_val;
};

class derived: public base_left, public base_right{
public:
    derived(){
        base_left_val = 33;
        base_right_val = 33;
        derived_val = 33;
    }
    void show ()
    {
        base_left::show();
        base_right::show();
        cout << "derived_val is:" << derived_val << endl;
    }
private:
    int derived_val;
};

int main(void)
{  
    base_left *pbl;
    base_right *pbr;
    derived *pd = new derived;
    pbl = pd;
    pbr = pd;
    pbl->show();
    pbr->show();
    return 0;
}

  上面代码中pblpbr 都指向了derived object, 按照上面的代码理论上来说两者都应该指向derived object的起始地址。但实际C++的编译器不是这么处理,里面有着编译器的介入,它介入的伪代码大概如下:

pbl = pd;
pbr = pb+sizeof(base_left);

  pblpbr实际上所指的是:

  通过打印指针也可以很清晰地看出来多继承时this指针的调整,多继承效率低于单继承的原因之一就是因为编译器须要对this做调整(VS2012 + Win7 SP1 64Bit)  

  如果我把pd的地址类型强制转换为别样的地址类型,那么this指针的调整会不会不被编译器实施呢?

 

  可以看到经过转换为一个char *指针再复制给pbr,VS编译器还是会帮你贴心的加上this指针的调整。这一点C++的结果也是一样,就不贴出来了。

构造函数

  多继承的构造函数也和上面单继承情况差不多。当一个base class带有构造函数的时候,编译器将会在每一个继承了base class derived class 的所有构造函数中安插初始化上一层base class的构造函数。

多态继承

多态下单一继承

  C++的动态绑定的实现是同过一张虚函数表来完成的,当通过基类指针来操作子类的时候,则使用这张虚函数表来索引到派生类类虚函数的正确地址,从而实现多态。而虚函数表的样子,如例子所示:

class base{
public:
    base(){
        base_val = 1;
    }
    virtual void get()
    {
        cout << "base get" << endl;
    }
    virtual void size()
    {
        cout << "base size is:" << sizeof(base) << endl;
    }
    void set()
    {
        cout << "base set" << endl;
    }

protected:
    int base_val;
};

class derived: public base{
public:
    derived()
    {
        derived_val = 2;
    }
    virtual void get()
    {
        cout << "derived get" << endl;
    }
    virtual void size()
    {
        cout << "derived size is:" << sizeof(derived) << endl;
    }
    virtual void show()
    {
        cout << "derived show is:"  << endl;
    }
    void set()
    {
        cout << "derived set" << endl;
    }
protected:
    int derived_val;
};

int main(void)
{
    derived *pd = new derived;

    base *pb = pd;
    int *p = (int *)pd;

    cout << "指向虚函数表的指针:    " << (int *)p[0] << endl;
    cout << "  虚表内容:" << endl;
    cout << "    get函数地址:    "      << (int *)*(*(int **)pb+0) << endl;
    //((void (*)())(*(*(int **)pd+0)))();
    cout << "    size函数地址:    " << (int *)*(*(int **)pb+1) << endl;
    //((void (*)())(*(*(int **)pd+1)))();
    cout << "    show函数地址:    " << (int *)*(*(int **)pb+2) << endl;
    cout << "    虚表结束标记:    " << (int *)*(*(int **)pb+3) << endl;
    cout << "base_val成员:        " << p[1] << endl;
    cout << "derived_val成员:    " << p[2] << endl;

    delete pd;
    return 0;
}

  运行结果在两个平台上测试都是一样的。

  在这里,base class中虽然没有show函数,但是因为pb指向的是derived object,从而可以看到pb中的虚表有show函数。因为目前pbpd指向的都是同一张虚表。但是pb不意味着可以使用show函数,当你使用pb->show();去调用show函数的时候编译器还是会报错的。这是因为base object 根本没derived object那么大,pb所指的范围仅仅是base object的范围。

构造/析构函数的总结(总结于《深入探索C++对对象模型》)

  当一个class 中函数virtual 函数的时候,那么编译器将自动帮你合成一个default constructor。编译器大概所作的工作如下:

  1. 一个virtual function table会被编译器产生出来,内放classvirtual functions 地址。

  2. 在每一个class object中,一个额外的 pointer member(也就是vptr)会被编译器产生出来,内含指向virtual functions table的地址。

  3. 为了实现多态机制,编译器必须为每一个base class的派生类的vptr设定初值,放置合适的virtual functions table地址。对于class所定义的每一个constructor(如果没有constructor编译器会帮你合成一个)编译器会安插一些代码来做vptr的设定工作。

  4. (这一条是我个人理解,《深入探索C++对象模型》并没有这一条)当一个object即将销毁时所调用的destructor应该安插一些代码,释放vptr所指的virtual functions table的资源。

  5.当建构一个derived对象的时候,会先建构base subobject,而在建构base subobject的时候,derived object并没有完全建构出来,所以在base 的构造函数之中是没办法使用到derived virtual function的。而下面这种情况,则base可以使用到derived objectvirtual functions。因为这时候的derived object已经完全建构出来了。(《深入探索C++对象模型》反复强调尽量不要在构造函数中做过多的初始化工作。)

  如果第五条你觉得有点抽象,那么看下面的代码来理解:

class base{
public:
    base(){
        base_val = 1;
        size();
    }
    virtual void get()
    {
        cout << "base get" << endl;
    }
    virtual void size()
    {
        cout << "base size is:" << sizeof(base) << endl;
    }
    void set()
    {
        cout << "base set" << endl;
    }

protected:
    int base_val;
};

class derived: public base{
public:
    derived()
    {
        derived_val = 2;
        size();
    }
    virtual void get()
    {
        cout << "derived get" << endl;
    }
    virtual void size()
    {
        cout << "derived size is:" << sizeof(derived) << endl;
    }
    virtual void show()
    {
        cout << "derived show is:"  << endl;
    }
    void set()
    {
        cout << "derived set" << endl;
    }
protected:
    int derived_val;
};

int main(void)
{
    derived *pd = new derived;
    
    delete pd;
    return 0;
}

  它的运行结果是:

  如果根据多态性的原理,base构造函数中应该调用的是derivedsize函数,但是在这里,base调用的却是base自身的size函数。因为完整的derived object还没完全构造出来,所以它能调用的只有base自身的size函数了。

自然多态

  1. C++在单一继承体系中,支持一种自然的多态。也就是说在运行时对于this指针不必做人为的调整。一个单一的继承串链中,一个class 的指针所指向的总是object 的起始地址,其差异仅仅derived object比较大。所以C++在一个单一继承体系中,virtual function机制的行为十分良好。  (总结于《深入探索C++对象模型》)

  2.  在单一的继承体系中,每一个object中最多有一张virtual functions table。   

多态下多继(总结于《深入探索C++对象模型》)

  "...多继承中支持virtual functions ,其复杂度围绕在第二个及后继的base classes身上,以及在执行期调整this指针"。(《深入探索C++对象模型》原文)

  多继承讨论的大多数内容将使用下面UML模型(模型改自《深入探索C++对象模型》p160页)进行讨论,其中斜体表示的函数是加上virtual的:

  代码如下(这一节基本讨论下面这个模型):

class base_left{
public:

    base_left()
    {
        base_left_val = 1;
        cout << "base_left base_left function" << endl;
    }

    virtual ~base_left()
    {
        cout << "base_left ~base_left function" << endl;
    }

    virtual void speak()
    {
        cout << "base_left speak function" << endl;
    }

    virtual base_left *clone()
    {
        cout << "base_left clone function" << endl;
        return this;
    }
protected:
    int base_left_val;
};

class base_right
{
public:

    base_right()
    {
        base_right_val = 2;
        cout << "base_right base_right function" << endl;
    }
    
    virtual ~base_right()
    {
        cout << "base_right ~base_right function" << endl;
    }

    virtual void mumble()
    {
        cout << "base_right mumble function" << endl;
    }

    virtual base_right *clone()
    {
        cout << "base_right clone function" << endl;
        return this;
    }
protected:
    int base_right_val;
};

class derived: public base_left, public base_right
{
public:
    derived()
    {
        derived_val = 3;
        cout << "derived derived function" << endl;
    } 

    virtual ~derived()
    {
        cout << "derived ~derived function" << endl;
    }

    virtual derived *clone()
    {
        cout << "derived clone function" << endl;
        return this;
    }
protected:
    int derived_val;
};

  先看一下多继承下derived的内存分布情况,在这里我是使用了编译器的/d1reportSingleClassLayoutderived开关来打印对象模型的:(VS2012 + Win7 SP1 64Bit):

  这里如果您想自己打印这个对象模型的话可以使用下面的代码:

int main(void)
{
    derived *pd = new derived;
    int *p = (int *)pd;
    cout << endl;

    cout << "指向base_left主虚表的指针:    "            << (int *)p[0] << endl;
    cout << "    derived中base_left主虚表内容:"        << endl;
    cout << "        derived::~derived函数地址:"        << (int *)*(*(int **)&p[0]+0) << endl;    //((void(*)())*(*(int **)&p[0]+0))();
    cout << "        derived::speak函数地址:      "        << (int *)*(*(int **)&p[0]+1) << endl;    //((void(*)())*(*(int **)&p[0]+1))();
    cout << "        derived::clone函数地址:      "        << (int *)*(*(int **)&p[0]+2) << endl;    //((void(*)())*(*(int **)&p[0]+2))();
    cout << "        虚表结束标记:          "                << (int *)*(*(int **)&p[0]+3) << endl;    //((void(*)())*(*(int **)&p[0]+3))();
    cout << "base_left_val成员:    "                    << p[1]  << endl;
    cout << "指向base_right次虚表的指针:    "        << (int *)p[2] << endl;
    cout << "    derived中base_right次虚表内容:  "        << endl;
    cout << "        derived::~derived函数地址:      "    << (int *)*(*(int **)&p[2]+0) << endl;    //((void(*)())*(*(int **)&p[2]+0))();
    cout << "        derived::mumble函数地址:      "        << (int *)*(*(int **)&p[2]+1) << endl;    //((void(*)())*(*(int **)&p[2]+1))();
    cout << "        base_right::clone函数地址:      "    << (int *)*(*(int **)&p[2]+2) << endl;    //((void(*)())*(*(int **)&p[2]+2))();
    cout << "        derived::clone函数地址:      "        << (int *)*(*(int **)&p[2]+3) << endl;    //((void(*)())*(*(int **)&p[2]+3))();
    cout << "        虚表结束标记:          "                << (int *)*(*(int **)&p[2]+4) << endl;    //((void(*)())*(*(int **)&p[2]+4))();
    cout << "base_right_val成员:    "                << p[3] << endl;
    cout << "derived_val成员:    "                    << p[4] << endl;

    cout << endl;
    delete pd;
    return 0;
}

  疑惑点:VS中编译出来的derived object中的base_right虚表为什么会有两个clone函数地址?

  不过研究多继承(虚继承也一样)的内存分布好像没什么意义。研究单继承的内存分布可以让我们比较好地理解C++多态是怎么实现的(对单继承C++和VS好像没有太大的区别),而随着多继承、虚继承复杂度的增加,各家编译器的采取实现编译器的算法差距比较大。其中也出现了各种不同的内存布局,如在VS下,上面base_right的虚表中就有两个clone函数的地址。而C++的base_right只有一个clone函数的地址。其实是我懒得再去GCC下做试验...  )

构造函数析构函数

  与前面一样,在derived 的构造函数中,编译器会安插一些代码可以初始化vptr和调用上一层基类,析构函数则释放vptr所指虚函数表,并插入调用相应的析构函数代码。

this指针的调整(也和上面的无多态多继承差不多)

  从上面打印出来的对象模型内存分布图可以看到编译器所作的this指针调整,在base_right的虚表中,每一个成员都有this -= 8;做this指针的调整。

  当利用第二个后继基类(base_right)的指针指向derived object(如下面所示),那么编译器必须帮你将derived object的地址做调整。以让base_right的指针能指向正确的derived object 起始地址。如下面所示的代码:

base_right *pbr = new derived;

  编译器将产生如下代码帮你做地址的调整:

derived *temp = new derived;
base_right *pbr = temp ? temp+sizeof(base_left) : NULL;

  为了直观,我把无多态多继承的内存分布图再次贴出来:

  而当调用delete pbr;时,编译器也会介入处理,帮你把pbr所指地址调整好,指向derived object 的起始地址,这样当调用delete清空资源时才会把资源清理干净。否则base_left  subobject的内容将没办法被清理到。

  下面是我根据理解的,当base_right的指针指向derived object时,调用delete编译器所作的调整工作:      

void *temp = pbr ? pbr-sizeof(base_left) : NULL;
delete temp;

  而第一个基类(base_left)因为在内存布局总是在object的最顶处,所以使用base_left指针操作derived object的时候将得到良好的效率。因为第一基类支持自然多态。

  当使用下代码来释放内存的时候,虽然都会调用~derived函数,但是两者却是通过不同的vptr取得~derived函数正确的地址。从derived的内存分布图上也可以看到derived object拥有两张virtual functions table。

base_left *pbl = new derived;
base_right *pbr = new derived;

delete pbl;
delete pbr;

  当base_left的指针指向derived ojbect时,通过base_left指针所调用被derived所覆盖的函数都是通过derived object的第一张主虚表(也就是base_left的虚表)索引到的函数。因为base_left是第一继承类,所以它的指针总是会指向derived object而不必做this指针的调整。而当使用base_right指针操作derived object那些覆盖了base_right的函数,是通过调整this指针所取得的第二个虚表(也就是base_right的虚表)的函数。

  总之,说了那么多,就是想说除了第一基类在实现多态上有着最好的效率外,通过后继基类来实现多态总会费去一些额外的操作。而且,多继承会增加一定的内存开销。每当多继承一个基类就会多出一张虚表。而这些虚表都是通过其各自的基类的虚函数来记录下那些将要被覆盖的虚函数地址。

  最后要说的是:除了第一基类,后继的基类存取derived object的数据也是要经由this指针调整才能正确存取。

当virtual function的返回值有所变化  

  直接看例子比较容易理解:

int main(void)
{
    derived *pd = new derived;
    base_left *pbl = pd;
    base_right *pbr = pd;

    cout << pbl->clone() << endl;
    cout << pbr->clone() << endl;
    
    return 0;
}

 运行结果:

  OK。这里可以看到clone返回的this指针经过了调整。 

复制代码
int main(void)
{
    
    base_left *pbl = new derived;
    base_right *pbr = (base_right *)pbl;

    pbr->mumble();
    cout << pbl->clone() << endl;
    cout << pbr->clone() << endl;
    
    return 0;
}
复制代码

  运行结果:

  这里可以看到pbl指向的deived object的指针通过强制转换后赋给base_right的指针,pbr没办法正确索引到它的虚表,结果导致的运行结果出乎了意料。当调用mumble()时,调用的却是base_left的speak()。这是因为,base_left和base_right这两个class压根就一点关系都没有,通过强制转换后pbr = pbl; 也就是说两者都指向了derived object的起始地址。

  如果pbl指向的是一个完整的derived object,那么可以使用下面的方法来使pbr实现多态。

base_right *pbr = dynamic_cast<base_right*>(pbl);
base_right *pbr = (derived *)pbl;

  如果是下面这种情况:

int main(void)
{
    
    base_left *pbl = new base_left;
    base_right *pbr = dynamic_cast<base_right*>(pbl);
    pbr->mumble();
    cout << pbl->clone() << endl;
    cout << pbr->clone() << endl;
    
    return 0;
}

  那么,因为pbl所指的内存空间根本没有包含base_right subobject。所以,再怎么转换也是没用的。

虚继承

  “...virtual base class的实现方法在不同的编译器之间有着极大的差异。然而,每一种实现法的共通点在于必须使virtual class base在其每一个derived class object中的位置,能够于执行期准备妥当...”(摘自《深入探索C++对象模型》p46页)

  我觉得要理解这句话要分两个方面,一,编译器的实现。二,C++语法上的规定。

  C++在语法语义上规定virtual base class将要实现的内容,而各家编译器厂商可以有自己的不同的做法。不过最终的目的就是一定要实现”使virtual class base在其每一个derived class object中的位置,能够于执行期准备妥当。“,所以不要太在意编译器之间object在内存分布情况的不同,而要在意的是C++语法语义上的规定。

  至于编译时期和执行时期,可以这么理解:编译时期是静态的,程序编译成可执行文件的时候它是什么状态就是什么状态,已经是固定的了。而执行时期只有程序在运行时才能显示出效果。(个人理解)

class base_up{public: int i;};
class base_left:virtual public base_up{};
class base_right:virtual public base_up{};
class derived :public base_left, public base_right{};

void foo(base_left *pbl){
     //无法在编译时期决定pbl->i的正确位置,只有在执行期才能确定pbl->i的正确位置。
     pbl->i = 100;
}

对构造函数的处理

  “...对于class所定义的每一个constructor。编译器会安插那些“允许每一个virtual base class”的执行期存取操作的代码。如果class没有声明任何constructor,编译器必须为它合成一个default constructor...”(摘自《深入探索C++对象模型》p47页)

  如果一个继承体系中加入了virtual 继承, 那么编译器对经过virtual 继承的class的处理就没单纯的继承那么简单了。如下面这个模型:

 class base_up{public: int i;};
 class base_left : virtual public base_up{};
 class base_right : virtual public base_up{};
 class derived :public base_left, public base_right{};
class derived :public base_left, public base_right{};

  编译器会在base_left和base_right的constructor中安插如下所示的伪代码,如果没有定义一个constructor那么编译器将帮你合成一个default constructor(base_right差不过就不贴出来了):

base_left * base_left::base_left(base_left *this, bool __most_derived)
{
    if (__most_derived != false)
        this->base_up::base_up();
    return this;
}

  上面代码中base_left constructor 调用virutal base class constructor多加了一个判断,如果这个class是最底层的class,那么将调用base_up的constructor初始化base_up object。而对于derived class constructor的代码安插,伪代码如下所示:

derived * derived::derived(derived *this, bool __most_derived)
{
    if (__most_derived != false)
        this->base_up::base_up();

    this->base_left::base_left();
    this->base_right::base_right();
    return this;
}

  通过加入一个__most_derived成员来判断这个class是否是最底层,从而避免了像下面没有虚继承时会出现多次调用base_up constructor的情况。

 class base_up{};
 class base_left : public base_up{};
 class base_right : public base_up{};
 class derived : public base_left, public base_right{};

关于虚继承的对象模型布局

  关于这一节大多是自己跟踪的结果,所以可能存在过多的错误

单一虚继承的情况

class base
{
public:
    base ()
    {
        base_x = 0xb1b1b1b1;
        base_y = 0xb2b2b2b2;
    }
    virtual void show()
    {
        cout << "base_x : " << base_x << endl;
        cout << "base_y : " << base_y << endl;
    }
protected:
    int base_x;
    int base_y;
};

class derived : virtual base
{
public:
    derived()
    {
        derived_x = 0xd1d1d1d1;
        derived_y = 0xd2d2d2d2;
    }
    virtual void show()
    {
        base::show();
        cout << "derived_x : " << derived_x << endl;
        cout << "derived_y : " << derived_y << endl;
    }
protected:
    int derived_x;
    int derived_y;
};

先看一下它在VS2012下的内存分布情况(测试环境为Win7 64b+VS2012):

    (A)根据上面所说虚继承的对象模型布局策略:先安排确定的部分(derived的内容),再安排virtual base subobject部分。

    (B)关于vtordisp(这玩意只有在VS编译器才有),在我的环境测试(Win7 64b + VS2012)是:当 "derived class 中重写了virtual base class 的虚函数" 且 "derived class 定义了构造函数或者定义了析构函数"。也就是说上述的两个条件是触发产生vtordisp的必要条件。而这个vtordisp的值总是为0。(MSDN上解释说vtordisp是为了解决虚基类和派生类之间转换时指针位置的调整)

     (C)关于vbptr的内容:

 

 为了比较容易看一点我画了一张图(其中vbptr所指的内容除了第一个字段研究了一下,其它字段没仔细研究过,如果您知道答案也麻烦能告诉我一生:) ):

  关于虚继承对象模型因为实在有些复杂,所以只是稍微大概看一下对象模型如何而已,不会做太深入的研究。不过GCC如果有虚继承的话,索引虚基类是通过第一张虚表来完成的。也就是说在GCC中,虚基类表和虚函数表是同一张表。可以稍微看一下在GCC中 derived 虚函数表大概的内存布局(测试环境为Win7 64b+mingw):

Vtable for derived
derived::_ZTV7derived: 8u entries
    12u
    (int (*)(...))0
    (int (*)(...))(& _ZTI7derived)
   derived::show
   4294967284u
   (int (*)(...))-0x0000000000000000c
   (int (*)(...))(& _ZTI7derived)
   derived::_ZTv0_n12_N7derived4showEv

  通过对derived对象的取值,它的内存分布图大概如下:

int main (void)
{
    derived *pd = new derived;

    int *p = (int *)pd;
    int **q = (int**)p;

    printf ("派生类的虚函数表地址:%p
", p[0]);
    printf ("   派生类的虚函数表:
");
    printf ("       虚基类地址跟派生类虚函数表地址的差:%d
", (*q)[-3]);     //虚基类地址跟派生类虚函数表指针的差
    printf ("       对象地址跟派生类虚函数表地址的差:%p
", (*q)[-2]);       //未知字段
    printf ("       未知:%p
", (*q)[-1]);                                //未知字段
    printf ("       虚函数show的地址:%p
", (*q)[0]);                      //虚函数
    //((void(*)())((*q)[0]))(); 
    printf ("       派生类虚函数表地址跟虚基类地址的差:%d
", (*q)[1]);       //派生类虚函数表地址跟本类地址的茶
    printf ("       派生类虚函数表地址跟虚基类地址的差:%d
", (*q)[2]);      //派生类虚函数表地址跟虚基类地址的差:
    printf ("       未知:%p
", (*q)[3]);                                 //未知
    printf ("       未知:%p
", (*q)[4]);                                 //未知

    printf ("derived_x:%p
", p[1]);
    printf ("derived_y:%p
", p[2]);
    printf ("虚基类函数表地址:%p
", p[3]); q = (int **)(p[3]);
    printf ("   未知:%p
", q[0]);
    printf ("   未知:%p
", q[1]);
    printf ("   未知:%p
", q[2]);

    printf ("%p
", p[4]);
    printf ("%p
", p[5]);


    return 0;
}

 运行结果如下:

   如果仔细观察可以发现虚基类函数表地址和派生类函数表地址距离并不远,通过查看内存得到如下内容(另外GCC的derived对象模型的图我就不画了。

0x47370c: 38 03 42 00 f4 ff ff ff|f4 ff ff ff e8 1c 47 00    8.B.ôÿÿÿôÿÿÿè.G.

0x47371c: 6c ab 46 00 00 00 00 00|00 1d 47 00 70 85 40 00    l«F.......G.p…@.

 多个虚继承的情况

class base_left
{
public:
    base_left ()
    {
        base_left_x = 0xb1b1b1b1;
        base_left_y = 0xb1b1b1b1;
    }
    virtual void show()
    {
        cout << "base_left_x : " << base_left_x << endl;
        cout << "base_left_y : " << base_left_y << endl;
    }
protected:
    int base_left_x;
    int base_left_y;
};

class base_right
{
public:
    base_right ()
    {
        base_right_x = 0xb2b2b2b2;
        base_right_y = 0xb2b2b2b2;
    }
    virtual void show()
    {
        cout << "base_left_x : " << base_right_x << endl;
        cout << "base_left_y : " << base_right_y << endl;
    }
protected:
    int base_right_x;
    int base_right_y;
};

class derived : virtual public base_left, virtual public base_right
{
public:
    derived()
    {
        derived_x = 0xd1d1d1d1;
        derived_y = 0xd2d2d2d2;
    }
    virtual void show()
    {
        cout << "derived_x : " << derived_x << endl;
        cout << "derived_y : " << derived_y << endl;
    }
protected:
    int derived_x;
    int derived_y;
};

  它在VS2012中对象模型如下:

  在这里,因为derived覆盖了两个基类的show函数,所以有了两个vtordisp。(我的理解是:vtordisp是为了在执行期间实现多态做内存调整所用的。如果derived不覆盖show函数的时候就没存在vtordisp了。)

class base_left
{
public:
    base_left ()
    {
        base_left_x = 0xb1b1b1b1;
        base_left_y = 0xb1b1b1b1;
    }
    virtual void show()
    {
        cout << "base_left_x : " << base_left_x << endl;
        cout << "base_left_y : " << base_left_y << endl;
    }
protected:
    int base_left_x;
    int base_left_y;
};

class base_right
{
public:
    base_right ()
    {
        base_right_x = 0xb2b2b2b2;
        base_right_y = 0xb2b2b2b2;
    }
    virtual void show()
    {
        cout << "base_left_x : " << base_right_x << endl;
        cout << "base_left_y : " << base_right_y << endl;
    }
protected:
    int base_right_x;
    int base_right_y;
};

class derived : virtual public base_left, virtual public base_right
{
public:
    derived()
    {
        derived_x = 0xd1d1d1d1;
        derived_y = 0xd1d1d1d1;
    }
    /*
    virtual void show()
    {
        cout << "derived_x : " << derived_x << endl;
        cout << "derived_y : " << derived_y << endl;
    }*/
protected:
    int derived_x;
    int derived_y;
};

 对象模型如下:

  可以发现如果没有覆盖基类的虚函数,那么vtordisp就不存在了。多虚继承的规律是:派生类有多少个虚基类,而当一个虚基类带有虚函数,派生类去覆盖这个虚基类的虚函数时,那么就会多一个vtordisp做这个虚基类的执行时期的调正。(个人理解)

  关于GCC的对象模型就不贴出来了。

 菱形继承

 先看一段代码:

class base_up{
public:
    base_up()
    {
        base_up_val = 1;
    }

    virtual void show ()
    {
        cout << "base_up::show" << endl;
    }
protected:
    int base_up_val;
};

class base_left:virtual public base_up
{
public:
    base_left()
    {
        base_left_val = 2;
    }
    
    virtual void show ()
    {
        cout << "base_left::show" << endl;
    }
protected:
    int base_left_val;
};

class base_right: virtual public base_up
{
public:

    base_right()
    {
        base_right_val = 3;
     
    }

    virtual void get()
    {
        cout << "derived::get" << endl;
    }
protected:
    int base_right_val;
};

class derived: public base_left, public base_right
{
public:
    derived()
    {
        derived_val = 4;
    }
    ~derived()
    {
        cout << "derived_val ~derived_val function" << endl;
    }

    virtual void show ()
    {
        cout << "base_right::show" << endl;
    }
protected:
    int derived_val;
};

  它的对象模型如下:

  在这里,derived 的左基类和右基类都带了一个虚基类表。至于右基类也带一个虚基类表的原因是为了能够使当发生下面这种情况时也能实现多态效果(个人理解):

int main (void)
{
    derived *pd = new derived;
    base_right *pbr = pd;
    base_up *pbu = pbr;  
    return 0;
}

  另外,base_left和derived 是共用一张虚函数表的。而这张虚函数表是放在第十个成员那里。与上面的单继承不同的是,单继承虚函数表总是放在object最开始的位置,而这里却放在第十个成员那里,那么当发生下面这种情况的时候pbl要取得正确的虚函数表位置。(个人理解)

derived *pd = new derived;
base_left *pbr = pd;

  如何正确取得,就靠derived 的虚函数表上面的一个成员-28来计算正确的位置,也就是说当发生这代码 base_left *pbr = pd; 的时候,虚函数表的取得是通过derived 的虚函数表中的-28来进行调整的。(个人理解)

   关于GCC的对象模型就不贴出来了。

 执行期的调整、虚继承小总结

   “...clas如果内涵一个或多个virtual base class subobject,像C++标准库中的istream那样,将被分割为两部分,一个不变的局部和一个共享的局部。不变的局部中的数据,不管后继如何衍化,总是拥有固定的offset(从object的开头算起),所以这一部分数据可以被直接存取,至于共享局部,所表现的就是virtual base class subobject, 这一部分数据其位置会因为每次的派生操作而有变化。所以它们只可以间接存取。各家编译器实现技术之间的差异就在于间接存取的方法不同...”(摘自《深入探索C++对象模型》p118页)

class ios{...};
class istream: virtual public ios{...};
class ostream: virtual public ios{...};
class iostream: public istream, public ostream{...};

  “...虚继承一般布局策略是先安排好derived class不变的部分,然后再建立其共享部分...”(摘自《深入探索C++对象模型》p119页)。像下面这个继承关系:

class base_up{
public:
    base_up()
    {
        base_up_val = 1;
        cout << "base_up base_up function" << endl;
    }
protected:
    int base_up_val;
};

class base_left:virtual public base_up
{
public:

    base_left()
    {
        base_left_val = 2;
        cout << "base_left base_left function" << endl;
    }
protected:
    int base_left_val;
};

class base_right: virtual public base_up
{
public:

    base_right()
    {
        base_right_val = 3;
        cout << "base_right base_right function" << endl;
    }
    
protected:
    int base_right_val;
};

class derived: public base_left, public base_right
{
public:
    derived()
    {
        derived_val = 4;
        cout << "derived_val derived_val function" << endl;
    }
    ~derived()
    {
        cout << "derived_val ~derived_val function" << endl;
    }
protected:
    int derived_val;
};
class base_up{
public:
    base_up()
    {
        base_up_val = 1;
        cout << "base_up base_up function" << endl;
    }
protected:
    int base_up_val;
};

class base_left:virtual public base_up
{
public:

    base_left()
    {
        base_left_val = 2;
        cout << "base_left base_left function" << endl;
    }
protected:
    int base_left_val;
};

class base_right: virtual public base_up
{
public:

    base_right()
    {
        base_right_val = 3;
        cout << "base_right base_right function" << endl;
    }
    
protected:
    int base_right_val;
};

class derived: public base_left, public base_right
{
public:
    derived()
    {
        derived_val = 4;
        cout << "derived_val derived_val function" << endl;
    }
    ~derived()
    {
        cout << "derived_val ~derived_val function" << endl;
    }
protected:
    int derived_val;
};

  通过打印出derived的布局情况可以看到VS2012 中的C++编译器的布局策略是先布局base_left、base_right部分,最后确定共享的virtual base class。

  

  “...虚继承下object如何布局才能够存取class共享的部分呢?...Microsoft 编译器引入所谓的virtual base class table,每一个class object如果有一个或多个virtual base classes, 就会由编译器安插一个指针,指向virtual base class table(这就是虚继承会比普通继承大的原因)。至于真正的virtual base class 指针,当然是放在该表格中...”(摘自《深入探索C++对象模型》p120,这个通过上面的对象模型可以看出来VS对虚继承的处理)。

  例如,上面中的vbptr就是一个指向virtual base class table的一个指针了。(因为上面的代码没加入虚函数,所以看不到虚表指针)

  "...第二个解决方法,是在virtual functions table中放置virtual base class的offset(而不是地址)..."(跟GCC的虚继承实现模型很像)

  加粗部分是《深入探索C++对象模型》列出来的一些C++编译器对虚继承的实现模型。

  "...上述每一种方法都是一种实现模型,而不是一种标准。每一种模型都是用来解决“存取shared subobject内的数据”所引发的问题,由于对virtual base class的支持带来额外的负担以及高度的复杂性,每一种实现模型多少有点不同,而且我想还会随着时间而进化..."(摘自《深入探索C++对象模型》p122)

  上面的模型放到GCC编译后,各个对象模型如下(各种含义就不解释了,有兴趣的童靴看一下,没兴趣直接跳掉~):

Class base_up
   size=4 align=4
   base size=4 base align=4
base_up (0xb67dac30) 0

Vtable for base_left
base_left::_ZTV9base_left: 3u entries
    8u
    (int (*)(...))0
    (int (*)(...))(& _ZTI9base_left)

VTT for base_left
base_left::_ZTT9base_left: 1u entries
    ((& base_left::_ZTV9base_left) + 12u)

Class base_left
   size=12 align=4
   base size=8 base align=4
base_left (0xb663db40) 0
    vptridx=0u vptr=((& base_left::_ZTV9base_left) + 12u)
  base_up (0xb67dad20) 8 virtual
      vbaseoffset=-0x0000000000000000c

Vtable for base_right
base_right::_ZTV10base_right: 3u entries
    8u
    (int (*)(...))0
    (int (*)(...))(& _ZTI10base_right)

VTT for base_right
base_right::_ZTT10base_right: 1u entries
    ((& base_right::_ZTV10base_right) + 12u)

Class base_right
   size=12 align=4
   base size=8 base align=4
base_right (0xb663dec0) 0
    vptridx=0u vptr=((& base_right::_ZTV10base_right) + 12u)
  base_up (0xb665c0f0) 8 virtual
      vbaseoffset=-0x0000000000000000c

Vtable for derived
derived::_ZTV7derived: 6u entries
    20u
    (int (*)(...))0
    (int (*)(...))(& _ZTI7derived)
   12u
   (int (*)(...))-0x00000000000000008
   (int (*)(...))(& _ZTI7derived)

Construction vtable for base_left (0xb665f240 instance) in derived
derived::_ZTC7derived0_9base_left: 3u entries
    20u
    (int (*)(...))0
    (int (*)(...))(& _ZTI9base_left)

Construction vtable for base_right (0xb665f280 instance) in derived
derived::_ZTC7derived8_10base_right: 3u entries
    12u
    (int (*)(...))0
    (int (*)(...))(& _ZTI10base_right)

VTT for derived
derived::_ZTT7derived: 4u entries
    ((& derived::_ZTV7derived) + 12u)
    ((& derived::_ZTC7derived0_9base_left) + 12u)
    ((& derived::_ZTC7derived8_10base_right) + 12u)
   ((& derived::_ZTV7derived) + 24u)

Class derived
   size=24 align=4
   base size=20 base align=4
derived (0xb6660230) 0
    vptridx=0u vptr=((& derived::_ZTV7derived) + 12u)
  base_left (0xb665f240) 0
      primary-for derived (0xb6660230)
      subvttidx=4u
    base_up (0xb665c4b0) 20 virtual
        vbaseoffset=-0x0000000000000000c
  base_right (0xb665f280) 8
      subvttidx=8u vptridx=12u vptr=((& derived::_ZTV7derived) + 24u)
    base_up (0xb665c4b0) alternative-path

  考虑虚继承中virtual base class带有nonstatic data members和virtual functions的情况时,this指针的调整:

class base_up{
public:
    base_up()
    {
        base_up_val = 1;
        cout << "base_up base_up function" << endl;
    }
    virtual ~base_up()
    {
        cout << "base_up ~base_up function" << endl;
    }
    virtual void show()
    {
        cout << "base_up show function" << endl;
    }
protected:
    int base_up_val;
};

class base_left:virtual public base_up
{
public:

    base_left()
    {
        base_left_val = 2;
        cout << "base_left base_left function" << endl;
    }
    virtual void show()
    {
        cout << "base_left show function" << endl;
    }
protected:
    int base_left_val;
};

  从下面的运行结果可以看到编译器对this指针做了调整,并没有取得object的开始地址。但是它调整并没有多继承那种情况来得单纯。在vs下,它是经过虚基类表来取得正确的base_up地址的, 而在G++下它是通过虚函数表来取得的。

   "...当一个virtual base class 从另一个virtual base class派生而来,并且两者都支持virtual functions和nonstatic data members时,编译器对于virtual base class的支持简直就像走进了迷宫一样...一般而言,这已经被证明是一项高难度的技术...我的建议是:不要在一个virtual base class中声明nonstatic data members。如果这么做,你会距离复杂的深渊愈来愈近,终不可拔..."(摘自《深入探索C++对象模型》p169)

  总之,C++规定了虚继承是怎么共享virtual base class,而各家编译器怎么去塑造模型是各家编译器的事。我们只需要知道使用这个语法语义之后会有什么结果。至于VS or G++的对象模型如何大可不必去深究...(个人理解)

关于构造函数的总结

  "...有四种情况,会导致“编译器必须为未声明constructor之classes合成一个default constructor”...."

  分为下面四种情况

  A). 带有default constructor的member class object。

  B). 带有default constructor的base class

  C). 带有一个virtual function 的class

  D). 带有一个virtual base class 的class

  (总结于《深入探索C++对象模型》)

关于析构函数的总结

  "...一个object生命结束于其destructor开始执行之时,由于每一个base class destructor都轮番被调用..."

  合成默认destructor的四种情况跟构造函数一样。

  destructor调用流程如下:

  A). destructor的函数本身首先被执行

  B). 如果class拥有member class objects,而后则拥有destructor,那么它们会以声明顺序的相反顺序被调用。

  C). 如果一个class内带一个vptr,那么首先重设相关的virtual tables。

  D). 如果有任何直接的(上一层)nonvirtual base classes 拥有destructor,它们会以其生命顺序的相反顺序被调用。

  E). 如果有任何virtual base classes拥有destructor,而当前讨论的这个class 最尾端的class,那么它们会以其原来的构造顺序相反顺序被调用。

  (总结于《深入探索C++对象模型》)

总结

   总之, C++ 提供一种语法语义上的标准,而各个编译器的厂商只需要根据C++这门语言的语法语义去编写编译器,而内部实现在没有C++标准的规定下,各个编译器的厂商可以根据需要去编写编译器。所以出现了类似VS and GCC 对象模型的区别。而学习C++,通常不必关心内部的实现方法,只需要在这个语法语义下写代码会有什么后果就行了。(个人理解)

参考资料 

《深入探索C++对象模型》 主要参考资料
MSDN
C++ under the Hood:http://www.openrce.org/articles/files/jangrayhood.pdf
C++ FQA:http://yosefk.com/c++fqa/index.html
C++ 对象的内存布局(上):http://blog.csdn.net/haoel/article/details/3081328
C++ 对象的内存布局(下):http://blog.csdn.net/haoel/article/details/3081385
RTTI、虚函数和虚基类的实现方式、开销分析及使用指导:http://baiy.cn/doc/cpp/inside_rtti

原文地址:https://www.cnblogs.com/Jer-/p/3297902.html