书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,咱们来看看编译器是怎么处理类成员内存分布的,特别是在继承、虚函数存在的状况下。函数
工欲善其事,必先利其器,咱们先用好Visual Studio工具,像下面这样一步一步来:工具
先选择左侧的C/C++->命令行,而后在其余选项这里写上/d1 reportAllClassLayout,它能够看到全部相关类的内存布局,若是写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。近期的VS版本都支持这样配置。布局
下面能够定义一个类,像下面这样:spa
1 class Base 2 { 3 int a; 4 int b; 5 public: 6 void CommonFunction(); 7 };
而后编译一下,能够看到输出框里面有这样的排布:命令行
这里不想花精力在内存对齐因素上,因此成员变量都设为int型。3d
从这里能够看到普通类的排布方式,成员变量依据声明的顺序进行排列(类内偏移为0开始),成员函数不占内存空间。指针
再看下继承,日后面添加以下代码:code
1 class DerivedClass: public Base 2 { 3 int c; 4 public: 5 void DerivedCommonFunction(); 6 };
编译,而后看到以下的内存分布(父类的内存分布不变,这里只讨论子类成员变量的内存分布):对象
能够看到子类继承了父类的成员变量,在内存排布上,先是排布了父类的成员变量,接着排布子类的成员变量,一样,成员函数不占字节。blog
下面给基类加上虚函数,暂时注释掉DerivedClass,看一下这时的内存排布:
1 class Base 2 { 3 int a; 4 int b; 5 public: 6 void CommonFunction(); 7 void virtual VirtualFunction(); 8 };
这个内存结构图分红了两个部分,上面是内存分布,下面是虚表,咱们逐个看。VS所带编译器是把虚表指针放在了内存的开始处(0地址偏移),而后再是成员变量;下面生成了虚表,紧跟在&Base1_meta后面的0表示,这张虚表对应的虚指针在内存中的分布,下面列出了虚函数,左侧的0是这个虚函数的序号,这里只有一个虚函数,因此只有一项,若是有多个虚函数,会有序号为1,为2的虚函数列出来。
编译器是在构造函数建立这个虚表指针以及虚表的。
那么编译器是如何利用虚表指针与虚表来实现多态的呢?是这样的,当建立一个含有虚函数的父类的对象时,编译器在对象构造时将虚表指针指向父类的虚函数;一样,当建立子类的对象时,编译器在构造函数里将虚表指针(子类只有一个虚表指针,它来自父类)指向子类的虚表(这个虚表里面的虚函数入口地址是子类的)。
因此,若是是调用Base *p = new Derived();生成的是子类的对象,在构造时,子类对象的虚指针指向的是子类的虚表,接着由Derived*到Base*的转换并无改变虚表指针,因此这时候p->VirtualFunction,其实是p->vfptr->VirtualFunction,它在构造的时候就已经指向了子类的VirtualFunction,因此调用的是子类的虚函数,这就是多态了。
下面加上子类,并在子类中添加虚函数,像下面这样:
1 class DerivedClass: public Base 2 { 3 int c; 4 public: 5 void DerivedCommonFunction(); 6 void virtual VirtualFunction(); 7 };
能够看到子类内存的排布以下:
上半部是内存分布,能够看到,虚表指针被继承了,且仍位于内存排布的起始处,下面是父类的成员变量a和b,最后是子类的成员变量c,注意虚表指针只有一个,子类并无再生成虚表指针了;下半部的虚表状况与父类是同样的。
咱们把子类换个代码,像这样:
1 class DerivedClass1 : public Base 2 { 3 int c; 4 public: 5 void DerivedCommonFunction(); 6 void virtual VirtualFunction2(); 7 };
注意到这时咱们并无覆写父类的虚方法,而是重声明了一个新的子类虚方法,内存分布以下:
仍是只有一个虚表指针,可是下方虚表的内容变化了,虚表的0号是父类的VirtualFunction,而1号放的是子类的VirtualFunction2。也就是说,若是定义了DerivedClass的对象,那么在构造时,虚表指针就会指向这个虚表,之后若是调用的是VirtualFunction,那么会从父类中寻找对应的虚函数,若是调用的是VirtualFunction2,那么会从子类中寻找对应的虚函数。
咱们再改造一会儿类,像这样:
1 class DerivedClass1 : public Base 2 { 3 int c; 4 public: 5 void DerivedCommonFunction(); 6 void virtual VirtualFunction(); 7 void virtual VirtualFunction2(); 8 };
咱们既覆写父类的虚函数,也有新添的虚函数,那么能够料想的到,是下面的这种内存分布:
下面来讨论多重继承,代码以下:
1 class Base 2 { 3 int a; 4 int b; 5 public: 6 void CommonFunction(); 7 void virtual VirtualFunction(); 8 }; 9 10 11 class DerivedClass1: public Base 12 { 13 int c; 14 public: 15 void DerivedCommonFunction(); 16 void virtual VirtualFunction(); 17 }; 18 19 class DerivedClass2 : public Base 20 { 21 int d; 22 public: 23 void DerivedCommonFunction(); 24 void virtual VirtualFunction(); 25 }; 26 27 class DerivedDerivedClass : public DerivedClass1, public DerivedClass2 28 { 29 int e; 30 public: 31 void DerivedDerivedCommonFunction(); 32 void virtual VirtualFunction(); 33 };
内存分布从父类到子类,依次以下:
Base中有一个虚表指针,地址偏移为0
DerivedClass1继承了Base,内存排布是先父类后子类。
DerivedClass2的状况是相似于DerivedClass1的。
下面咱们重点看看这个类DerivedDerivedClass,由外向内看,它并列地排布着继承而来的两个父类DerivedClass1与DerivedClass2,还有自身的成员变量e。DerivedClass1包含了它的成员变量c,以及Base,Base有一个0地址偏移的虚表指针,而后是成员变量a和b;DerivedClass2的内存排布相似于DerivedClass1,注意到DerivedClass2里面居然也有一份Base。
这里有两份虚表了,分别针对DerivedClass1与DerivedClass2,在&DerivedDericedClass_meta下方的数字是首地址偏移量,靠下面的虚表的那个-16表示指向这个虚表的虚指针的内存偏移,这正是DerivedClass2中的{vfptr}在DerivedDerivedClass的内存偏移。
若是采用虚继承,像下面这样:
1 class DerivedClass1: virtual public Base 2 { 3 int c; 4 public: 5 void DerivedCommonFunction(); 6 void virtual VirtualFunction(); 7 }; 8 9 class DerivedClass2 : virtual public Base 10 { 11 int d; 12 public: 13 void DerivedCommonFunction(); 14 void virtual VirtualFunction(); 15 }; 16 17 class DerivedDerivedClass : public DerivedClass1, public DerivedClass2 18 { 19 int e; 20 public: 21 void DerivedDerivedCommonFunction(); 22 void virtual VirtualFunction(); 23 };
Base类没有变化,但往下看:
DerivedClass1就已经有变化了,原来是先排虚表指针与Base成员变量,vfptr位于0地址偏移处;但如今有两个虚表指针了,一个是vbptr,另外一个是vfptr。vbptr是这个DerivedClass1对应的虚表指针,它指向DerivedClass1的虚表vbtable,另外一个vfptr是虚基类表对应的虚指针,它指向vftable。
下面列出了两张虚表,第一张表是vbptr指向的表,8表示{vbptr}与{vfptr}的偏移;第二张表是vfptr指向的表,-8指明了这张表所对应的虚指针位于内存的偏移量。
DerivedClass2的内存分布相似于DerivedClass1,一样会有两个虚指针,分别指向两张虚表(第二张是虚基类表)。
下面来仔细看一下DerivedDerivedClass的内存分布,这里面有三个虚指针了,但base却只有一份。第一张虚表是内含DerivedClass1的,20表示它的虚指针{vbptr}离虚基表指针{vfptr}的距离,第二张虚表是内含DerivedClass2的,12表示它的虚指针{vbptr}离虚基表指针{vfptr}的距离,最后一张表是虚基表,-20指明了它对应的虚指针{vfptr}在内存中的偏移。
虚继承的做用是减小了对基类的重复,代价是增长了虚表指针的负担(更多的虚表指针)。
下面总结一下(当基类有虚函数时):
1. 每一个类都有虚指针和虚表;
2. 若是不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)。有多少个虚函数,虚表里面的项就会有多少。多重继承时,可能存在多个的基类虚表与虚指针;
3. 若是是虚继承,那么子类会有两份虚指针,一份指向本身的虚表,另外一份指向虚基表,多重继承时虚基表与虚基表指针有且只有一份。