前一段时间被问到过一个问题,当时模模糊糊,就是说不清楚,问题问到说:什么情况下会将基类的析构函数定义成虚函数?
当时想到 如果子类B继承了父类A,那么定义出一个子类对象b,析构时,调用完子类析构函数,不是自动调用父类的析构函数吗!干嘛还要把定义为虚函数。将基类析构函用到了数定义成虚函数,难道是也是为了实现多态?。。 额,现在想想,其实自己都想到多态了,可惜还是没加点劲想到点上。这个问题用到了多态的原理。首先鉴于父子类的析构函数底层其实是同名(编译器做了特殊处理,都叫destructor),然后它们又是虚函数的话,便构成重写,达到了多态的条件;而如果基类析构不是虚函数,而恰好又要delete一个指向子类的基类指针时,此时函数对象按类型调用,于是便会只调用基类析构,未调用子类析构函数而产生内存泄漏。如
1 #include<iostream> 2 using namespace std; 3 4 class A 5 { 6 public: 7 ~A() 8 { 9 cout<<"~A()"<<endl; 10 } 11 protected: 12 int _a; 13 14 }; 15 class B:public A 16 { 17 public: 18 ~B() 19 { 20 cout<<"~B()"<<endl; 21 } 22 private: 23 int _b; 24 }; 25 int main(void) 26 { 27 A* p = new B; 28 delete p; 29 return 0; 30 }
按《Effective C++》中的观点其实是:只要一个类有可能会被其它类所继承, 析构函数就应该声明是虚析构函数。
那为什么定义成虚析构函数就能解决这个问题呢?
因为实现了多态。此时子类对象模型里父类析构函数被覆盖,(编译器依旧能知晓父类析构)当父类指针/引用指向父类对象时,调用的是父类的虚函数,指向子类对象时调用的是子类的虚函数;所以析构函数被定义为虚函数就不难理解了。
那多态底层又是怎么实现的呢?来探索一下。
多态底层实现
多态实现利用到了一个叫虚函数表(虚表V-table)的东西。它是一块虚函数的地址表,通过一块连续内存来存储虚函数的地址。这张表解决了继承、虚函数(重写)的问题。在有虚函数的对象实例中都存在一张虚函数表,虚函数表就像一张地图,指明了实际应该调用的虚函数函数。
Vs2008下,虚表(v-table)大致是这样,
简化后就像这样
注意 :
①每个虚表后面都有一个‘0’,它类似字符串的‘ ’,用来标识虚函数表的结尾。结束标识在不同的编译器下可能会有所不同。
②不难发现虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
多态是如何利用虚表
假使实现这样一个单继承,Derive继承Base,
1 class Base 2 { 3 public : 4 virtual void func1() 5 { 6 cout<<"Base::func1" <<endl; 7 } 8 virtual void func2() 9 { 10 cout<<"Base::func2" <<endl; 11 } 12 private : 13 int a ; 14 }; 15 class Derive : public Base 16 { 17 public : 18 virtual void func1() 19 { 20 cout<<"Derive::func1" <<endl; 21 } 22 virtual void func3() 23 { 24 cout<<"Derive::func3" <<endl; 25 } 26 virtual void func4() 27 { 28 cout<<"Derive::func4" <<endl; 29 } 30 private : 31 int b ; 32 };
不难发现子类对象模型中继承的基类部分存了一个虚表指针,它又指向了一个虚表,这个虚表里面的值(虚函数地址)也从父类继承过来。
但是注意几个地方
1.子类重写了的虚函数会覆盖它虚表中原来存放基类虚函数地址的值(而且我们通常也需要构成覆盖,因为没有覆盖,不实现多态,那定义出虚函数又创建出虚表就没有意义了)
2.没有被覆盖的虚函数,在虚表中保持原有状态(这个地方 Vs下监视窗口没有显示f3,f4是vs的bug)
3.同类对象的虚表指针指向同一张虚表(同类的对象,大小一样,指向同一张虚表便减少开销)
大致可以这样判断
多继承情况
像这种继承关系
1 class A 2 { 3 public : 4 virtual void f1() 5 { 6 cout<<"A::f1" <<endl; 7 } 8 9 virtual void func2() 10 { 11 cout<<"A::f2" <<endl; 12 } 13 14 protected : 15 int _a ; 16 }; 17 class B 18 { 19 public : 20 virtual void func1() 21 { 22 cout<<"B::f1" <<endl; 23 } 24 25 virtual void func2() 26 { 27 cout<<"B::f2" <<endl; 28 } 29 30 protected : 31 int _b ; 32 }; 33 34 class C : public A, public B 35 { 36 public : 37 virtual void func1() 38 { 39 cout<<"C::f1" <<endl; 40 } 41 42 virtual void func3() 43 { 44 cout<<"C::f3" <<endl; 45 } 46 47 private: 48 int _c ; 49 }; 50 51 void Test1 () 52 { 53 C c; 54 55 }
按照前面的情况,不难得到上面的对象模型,从中注意到
①子类的虚函数c::f1()同时覆盖虚表1和虚表2中被重写的虚函数
②子类里不构成重写的虚函数的地址会按继承顺序放到第一个父类的虚表中。
这样做就为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
提到多继承就不得不想到棱形继承,那如果是棱形继承的方式,对象模型又是怎么样的呢?继续探索探索
菱形继承情况
先看看普通菱形继承(通常这种继承因冗余和二义性没有意义,但可以用它来作对比,方便理解对象模型)
void Test1() { D d; d.B::_a = 0; d.C::_a = 1 d._b = 2; d._c = 3; d._d = 4; }
和上面多继承有着相同特点。最终子类D::f1()覆盖了两个虚表中被重写的B::f1()和C::f1(); 并且虚表指针都在相应对象模型的前面。
现在有了普通的菱形继承对象模型。我们又知道解决菱形继承的两大缺陷会用到虚继承,于是当以虚继承方式的棱形继承的对象模型又是怎么呢,怎么完成的多态?再来探索探索。。
1 class A 2 { 3 public: 4 virtual void f1() 5 {} 6 7 virtual void f2() 8 {} 9 10 int _a; 11 }; 12 13 14 class B : virtual public A 15 { 16 public: 17 virtual void f1() 18 {} 19 20 //virtual void f3() 21 //{} 22 23 int _b; 24 }; 25 26 class C : virtual public A 27 { 28 public: 29 virtual void f1() 30 {} 31 32 //virtual void f3() 33 //{} 34 35 int _c; 36 }; 37 38 class D : public B, public C 39 { 40 public: 41 virtual void f1() 42 { 43 cout<<"D:f1()"<<endl; 44 } 45 46 virtual void f4() 47 { 48 cout<<"D:f4()"<<endl; 49 } 50 51 int _d; 52 }; 53 54 void Test1() 55 { 56 D d; 57 d._a = 1; 58 d._b = 2; 59 d._c = 3; 60 d._d = 4; 61 62 }
进入调试
从图可以大致推敲出子类对象模型。现在有一个问题是,因为是虚继承,那么模型里面就会有一个虚基表指针,指向虚基表(里面存放偏移量),那模型里面虚表指针以及虚基表指针又是怎么布局呢?按前面经验,编译器为了高性能,通常把虚表指针放最前面,大小相近的应该是同一种指针,大胆猜测一下它布局
调试查看内存图,基本可以确认此种对象模型。但这是类B和类C的虚函数f1() 都被D重写的情况,当它们存在没有被子类重写的虚函数时,这些虚函数又会存在哪个虚表?
比如此时加上B::f3() C::f3()
1 class A 2 { 3 public: 4 virtual void f1() 5 {} 6 7 virtual void f2() 8 {} 9 10 int _a; 11 }; 12 13 14 class B : virtual public A 15 { 16 public: 17 virtual void f1() 18 {} 19 20 virtual void f3() //有一个不被重写虚函数 21 {} 22 23 int _b; 24 }; 25 26 class C : virtual public A 27 { 28 public: 29 virtual void f1() 30 {} 31 32 virtual void f3() //不被重写的虚函数 33 {} 34 35 int _c; 36 }; 37 38 class D : public B, public C 39 { 40 public: 41 virtual void f1() 42 { 43 cout<<"D:f1()"<<endl; 44 } 45 46 virtual void f4() 47 { 48 cout<<"D:f4()"<<endl; 49 } 50 51 int _d; 52 }; 53 54 void Test1() 55 { 56 D d; 57 d._a = 1; 58 d._b = 2; 59 d._c = 3; 60 d._d = 4; 61 62 }
再查看内存图,最后大致画得如下模型图
试着从这些图中总结菱形继承时的规律:
1.有几个“含有虚表的父类”,子类就有几个虚表
2. 遵循“先继承那个父类,就把子类虚函数地址放在那个父类对应的虚表中”
3.利用虚继承方式时:把父类的虚函数指针放在一个公共区,并把公共区地址放在子类对象里的最后面。
①有几个“虚继承同一个类的”父类(如B,C),子类就有几个虚基表指针
②原始基类里虚函数会被放到公共区(最高位虚表)当中,若当中某个虚函数被重写,就将其覆盖。其子类通过虚基表里 面的偏移量来找到虚表指针(公共区),进而实现多态
③类里未参与重写的虚函数不会放到公共区,而是存在对象模型中自己相对应的那个虚表里面