C++ 虚函数内存布局

由sizeof()求带有虚函数的类的大小引发的思考,在查看相关资料的过程中看到了陈皓先生的一篇文章《C++ 虚函数表解析》(详情见http://blog.csdn.net/haoel)觉得讲的十分的好,只是看完之后仍对为什么要为什么可以由父类的指针调用子类的对象的虚函数不解,如这种典型的描述:

Derive d;//Derive 是Base的子类

Base *b1 = &d;//这必须使用父类的指针???稍后将会揭开谜底

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

因为毕竟子类继承了父类,对父类进行了扩充,父类可能没有子类有的特性,那么该如何来分配呢?我想丢失子类特有的部分是必然的,那到底在内存上如何分布的呢?本文将对陈皓先生的文章有所补充。

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术(所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。)

言归正传,让我们一起进入虚函数的世界。

 

一、虚函数表 

C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的,简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

这里我们着重看一下这张虚函数表。C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表有最高的性能——如果有多层继承或是多重继承的情况下)。这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

这儿先来看看虚函数表的指针的内存布局,具体看下例

#include <stdio.h>

class simpleClass{

public:

  static int nCount;

  int nValue;

  char c;

 

  simpleClass(){};

  virtual ~simpleClass(){};

 

  int getValue(void);

  virtual void foo(void){};

  static void addCount();

};

 

int main()

{

  simpleClass aSimple;

  printf("Object start address:   %x\n",&aSimple);

  printf("nValue address:         %x\n",&aSimple.nValue );

  printf("c address:              %x\n",&aSimple.c );

  printf("size: %d\n",sizeof(simpleClass));

  return 0;

}

所得结果如图所示:

 C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

如是乎一个简单的C++对象的内存布局展示出来了:

C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

由以上分析可以证明以下结论:

虚函数表的指针存在于对象实例中最前面的位置(我使用的是VC6,至于其他的编译器嘛,个人能力有限,有待以后考查)。

【除此之外还包含有对其他许多知识点的验证:

1 非静态数据成员是影响对象占据内存大小的主要因素,随着对象数目的增加,非静态数据成员占据的内存会相应增加。

2  所有的对象共享一份静态数据成员,所以静态数据成员占据的内存的数量不会随着对象数目的增加而增长。

3 静态数据成员和非静态数据成员不会影响对象内存的大小,虽然其实现会占据相应的内存空间,同样也不会随着对象数目的增加而增加。】

还有一点需要补充的是:所有的虚函数共享同一张虚函数表。

在上述main函数中添加如下几行:

typedef void(*Fun)(void);

Fun pFun = NULL;

pFun =(Fun)*((int*)*(int*)(&aSimple)+1);

pFun();

所得结果如下图:

C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

我想通过以上分析你应该对虚函数表有个大概的了解了吧,让我们继续吧。

 

二、单继承(无虚函数覆盖)

假设有如下所示的一个继承关系:

C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

请注意,在这个继承关系中,子类没有覆盖任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:

假设我们有这样的一个类:

class Base {

   public:

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

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

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

 

};

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

C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

我们可以看到下面几点:

1)虚函数按照其声明顺序放于表中。

2)父类的虚函数在子类的虚函数前面。

因为在实际的应用中没有人会如此无聊的去定义这样的派生类Derive,所以读者可以自己尝试的写写验证程序,类似第一节中的pFun

 

三、单继承(有虚函数覆盖)

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

C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f ()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:

C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

我们从表中可以看到下面几点,

1)覆盖的f ()函数被放到了虚表中原来父类虚函数的位置。

2)没有被覆盖的函数依旧。

这样,我们就可以看到对于下面这样的程序,

      Base *b = new Derive();

     b->f();

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

验证程序代码如下:

#include <stdio.h>

class simpleClass{

public:

   static int nCount;

   int nValue;

   char c;

   simpleClass(){};

   virtual ~simpleClass(){};

   int getValue(void);

   virtual void foo(void){};

   static void addCount();

};

class derivedClass:public simpleClass

{

public:

   int nSubValue;

   derivedClass() {printf("creat derived class.\n\n");};

   ~derivedClass() {printf("destroy derived class.\n");};

   virtual void foo(void) {printf("foo in derivedClass\n");};

};

int simpleClass::nCount =0;

int main()

{

   derivedClass aSimple;

   printf("Object start address:   %x\n",&aSimple);

   printf("nValue address:         %x\n",&aSimple.nValue );

   printf("c address:              %x\n",&aSimple.c );

   printf("nSubValue address:      %x\n",&aSimple.nSubValue );

   printf("baseClass size: %d, derivedClass size:%d\n\n",sizeof(simpleClass),sizeof(derivedClass));

   return 0;

}

VC6运行结果如图:

C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

由此得出单继承对象的内存布局如下:

C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

 

我想在文章的开篇所提到的为什么可以的问题我们已经得到答案了(此时无声胜有声)。

Go on!

 

 

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

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

C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

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

C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

我们可以看到:

1  每个父类都有自己的虚表。

2  子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

 

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

 

 

 

五、多重继承(有虚函数覆盖)

下面我们再来看看,如果发生虚函数覆盖的情况。

 

下图中,我们在子类中覆盖了父类的f ()函数。

C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

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

C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

我们可以看见,三个父类虚函数表中的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()

 

验证程序的代码如下:

#include <stdio.h>

class simpleClass1{

public:

     int nValue1;

     char c;

 

     simpleClass1(){printf("creat simpleClass1.\n");};

     virtual ~simpleClass1(){printf("destroy simpleClass1.\n");};

 

     int getValue(void);

     virtual void foo1(void){};

};

 

class simpleClass2{

public:

     int nValue2;

 

     simpleClass2(){printf("creat simpleClass2.\n");};

     virtual ~simpleClass2(){printf("destroy simpleClass2.\n");};

 

     int getValue(void);

     virtual void foo2(void){};

};

 

class derivedClass:public simpleClass1,public simpleClass2

{

public:

     int nSubValue;

     derivedClass() {printf("creat derived class.\n\n");};

     ~derivedClass() {printf("destroy derived class.\n");};

     virtual void foo2(void) {};

};

 

 

int main()

{

     derivedClass aSimple;

 

     printf("Object start address:   %x\n",&aSimple);

     printf("nValue1 address:         %x\n",&aSimple.nValue1 );

     printf("c address:              %x\n",&aSimple.c );

     printf("nValue2 address:        %x\n",&aSimple.nValue2);

     printf("nSubValue address:      %x\n",&aSimple.nSubValue );

     printf("simpleClass1 size: %d,simpleClass2 size: %d, derivedClass size:%d\n\n",

          sizeof(simpleClass1),sizeof(simpleClass2),sizeof(derivedClass));

 

     return 0;

}

运行结果如下:

C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

 

通过运行结果可以得到多重继承对象的布局如下:

C++ 虚函数内存布局学习笔记 - coolwater2008 - Program for GameGIS

 

现在是时候回答为什么要的问题了,一个父类可以有多个子类,比如一个shape类可以有circle, rectangle, triangle... 子类,他们都要overwrite父类的纯虚函数draw(),而一个图上有若干个各种图形,在程序中我们用一个shape的指针数组来保存这些不同的图形,于是就可以调用不同图像对象对应的draw()函数现实绘图,而且以后如果想添加新的图形对象也不需要重写代码!!!(

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭