C++的多态机制正是基于这种虚标实现的,虚(virtual)函数的一般实现模型是

 基础     |      2020-03-18 13:08

虚(virtual)函数的一般实现模型是:每一个类(class)有一个虚表(virtual table),内含该class之中有作用的虚(virtual)函数的地址,然后每个对象有一个vptr,指向虚表(virtual table)的所在。

今天看了的,感觉需要了解对象内存的问题。参考:

C++ 类的虚表 20130929

请允许我援引自深度探索c++对象模型一书上的一个例子:

1.何为C++对象模型?

关键技术:封装、继承、组合、虚函数、抽象基类、动态绑定、多态性等等

class Point { 
public: 
   virtual ~Point();  

   virtual Point& mult( float ) = 0; 

   float x() const { return _x; }     //非虚函数,不作存储 
   virtual float y() const { return 0; }   
   virtual float z() const { return 0; }   
   // ...

protected: 
   Point( float x = 0.0 ); 
   float _x; 
};

引用《深度探索C++对象模型》这本书中的话:

1.首先整理一下在阿里巴巴面试遇到的函数虚表的问题。

1、在Point的对象pt中,有两个东西,一个是数据成员_x,一个是_vptr_Point。其中_vptr_Point指向着virtual table point,而virtual table(虚表)point中存储着以下东西:

  有两个概念可以解释C++对象模型:

  1. 语言中直接支持面向对象程序设计的部分。

  2. 对于各种支持的底层实现机制。

直接支持面向对象程序设计,包括了构造函数、析构函数、多态、虚函数等等,这些内容在很多书籍上都有讨论,也是C++最被人熟知的地方(特性)。而对象模型的底层实现机制却是很少有书籍讨论的。对象模型的底层实现机制并未标准化,不同的编译器有一定的自由来设计对象模型的实现细节。在我看来,对象模型研究的是对象在存储上的空间与时间上的更优,并对C++面向对象技术加以支持,如以虚指针、虚表机制支持多态特性。

         在C++中的Class中的函数式存储在Class数据机构的虚表中。每一个Class对应的所有的函数地址都会在Class的数据结构虚表中,每一个Class的对象在对象开始的地方都会有一个指针(计算机的位数一般是是32位)指向Class的函数虚表,函数虚表中每一个函数地址是按照在Class中声明的顺序。一般也是一个32bit的指针。

  • virtual ~Point()被赋值slot 1,
  • mult() 将被赋值slot 2.
  • y() is 将被赋值slot 3
  • z() 将被赋值slot 4.

2.文章内容简介

         C++的多态机制正是基于这种虚标实现的。

class Point2d : public Point { 
public: 
   Point2d( float x = 0.0, float y = 0.0 )   
      : Point( x ), _y( y ) {} 
   ~Point2d();   //1

   //改写base class virtual functions 
   Point2d& mult( float );  //2 
   float y() const { return _y; }  //3

protected: 
   float _y; 
};

这篇文章主要来讨论C++对象在内存中的布局,属于第二个概念的研究范畴。而C++直接支持面向对象程序设计部分则不多讲。文章主要内容如下:

         首先看一个父类:

2、在 Point2d的对象pt2d中,有三个东西,首先是继承自基类pt对象的数据成员_x,然后是pt2d对象本身的数据成员_y,最后是 _vptr_Point。其中_vptr_Point指向着virtual table point2d。由于Point2d继承自Point,所以在virtual table point2d中存储着:改写了的其中的~Point2d()、Point2d& mult( float )、float y() const,以及未被改写的Point::z()函数。

  • 虚函数表解析。含有虚函数或其父类含有虚函数的类,编译器都会为其添加一个虚函数表,vptr,先了解虚函数表的构成,有助对C++对象模型的理解。

  • 虚基类表解析。虚继承产生虚基类表(vbptr),虚基类表的内容与虚函数表完全不同,我们将在讲解虚继承时介绍虚函数表。

  • 对象模型概述:介绍简单对象模型、表格驱动对象模型,以及非继承情况下的C++对象模型。

  • 继承下的C++对象模型。分析C++类对象在下面情形中的内存布局:

class Base {public:

class Point3d: public Point2d { 
public: 
   Point3d( float x = 0.0, 
            float y = 0.0, float z = 0.0 ) 
      : Point2d( x, y ), _z( z ) {} 
   ~Point3d();

   // overridden base class virtual functions 
   Point3d& mult( float ); 
   float z() const { return _z; }

   // ... other operations ... 
protected: 
   float _z; 
};

  1. 单继承:子类单一继承自父类,分析了子类重写父类虚函数、子类定义了新的虚函数情况下子类对象内存布局。

  2. 多继承:子类继承于多个父类,分析了子类重写父类虚函数、子类定义了新的虚函数情况下子类对象内存布局,同时分析了非虚继承下的菱形继承。

  3. 虚继承:分析了单一继承下的虚继承、多重基层下的虚继承、重复继承下的虚继承。

virtual void f() { cout << "Base::f" << endl; }

3、在 Point3d的对象pt3d中,则有四个东西,一个是_x,一个是_vptr_Point,一个是_y,一个是_z。其中_vptr_Point指向着 virtual table point3d。由于point3d继承自point2d,所以在virtual table point3d中存储着:已经改写了的point3d的~Point3d(),point3d::mult()的函数地址,和z()函数的地址,以及未被改写的point2d的y()函数地址。

理解对象的内存布局之后,我们可以分析一些问题:

virtual void g() { cout << "Base::g" << endl; }

ok,上述1、2、3所有情况的详情,请参考下图。

  1. C++封装带来的布局成本是多大?

  2. 由空类组成的继承层次中,每个类对象的大小是多大?

virtual void h() { cout << "Base::h" << endl; }

竞博jbo电竞官网网址下载 1

至于其他与内存有关的知识,我假设大家都有一定的了解,如内存对齐,指针操作等。本文初看可能晦涩难懂,要求读者有一定的C++基础,对概念一有一定的掌握。

};

(图:virtual table(虚表)的布局:单一继承情况)

 

这样Class Base的函数虚表中就是三个函数指针,分别指向这三个对应的函数,但是我们如何获取这些函数指针的值。

本文,日后可能会酌情考虑增补有关内容。ok,更多,可参考深度探索c++对象模型一书第四章。 
最近几章难度都比较小,是考虑到狂想曲有深有浅的原则,后续章节会逐步恢复到相应难度。

3.理解虚函数表

我们声明一个对象Base base;

第四节、虚函数的布局与汇编层面的考察

3.1.多态与虚表

这样在对象base中的首地址开始的一个机器长度的空间是一个指针,指向的是虚表的首地址,其中虚表的首地址第一个函数指针就是指向class中的第一个声明的函数。然后对该函数指针++ 操作便可以指向下一个函数,这也正是可以通过函数指针访问private函数的方式。(防君子不防小人)。

      ivan、老梦的两篇文章继续对虚函数进行了一番深入,我看他们已经写得很好了,我就不饶舌了。ok,请看:1、VC虚函数布局引发的问题,2、从汇编层面深度剖析C++虚函数、http://blog.csdn.net/linyt/archive/2011/04/20/6336762.aspx。

       C++中虚函数的作用主要是为了实现多态机制。多态,简单来说,是指在继承层次中,父类的指针可以具有多种形态——当它指向某个子类对象时,通过它能够调用到子类的函数,而非父类的函数。

一般继承中是没有函数覆盖的,所以在虚表中首先是父类的函数指针,后面是子类的函数指针。

第五节、虚函数表的详解

#include<iostream>
using namespace std;


class Base{public: virtual void print(void); }; class Drive1:public Base{ public:virtual void print(void); }; class Drive2 :public Base{public: virtual void print(void); };
int main() { Base ptr1 = new Base; Base ptr2 = new Drive1; Base *ptr3 = new Drive2;
ptr1->print(); ptr2->print(); ptr3->print(); return 0; }

继承中有虚函数覆盖的情况,则子类中的Class虚表中对应的函数指针指向的是子类的函数地址,其他的不变。

本节全部内容来自淄博的共享,非常感谢。

这是一种运行期多态,即父类指针唯有在程序运行时才能知道所指的真正类型是什么。这种运行期决议,是通过虚函数表来实现的。

对于多重继承,则在对象的首地址前面有n个父类的虚表指针,分别指向对应父类的虚表,子类的函数地址存放在第一个父类的虚表中。这样是为了解决不同的父类指针指向同一个子类的对象的时候,可以调用实际对应的函数。

一般继承(无虚函数覆盖) 
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:

竞博jbo电竞官网网址下载 2

请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,

对于实例:Derive d; 的虚函数表如下:

竞博jbo电竞官网网址下载 3

我们可以看到下面几点: 
1)虚函数按照其声明顺序放于表中。 
2)父类的虚函数在子类的虚函数前面。 
我相信聪明的你一定可以参考前面的那个程序,来编写一段程序来验证。

一般继承(有虚函数覆盖) 
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。 
下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。

竞博jbo电竞官网网址下载 4

为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()  。 
那么,对于派生类的实例,其虚函数表会是下面的一个样子: 
竞博jbo电竞官网网址下载 5 
我们从表中可以看到下面几点, 
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。 
2)没有被覆盖的函数依旧。 
这样,我们就可以看到对于下面这样的程序, 
Base *b = new Derive();

b->f();

由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代, 
于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

多重继承(无虚函数覆盖)

下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系(注意:子类并没有覆盖父类的函数):

竞博jbo电竞官网网址下载 6

对于子类实例中的虚函数表,是下面这个样子:

竞博jbo电竞官网网址下载 7

我们可以看到: 
1) 每个父类都有自己的虚表。 
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

多重继承(有虚函数覆盖) 
下面我们再来看看,如果发生虚函数覆盖的情况。 
下图中,我们在子类中覆盖了父类的f()函数。

竞博jbo电竞官网网址下载 8

下面是对于子类实例中的虚函数表的图:

竞博jbo电竞官网网址下载 9

我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。 
这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:

Derive d; 
Base1 *b1 = &d; 
Base2 *b2 = &d; 
Base3 *b3 = &d; 
b1->f(); //Derive::f() 
b2->f(); //Derive::f() 
b3->f(); //Derive::f() 
b1->g(); //Base1::g() 
b2->g(); //Base2::g() 
b3->g(); //Base3::g()

安全性 
每次写C++的文章,总免不了要批判一下C++。 
这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。 
水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。

一、通过父类型的指针访问子类自己的虚函数 
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。 
虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:

Base1 *b1 = new Derive(); 
b1->g1(); //编译出错

任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,即基类指针不能调用子类自己定义的成员函数。所以,这样的程序根本无法编译通过。 
但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。 
(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)

二、访问non-public的虚函数 
另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中, 
所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。 
如:

class Base { 
private:  
virtual void f() { cout << "Base::f" << endl; }  
};

class Derive : public Base{  
}; 
typedef void(*Fun)(void); 
void main() { 
Derive d; 
Fun pFun = (Fun)*((int*)*竞博jbo电竞官网网址下载 ,(int*)(&d)+0); 
pFun();  
}

对上面粗体部分的解释(@a && x):

  1. (int*)(&d)取vptr地址,该地址存储的是指向vtbl的指针 
  2. (int*)*(int*)(&d)取vtbl地址,该地址存储的是虚函数表数组 
  3. (Fun)*((int*)*(int*)(&d) +0),取vtbl数组的第一个元素,即Base中第一个虚函数f的地址 
  4. (Fun)*((int*)*(int*)(&d) +1),取vtbl数组的第二个元素(这第4点,如下图所示)。

下图也能很清晰的说明一些东西(@5):

竞博jbo电竞官网网址下载 10

ok,再来看一个问题,如果一个子类重载的虚拟函数为privete,那么通过父类的指针可以访问到它吗?

#include <IOSTREAM>   
class B   
{     
public:     
    virtual void fun()       
    {      
        std::cout << "base fun called";      
    };     
};  

class D : public B    
{     
private:   
    virtual void fun()       
    {      
        std::cout << "driver fun called";     
    };     
};  

int main(int argc, char* argv[])   
{        
    B* p = new D();     
    p->fun();     
    return 0;     
}  
运行时会输出 driver fun called

从这个实验,可以更深入的了解虚拟函数编译时的一些特征: 
在编译虚拟函数调用的时候,例如p->fun(); 只是按其静态类型来处理的, 在这里p的类型就是B,不会考虑其实际指向的类型(动态类型)。 
    也就是说,碰到p->fun();编译器就当作调用B的fun来进行相应的检查和处理。 
因为在B里fun是public的,所以这里在“访问控制检查”这一关就完全可以通过了。 
然后就会转换成(*p->vptr[1])(p)这样的方式处理, p实际指向的动态类型是D, 
    所以p作为参数传给fun后(类的非静态成员函数都会编译加一个指针参数,指向调用该函数的对象,我们平常用的this就是该指针的值), 实际运行时p->vptr[1]则获取到的是D::fun()的地址,也就调用了该函数, 这也就是动态运行的机理。

为了进一步的实验,可以将B里的fun改为private的,D里的改为public的,则编译就会出错。 
C++的注意条款中有一条" 绝不重新定义继承而来的缺省参数值" 
(Effective C++ Item37, never redefine a function's inherited default parameter value) 也是同样的道理。

可以再做个实验 
class B   
{     
public:   
    virtual void fun(int i = 1)       
    {      
        std::cout << "base fun called, " << i;      
    };     
};  

class D : public B    
{     
private:     
    virtual void fun(int i = 2)       
    {      
        std::cout << "driver fun called, " << i;      
    };     
}; 

则运行会输出driver fun called, 1

关于这一点,Effective上讲的很清楚“virtual 函数系动态绑定, 而缺省参数却是静态绑定”, 
也就是说在编译的时候已经按照p的静态类型处理其默认参数了,转换成了(*p->vptr[1])(p, 1)这样的方式。

补遗

   一个类如果有虚函数,不管是几个虚函数,都会为这个类声明一个虚函数表,这个虚表是一个含有虚函数的类的,不是说是类对象的。一个含有虚函数的类,不管有多少个数据成员,每个对象实例都有一个虚指针,在内存中,存放每个类对象的内存区,在内存区的头部都是先存放这个指针变量的(准确的说,应该是:视编译器具体情况而定),从第n(n视实际情况而定)个字节才是这个对象自己的东西。

下面再说下通过基类指针,调用虚函数所发生的一切: 
One *p; 
p->disp();

1、上来要取得类的虚表的指针,就是要得到,虚表的地址。存放类对象的内存区的前四个字节其实就是用来存放虚表的地址的。 
2、得到虚表的地址后,从虚表那知道你调用的那个函数的入口地址。根据虚表提供的你要找的函数的地址。并调用函数;你要知道,那个虚表是一个存放指针变量的数组,并不是说,那个虚表中就是存放的虚函数的实体。

原文:

3.2.使用指针访问虚表

当出现覆盖的情况的时候,就会将这些函数虚表中的虚函数换成对应子类的虚函数地址,在调用的时候会根据父类指针的类型,分别调用不同的父类的虚函数,而对于多态,则调用子类中的虚函数,在三个对应的虚表中,所有的改虚函数指针都会被修改指向子类的虚函数地址。

如果我们丰富我们的Base类,使其拥有多个virtual函数:

 

class Base
{
public: 
    Base(int i) :baseI(i){};
    virtual void print(void){ cout << "调用了虚函数Base::print!"; }
    virtual void setI(){ cout << "调用的虚函数Base::setI!"; }
    virtual ~Base(){}
private:
    int baseI;
};

对于子类实例中的虚函数表,是下面这个样子:

      当一个类本身定义了虚函数,或其父类有虚函数时,为了支持多态机制,编译器将为该类添加一个虚函数指针(vptr)虚函数指针一般都放在对象内存布局的第一个位置上,这是为了保证在多层继承或多重继承的情况下能以最高效率取到虚函数表。

 

      当vprt位于对象内存最前面时,对象的地址即为虚函数指针地址。我们可以取得虚函数指针的地址:

我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:

Base b(1000);

int * vptrAdree = (int *)(&b);  

cout << "虚函数指针(vprt)的地址是:t"<<vptrAdree << endl;

Derive d;Base1 *b1 = &d;Base2 *b2 = &d;Base3 *b3 = &d;

我们运行代码出结果:虚函数指针vptr的地址是:        0018FB28

b1->f(); //Derive::f()

 

b2->f(); //Derive::f()

      我们强行把类对象的地址转换为 int* 类型,取得了虚函数指针的地址。虚函数指针指向虚函数表,虚函数表中存储的是一系列虚函数的地址,虚函数地址出现的顺序与类中虚函数声明的顺序一致。对虚函数指针地址值,可以得到虚函数表的地址,也即是虚函数表第一个虚函数的地址:

b3->f(); //Derive::f()

typedef void(*Fun)(void);

    Fun vfunc = (Fun)*( (int *)*(int*)(&b));

    cout << "第一个虚函数的地址是:" << (int *)*(int*)(&b) << endl;

    cout << "通过地址,调用虚函数Base::print():";

    vfunc();

b1->g(); //Base1::g()

 

b2->g(); //Base2::g()

  • 我们把虚表指针的值取出来: *(int*)(&b),它是一个地址,虚函数表的地址

  • 把虚函数表的地址强制转换成 int* : ( int *) *( int* )( &b )

  • 再把它转化成我们Fun指针类型 : (Fun )*(int *)*(int*)(&b)

b3->g(); //Base3::g()

这样,我们就取得了类中的第一个虚函数,我们可以通过函数指针访问它。

       存在的一些不友好的地方:任何使用父类的指针调用子类没有覆盖父类成员函数的行为都会被编译器视为非法行为,出现编译出错的问题。但是可以在运行期间通过指针的方式访问虚函数表来达到违反C++语义的行为。

 

正常的方式访问

 运行结果:

typedef void(*Fun)(void);//

第一个虚函数的地址是:009CDC80

这是一个函数指针,使用Func 来表示指向一个函数,该函数的返回值类型是 void,参数是void。使用方式: Func pFunc = NULL ; 指向一个函数指针

通过地址,调用虚函数Base::print():调用了虚函数Base::print!请按任意键继续. . .

    pFunc=(Fun)*((int*)*(int*)(&base));

同理,第二个虚函数setI()的地址为: 

class Base{

(int * )(*(int*)(&b)+1)

public:

 

    virtual void fun_a(void){

同样可以通过函数指针访问它,这里留给读者自己试验。

        cout << "base::fun_a" << endl;

到目前为止,我们知道了类中虚表指针vprt的由来,知道了虚函数表中的内容,以及如何通过指针访问虚函数表。下面的文章中将常使用指针访问对象内存来验证我们的C++对象模型,以及讨论在各种继承情况下虚表指针的变化,先把这部分的内容消化完再接着看下面的内容。

    }

 

    virtual void fun_b(void){

4.对象模型概述

        cout << "base::fun_b" << endl;

在C++中,有两种数据成员(class data members):static 和nonstatic,以及三种类成员函数(class member functions):static、nonstatic和virtual:

    }

现在我们有一个类Base,它包含了上面这5中类型的数据或函数:

    virtual void fun_c(void){

class Base
{
public:
    Base(int i) :baseI(i){};
    int getI(){ return baseI; }
    static void countI(){};
    virtual void print(void){ cout << "Base::print()"; }
    virtual ~Base(){}
private:
    int baseI;
    static int baseS;
};

        cout << "base::fun_c" << endl;

那么,这个类在内存中将被如何表示?5种数据都是连续存放的吗?如何布局才能支持C++多态? 我们的C++标准与编译器将如何塑造出各种数据成员与成员函数呢?

    }

 

private:

4.1.简单对象模型

    int a;

 

};

说明:在下面出现的图中,用蓝色边框框起来的内容在内存上是连续的。

 

 这个模型非常地简单粗暴。在该模型下,对象由一系列的指针组成,每一个指针都指向一个数据成员或成员函数,也即是说,每个数据成员和成员函数在类中所占的大小是相同的,都为一个指针的大小。这样有个好处——很容易算出对象的大小,不过赔上的是空间和执行期效率。想象一下,如果我们的Point3d类是这种模型,将会比C语言的struct多了许多空间来存放指向函数的指针,而且每次读取类的数据成员,都需要通过再一次寻址——又是时间上的消耗。所以这种对象模型并没有被用于实际产品上。

class Derive: public Base{

                 竞博jbo电竞官网网址下载 11 

public:

4.2.表格驱动模型

    virtual void fun_a(void){

      这个模型在简单对象模型的基础上又添加一个间接层,它把类中的数据分成了两个部分:数据部分与函数部分,并使用两张表格,一张存放数据本身,一张存放函数的地址(也即函数比成员多一次寻址),而类对象仅仅含有两个指针,分别指向上面这两个表。这样看来,对象的大小是固定为两个指针大小。这个模型也没有用于实际应用于真正的C++编译器上。

        cout << "derive::fun_a" << endl;

4.3.非继承下的C++对象模型

    }

      概述:在此模型下,nonstatic 数据成员被置于每一个类对象中,而static数据成员被置于类对象之外。static与nonstatic函数也都放在类对象之外,而对于virtual 函数,则通过虚函数表+虚指针来支持,具体如下:

    virtual void fun_b(void){

  • 每个类生成一个表格,称为虚表(virtual table,简称vtbl)。虚表中存放着一堆指针,这些指针指向该类每一个虚函数。虚表中的函数地址将按声明时的顺序排列,不过当子类有多个重载函数时例外,后面会讨论。

  • 每个类对象都拥有一个虚表指针(vptr),由编译器为其生成。虚表指针的设定与重置皆由类的复制控制(也即是构造函数、析构函数、赋值操作符)来完成。vptr的位置为编译器决定,传统上它被放在所有显示声明的成员之后,不过现在许多编译器把vptr放在一个类对象的最前端。关于数据成员布局的内容,在后面会详细分析。
    另外,虚函数表的前面设置了一个指向type_info的指针,用以支持RTTI(Run Time Type Identification,运行时类型识别)。RTTI是为多态而生成的信息,包括对象继承关系,对象本身的描述等,只有具有虚函数的对象在会生成。

        cout << "derive::fun_b" << endl;

先在VS上验证类对象的布局:

    }

Base b(1000);

    virtual void fun_d(void){

 竞博jbo电竞官网网址下载 12

        cout << "derive::fun_d" << endl;

可见对象b含有一个vfptr,即vprt。并且只有nonstatic数据成员被放置于对象内。我们展开vfprt:

    }

竞博jbo电竞官网网址下载 13 

private:

vfptr中有两个指针类型的数据(地址),第一个指向了Base类的析构函数,第二个指向了Base的虚函数print,顺序与声明顺序相同。

    int b;

 

};

这与上述的C++对象模型相符合。也可以通过代码来进行验证:

 

void testBase( Base&p)

{

    cout << "对象的内存起始地址:" << &p << endl;

    cout << "type_info信息:" << endl;

    RTTICompleteObjectLocator str = *((RTTICompleteObjectLocator*)*((int*)*(int*)(&p) - 1));

 

    string classname(str.pTypeDescriptor->name);

    classname = classname.substr(4, classname.find("@@") - 4);

    cout <<  "根据type_info信息输出类名:"<< classname << endl;

    cout << "虚函数表地址:" << (int *)(&p) << endl;

    //验证虚表

    cout << "虚函数表第一个函数的地址:" << (int *)*((int*)(&p)) << endl;

    cout << "析构函数的地址:" << (int* )*(int *)*((int*)(&p)) << endl;

    cout << "虚函数表中,第二个虚函数即print()的地址:" << ((int*)*(int*)(&p) + 1) << endl;

 

    //通过地址调用虚函数print()

    typedef void(*Fun)(void);

    Fun IsPrint=(Fun)* ((int*)*(int*)(&p) + 1);

    cout << endl;

    cout<<"调用了虚函数";

    IsPrint(); //若地址正确,则调用了Base类的虚函数print()

    cout << endl;

 

    //输入static函数的地址

    p.countI();//先调用函数以产生一个实例

    cout << "static函数countI()的地址:" << p.countI << endl;

 

    //验证nonstatic数据成员

    cout << "推测nonstatic数据成员baseI的地址:" << (int *)(&p) + 1 << endl;

    cout << "根据推测出的地址,输出该地址的值:" << *((int *)(&p) + 1) << endl;

    cout << "Base::getI():" << p.getI() << endl;

}

Base b(1000);

testBase(b);

int main(){

 竞博jbo电竞官网网址下载 14

 

结果分析:

    Derive derive;

  • 通过 (int *)(&p)取得虚函数表的地址

  • type_info信息的确存在于虚表的前一个位置。通过((int)(int*)(&p) – 1))取得type_infn信息,并成功获得类的名称的Base

  • 虚函数表的第一个函数是析构函数。

  • 虚函数表的第二个函数是虚函数print(),取得地址后通过地址调用它(而非通过对象),验证正确

  • 虚表指针的下一个位置为nonstatic数据成员baseI。

  • 可以看到,static成员函数的地址段位与虚表指针、baseI的地址段位不同。

 

好的,至此我们了解了非继承下类对象五种数据在内存上的布局,也知道了在每一个虚函数表前都有一个指针指向type_info,负责对RTTI的支持。而加入继承后类对象在内存中该如何表示呢?

    cout << "sizeof(derive):"<< sizeof(derive) << endl;

 

    cout << "derive  首地址:"  << &derive <<endl;

5.继承下的C++对象模型

    ((Fun)*((int*)*(int*)(&derive) ))();

 

    ((Fun)*((int*)*(int*)(&derive) + 1 ))();

5.1.单继承

    ((Fun)*((int*)*(int*)(&derive) + 2 ))();

如果我们定义了派生类

    ((Fun)*((int*)*(int*)(&derive) + 3 ))();

class Derive : public Base

{

public:

    Derive(int d) :Base(1000),   DeriveI(d){};

    //overwrite父类虚函数

    virtual void print(void){ cout << "Drive::Drive_print()" ; }

    // Derive声明的新的虚函数

    virtual void Drive_print(){ cout << "Drive::Drive_print()" ; }

    virtual ~Derive(){}

private:

    int DeriveI;

};

 

一个派生类如何在机器层面上塑造其父类的实例呢?在简单对象模型中,可以在子类对象中为每个类子对象分配一个指针。如下图:

    Base base;

 

    Fun pFun = NULL;

竞博jbo电竞官网网址下载 15竞博jbo电竞官网网址下载 16

    cout << "对象base的地址:" << &base << endl;

 

    cout << "第一个虚函数的地址:" <<  (int*)*(int*)(&base) << endl;

简单对象模型的缺点就是因间接性导致的空间存取时间上的额外负担,优点则是类的大小是固定的,基类的改动不会响子类对象的大小。

 

在表格驱动对象模型中,我们可以为子类对象增加第三个指针:基类指针(bptr),基类指针指向指向一个基类表(base class table),同样的,由于间接性导致了空间和存取时间上的额外负担,优点则是无须改变子类对象本身就可以更改基类。表格驱动模型的图就不再贴出来了。

    pFun = (Fun)*((int*)*(int*)(&base));

 

    pFun();

在C++对象模型中,对于一般继承(这个一般是相对于虚拟继承而言),若子类重写(overwrite)了父类的虚函数,则子类虚函数将覆盖虚表中对应的父类虚函数(注意子类与父类拥有各自的一个虚函数表);若子类并无overwrite父类虚函数,而是声明了自己新的虚函数,则该虚函数地址将扩充到虚函数表最后(在vs中无法通过监视看到扩充的结果,不过我们通过取地址的方法可以做到,子类新的虚函数确实在父类子物体的虚函数表末端)。而对于虚继承,若子类overwrite父类虚函数,同样地将覆盖父类子物体中的虚函数表对应位置,而若子类声明了自己新的虚函数,则编译器将为子类增加一个新的虚表指针vptr,这与一般继承不同,在后面再讨论。

    ((Fun)*((int*)*(int*)(&base) + 1))();

我们使用代码来验证以上模型

    ((Fun)*((int*)*(int*)(&base) + 2))();

 

    cout <<"sizeof(base):"<< sizeof(base) << endl;

typedef void(*Fun)(void);

int main()

{

    Derive d(2000);

    //[0]

    cout << "[0]Base::vptr";

    cout << "t地址:" << (int *)(&d) << endl;

        //vprt[0]

        cout << "  [0]";

        Fun fun1 = (Fun)*((int *)*((int *)(&d)));

        fun1();

        cout << "t地址:t" << *((int *)*((int *)(&d))) << endl;

 

        //vprt[1]析构函数无法通过地址调用,故手动输出

        cout << "  [1]" << "Derive::~Derive" << endl;

       //vprt[2]

        cout << "  [2]";

        Fun fun2 = (Fun)*((int *)*((int *)(&d)) + 2);

        fun2();

        cout << "t地址:t" << *((int *)*((int *)(&d)) + 2) << endl;

    //[1]

    cout << "[2]Base::baseI=" << *(int*)((int *)(&d)

  • 1);

    cout << "t地址:" << (int *)(&d) + 1;

    cout << endl;

    //[2]

    cout << "[2]Derive::DeriveI=" << *(int*)((int *)(&d) + 2);

    cout << "t地址:" << (int *)(&d) + 2;

    cout << endl;

    getchar();

}

    return 0;

 

}

运行结果:竞博jbo电竞官网网址下载 17

 

这个结果与我们的对象模型符合。

 

 

 

5.2.多继承

防君子不防小人的实现:

5.2.1一般的多重继承(非菱形继承)

       对于父类中的private and protected修饰的虚函数,在继承的时候,同样会存在在函数的虚表中,这样的话我们便可以通过访问虚函数的方式访问这些非public函数。

单继承中(一般继承),子类会扩展父类的虚函数表。在多继承中,子类含有多个父类的子对象,该往哪个父类的虚函数表扩展呢?当子类overwrite了父类的函数,需要覆盖多个父类的虚函数表吗?

typedef void(*Fun)(void);

  • 子类的虚函数被放在声明的第一个基类的虚函数表中。

  • overwrite时,所有基类的print()函数都被子类的print()函数覆盖。

  • 内存布局中,父类按照其声明顺序排列。

class Base{

其中第二点保证了父类指针指向子类对象时,总是能够调用到真正的函数。

private:

为了方便查看,我们把代码都粘贴过来

    virtual void fun_a(void){

class Base
{
public:
    Base(int i) :baseI(i){};
    virtual ~Base(){}
    int getI(){ return baseI; }
    static void countI(){};
    virtual void print(void){ cout << "Base::print()"; }
private:
    int baseI;
    static int baseS;
};
class Base_2
{
public:
    Base_2(int i) :base2I(i){};
    virtual ~Base_2(){}
    int getI(){ return base2I; }
    static void countI(){};
    virtual void print(void){ cout << "Base_2::print()"; }
private:
    int base2I;
    static int base2S;
};
class Drive_multyBase :public Base, public Base_2
{
public:
    Drive_multyBase(int d) :Base(1000), Base_2(2000), Drive_multyBaseI(d){};
    virtual void print(void){ cout << "Drive_multyBase::print"; }
    virtual void Drive_print(){ cout << "Drive_multyBase::Drive_print"; }   //虚函数表在哪额?  
private:
    int Drive_multyBaseI;
};

        cout << "base::fun_a" << endl;

此时Drive_multyBase 的对象模型是这样的:

    }

 

    virtual void fun_b(void){

 竞博jbo电竞官网网址下载 18

        cout << "base::fun_b" << endl;

我们使用代码验证:

    }

typedef void(*Fun)(void);

int main()

{

    Drive_multyBase d(3000);

    //[0]

    cout << "[0]Base::vptr";

    cout << "t地址:" << (int *)(&d) << endl;

        //vprt[0]析构函数无法通过地址调用,故手动输出

        cout << "  [0]" << "Derive::~Derive" << endl;

        //vprt[1]

        cout << "  [1]";

        Fun fun1 = (Fun)*((int *)*((int *)(&d))+1);

        fun1();

        cout << "t地址:t" << *((int *)*((int *)(&d))+1) << endl;

        //vprt[2]

        cout << "  [2]";

        Fun fun2 = (Fun)*((int *)*((int *)(&d)) + 2);

        fun2();

        cout << "t地址:t" << *((int *)*((int *)(&d)) + 2) << endl;

    //[1]

    cout << "[1]Base::baseI=" << *(int*)((int *)(&d)

  • 1);

    cout << "t地址:" << (int *)(&d) + 1;

    cout << endl;

  //[2]

    cout << "[2]Base_::vptr";

    cout << "t地址:" << (int *)(&d)+2 << endl;

        //vprt[0]析构函数无法通过地址调用,故手动输出

        cout << "  [0]" << "Drive_multyBase::~Derive" << endl;

        //vprt[1]

        cout << "  [1]";

        Fun fun4 = (Fun)*((int *)*((int *)(&d))+1);

        fun4();

        cout << "t地址:t" << *((int *)*((int *)(&d))+1) << endl;

    //[3]

    cout << "[3]Base_2::base2I=" << *(int*)((int *)(&d) + 3);

    cout << "t地址:" << (int *)(&d) + 3;

    cout << endl;

   //[4]

    cout << "[4]Drive_multyBase::Drive_multyBaseI=" << *(int*)((int *)(&d) + 4);

    cout << "t地址:" << (int *)(&d) + 4;

    cout << endl;

   getchar();

}

    virtual void fun_c(void){

 

        cout << "base::fun_c" << endl;

5.2.2 菱形继承

    }

     菱形继承也称为钻石型继承或重复继承,它指的是基类被某个派生类简单重复继承了多次。这样,派生类对象中拥有多份基类实例(这会带来一些问题)。为了方便叙述,我们不使用上面的代码了,而重新写一个重复继承的继承层次:

private:

class B

{

public:

    int ib;

public:

    B(int i=1) :ib(i){}

    virtual void f() { cout << "B::f()" << endl; }

    virtual void Bf() { cout << "B::Bf()" << endl; }

};

class B1 : public B

{

public:

    int ib1;

public:

    B1(int i = 100 ) :ib1(i) {}

    virtual void f() { cout << "B1::f()" << endl; }

    virtual void f1() { cout << "B1::f1()" << endl; }

    virtual void Bf1() { cout << "B1::Bf1()" << endl; } 

};

class B2 : public B

{

public:

    int ib2;

public:

    B2(int i = 1000) :ib2(i) {}

    virtual void f() { cout << "B2::f()" << endl; }

    virtual void f2() { cout << "B2::f2()" << endl; }

    virtual void Bf2() { cout << "B2::Bf2()" << endl; }

};

class D : public B1, public B2

{

public:

    int id;

public:

    D(int i= 10000) :id(i){}

    virtual void f() { cout << "D::f()" << endl; }

    virtual void f1() { cout << "D::f1()" << endl; }

    virtual void f2() { cout << "D::f2()" << endl; }

    virtual void Df() { cout << "D::Df()" << endl; }

};

    int a;

 

};

这时,根据单继承,我们可以分析出B1,B2类继承于B类时的内存布局。又根据一般多继承,我们可以分析出D类的内存布局。我们可以得出D类子对象的内存布局如下图:

 

    D类对象内存布局中,图中青色表示b1类子对象实例,黄色表示b2类子对象实例,灰色表示D类子对象实例。从图中可以看到,由于D类间接继承了B类两次,导致D类对象中含有两个B类的数据成员ib,一个属于来源B1类,一个来源B2类。这样不仅增大了空间,更重要的是引起了程序歧义:

class Derive: public Base{

 

private:

D d; 

d.ib =1 ;               //二义性错误,调用的是B1的ib还是B2的ib?

d.B1::ib = 1;           //正确

d.B2::ib = 1;           //正确

    int b;

尽管我们可以通过明确指明调用路径以消除二义性,但二义性的潜在性还没有消除,我们可以通过虚继承来使D类只拥有一个ib实体。

};

 

 

6.虚继承

int main(){

虚继承解决了菱形继承中最派生类拥有多个间接父类实例的情况。虚继承的派生类的内存布局与普通继承很多不同,主要体现在:

    Derive derive;

虚继承的子类,如果本身定义了新的虚函数,则编译器为其生成一个虚函数指针(vptr)以及一张虚函数表。该vptr位于对象内存最前面。

    Base* base = &derive;

  • vs非虚继承:直接扩展父类虚函数表。

    Fun pFun = NULL;

虚继承的子类也单独保留了父类的vprt与虚函数表。这部分内容接与子类内容以一个四字节的0来分界。

    pFun = (Fun)*(int*)*(int*)&(*base);

虚继承的子类对象中,含有四字节的虚表指针偏移值。

    pFun();

为了分析最后的菱形继承,我们还是先从单虚继承继承开始。

    ((Fun)* ((int*)*(int*)&(*base) + 1 ) )();

 

    ((Fun)* ((int*)*(int*)&(*base) + 2 ) )();

6.1.虚基类表解析

    return 0;

      在C++对象模型中,虚继承而来的子类会生成一个隐藏的虚基类指针(vbptr),在Microsoft Visual C++中,虚基类表指针总是在虚函数表指针之后,因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。

}

     一个类的虚基类指针指向的虚基类表,与虚函数表一样,虚基类表也由多个条目组成,条目中存放的是偏移值。第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由第一段的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。我们通过一张图来更好地理解。

虚基类表的第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值,这点我们在下面会验证。

 

6.2.简单虚继承 

如果我们的B1类虚继承于B类:

//类的内容与前面相同

class B{...}

class B1 : virtual public B

 

在我们的例子中,也就是B类实例内存地址相对于vbptr的偏移值,也即是:[4]-[1]的偏移值,结果即为12,从地址上也可以计算出来:007CFDFC-007CFDF4结果的十进制数正是12。现在,我们对虚基类表的构成应该有了一个更好的理解。

 

6.3.虚拟菱形继承

如果我们有如下继承层次:

class B{...}

class B1: virtual public  B{...}

class B2: virtual public  B{...}

class D : public B1,public B2{...}

菱形虚拟继承下,最派生类D类的对象模型又有不同的构成了。在D类对象的内存构成上,有以下几点: 

  • 在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)

  • D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔。

  • 编译器没有为D类生成一个它自己的vptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同。

  • 超类B的内容放到了D类对象内存布局的最后。

 

7.一些问题解答

7.1.C++封装带来的布局成本是多大?

在C语言中,“数据”和“处理数据的操作(函数)”是分开来声明的,也就是说,语言本身并没有支持“数据和函数”之间的关联性。

在C++中,我们通过类来将属性与操作绑定在一起,称为ADT,抽象数据结构。

C语言中使用struct(结构体)来封装数据,使用函数来处理数据。举个例子,如果我们定义了一个struct Point3如下:

typedef struct Point3

{

    float x;

    float y;

    float z;

} Point3;

为了打印这个Point3d,我们可以定义一个函数:

void Point3d_print(const Point3d *pd)

{

    printf("(%f,%f,%f)",pd->x,pd->y,pd_z);

}

而在C++中,我们更倾向于定义一个Point3d类,以ADT来实现上面的操作: 

看到这段代码,很多人第一个疑问可能是:加上了封装,布局成本增加了多少?答案是class Point3d并没有增加成本。学过了C++对象模型,我们知道,Point3d类对象的内存中,只有三个数据成员。

上面的类声明中,三个数据成员直接内含在每一个Point3d对象中,而成员函数虽然在类中声明,却不出现在类对象(object)之中,这些函数(non-inline)属于类而不属于类对象,只会为类产生唯一的函数实例 

所以,Point3d的封装并没有带来任何空间或执行期的效率影响。而在下面这种情况下,C++的封装额外成本才会显示出来:

  • 虚函数机制(virtual function) , 用以支持执行期绑定,实现多态。

  • 虚基类 (virtual base class) ,虚继承关系产生虚基类,用于在多重继承下保证基类在子类中拥有唯一实例。

不仅如此,Point3d类数据成员的内存布局与c语言的结构体Point3d成员内存布局是相同的。C++中处在同一个访问标识符(指public、private、protected)下的声明的数据成员,在内存中必定保证以其声明顺序出现。而处于不同访问标识符声明下的成员则无此规定。对于Point3类来说,它的三个数据成员都处于private下,在内存中一起声明顺序出现。我们可以做下实验:

//测试代码

    Point3d a(1,2,3);

    TestPoint3Member(a); style="color: #000000; line-height: 1.5;"> 

总结一下:

不考虑虚函数与虚继承,当数据都在同一个访问标识符下,C++的类与C语言的结构体在对象大小和内存布局上是一致的,C++的封装并没有带来空间时间上的影响。

 

 //无法调用