第七章 多态
-
多态:对于同一种指令,对于不同的对象,产生不同的行为。
-
多态的类型:
静态多态:函数重载,运算符重载,模板,发生的时机是在编译的时候;
动态多态:发生的时机是在运行的时候,通过虚函数来实现。
-
虚函数:在成员函数前加上virtual关键字。
如果一个基类的成员函数定义为虚函数,那么它在所有派生类中也保持为虚函数,即使在派生类中省略了virtual关键字,也仍然是虚函数。派生类要对虚函数进行中可根据需要重定义,重定义的格式有一定的要求:
与基类的虚函数有相同的参数个数;
与基类的虚函数有相同的参数类型;
与基类的虚函数有相同的返回类型。
-
在函数最后“override”关键字,是重定义/重写的标记。
-
虚函数实现多态的原理
当基类定义虚函数的时候,就会在基类对象的存储布局的前面多一个虚函数指针,该虚函数指针指向基类自己的虚函数表(虚表),该虚表存放的是基类的虚函数的入口地址;当派生类继承基类的时候,会把基类的虚函数吸收过来,此时派生类也就有虚函数,有虚函数就有虚函数指针,该虚函数指针指向派生类自己的虚函数表,该虚表存放的是派生类从基类吸收来的虚函数的入口地址,如果此时派生类重写了该虚函数,那么在虚函数表中,派生类重写的虚函数的入口地址会覆盖掉从基类吸收过来的虚函数的入口地址。
基类指针哪怕指向的是派生类对象,它所能访问的数据成员也只是占内存sizeof(基类)的大小的那一部分。
-
多态激活的条件(面试)
- 基类要定义虚函数;
- 派生类要重定义(重写)该虚函数;
- 创建派生类对象;
- 用基类的指针指向(引用绑定到)派生类的对象;
- 基类的指针(引用)去调用虚函数。
-
哪些函数不能设为虚函数:
- 普通函数(非成员函数):是一个非成员函数,所以不能定义为虚函数。
- 静态成员函数:发生时机在编译时候,虚函数体现多态在运行时;静态成员函数没有this。
- 内联成员函数:发生时机在编译时候,虚函数体现多态在运行时;内联成员函数前面加上virtual关键字,编译器不会报错,但是此时已经失去内联的意义。
- 友元函数:如果友元函数本身是一个非成员函数(普通函数)的时候,是不能被设计成虚函数的;但是如果友元函数本身是一个成员函数的时候,是可以设计成虚函数。
- 构造函数:发生时机在编译时候,虚函数体现多态在运行时;如果构造函数是虚函数,虚函数的调用会满足虚函数调用的逻辑结构,会入虚表,会有虚函数指针,而构造函数的作用是为了初始化数据成员的,此时构造函数没有调用完成,就证明对象还没有构建完成,那虚函数指针就不一定存在,就调不到虚表,所以构造函数不能被设计为虚函数。
-
虚函数的访问
编译时多态:静态联编,早期联编
运行时多态:动态多态,晚期联编
指针访问:动态联编
引用访问:动态联编
对象访问:静态联编,和调用普通成员函数一样。
成员函数中访问:动态联编,成员函数中会传this指针进去,以this指针来调用虚函数,所以满足多态。看this指针是指向基类对象还是派生类对象。
//基类类内: void func1() { //Base *pbase2 = &derived1; //Base * const this = &derived1; this->print();//体现动态多态 } //派生类类内: void print() const override//重写,重定义 { cout << "Derived::_derived = " << _derived << endl; } //测试: Base *pbase = &derived1; pbase->func1();
构造函数和析构函数中访问:静态联编,调用的虚函数是自己类中定义的函数,如果在自己的类中没有实现该函数,则调用的是基类中的虚函数。但绝不会调用任何在派生类中重定义的虚函数。
思考构造函数跟析构函数中调用为什么会调用自己类中的函数?从生命周期来考虑。
-
动态多态和虚函数是否等价?
不等价,动态多态的体现必须要有虚函数,但是调用虚函数不一定能体现动态多态。
-
抽象类
声明了纯虚函数的类称为抽象类,抽象类不能创建对象。
//example成员函数: virtual void display() = 0;
如果派生类继承自抽象类,但是派生类没有将抽象类中的所有纯虚函数进行实现的话,该派生类也是一个抽象类,该抽象类也不能创建对象。
如果一个类的构造函数被protected进行修饰,那么该类就是抽象类,抽象类不能创建对象,但是可以创建指针。[此时构造函数在类外定义对象时不能被调用,但是它的派生类可以调用]
-
虚析构函数
当基类的析构函数被设计虚函数的时候,派生的析构函数会被自动变成虚函数。此时对于编译器而言,就是一个虚函数的重写。对于程序员而言,属于一个名字不一样的重写。编译器会将析构函数解释为destructor,所以编译就认为析构函数就是重写了。析构函数一个类只有一个,具有唯一性。
//基类析构函数 virtual ~Base() //~destructor { cout << "~Base()" << endl; if(_pbase) { delete [] _pbase; _pbase = nullptr; } } //派生类析构函数 ~Derived() //~destructor { cout << "~Derived()" << endl; if(_pderived) { delete [] _pderived; _pderived = nullptr; } } //测试 int main(int argc, char **argv) { Base *pbase = new Derived("hello", "world"); pbase->print(); delete pbase; //执行析构函数 pbase->~Base(); //pbase->~destructor() //delete dynamic_cast<Derived *>(pbase); return 0; }
-
重载、隐藏、覆盖
-
重载:发生在同一个作用域中,函数名称相同,但参数的类型、个数、顺序不同(参数列表不一样)
-
覆盖:发生在基类与派生类之间,同名虚函数,参数列表完全相同。可以理解成派生类吸收了基类的虚函数,所以派生类就有了虚函数,有虚函数就会有自己的虚函数指针和虚函数表,虚函数指针指向自己的虚函数表,基类的虚函数指针和派生类的虚函数指针的值始终不一样,指向各自的虚函数表(基类和派生类各有一张虚函数表),当派生类没有重写基类的虚函数时,派生类虚函数指针指向的虚表中存放着基类的虚函数的入口地址,一旦派生类重写了虚函数,派生类的虚函数的入口地址就会覆盖掉派生类的虚表中的基类的虚函数的入口地址。
在派生类没有重写且新增虚函数的情况下:
Base base; Derive derive; printf("%p\n",(long *)&base); printf("%p\n",(long *)&derive); printf("base的虚函数指针的值:%p\n",(long *)*(long *)&base); printf("derive的虚函数指针的值:%p\n",(long *)*(long *)&derive); printf("base的第一个虚函数的入口地址:%p\n",(long *)*(long *)*(long *)&base); printf("derive的第一个虚函数指针的入口地址:%p\n",(long *)*(long *)*(long *)&derive);
-
隐藏:发在基类与派生类之间,非虚同名函数,不管参数列表是啥情况,派生类的同名函数会屏蔽基类同名函数,无法调用,必须通过类名调用。如果基类与派生类中有同名数据成员,基类同名数据成员也会被隐藏。
-
通过对象或者指针调用非虚函数;
-
通过对象调用虚函数。
class Base { public: Base() : _base(0) { cout << "Base()" << endl; } explicit Base(long base) : _base(base) { cout << "Base(long)" << endl;} void display() const { cout << "...Base::_base :" << _base << endl; } virtual ~Base() { cout << "~Base()" << endl; } protected: long _base; }; class Derived : public Base { public: Derived(long derived, long base) : Base(111) , _derived(derived) , _base(base) { cout << "Derived(long)" << endl; } ~Derived() { cout << "~Derived()" << endl; } void display(int x) const { cout << "Base::_base:" << Base::_base << endl; cout << "Derived::_base: " << _base << endl; cout << "Derived::_derived:" << _derived<< endl; } private: long _derived; long _base; }; int main(int argc, char **argv) { //栈空间对象 Derived d(10, 100); //d.display();//隐藏了基类的同名函数 d.Base::display();//直接使用基类的作用域才能访问到其同名函数 d.display(1); return 0; }
-
-
-
测试虚表的存在
通过一个二级指针的用法,是能够通过编程验证虚函数表的存在。
void test0() { Derived d(10, 100); long *pvtable = (long*)&d; cout << pvtable[0] << endl;//虚函数表的首地址 cout << pvtable[1] << endl; long * pvtable2 = (long*) pvtable[0]; cout << pvtable2[0] << endl;//程序代码区中第一个虚函数的地址 cout << pvtable2[1] << endl; cout << pvtable2[2] << endl; //函数类型: 返回值 参数列表 typedef void (*Function)(); typedef void (*Function2)(Derived*); Function f1 = (Function)pvtable2[0]; f1();//在这里确实没有传this指针 f1 = (Function)pvtable2[1]; f1(); f1 = (Function)pvtable2[2]; f1(); cout << endl; Function2 f2 = (Function2)pvtable2[0]; f2(&d); }
另外一种写法:
//虚表是存在的,并且只读段,对于本例子而言,虚表只有一张 //(对于普通的带虚函数的单继承而言,一个类只有一张虚表) Derived derived(10, 20); printf("派生类对象derived的地址 : %p\n", &derived); printf("派生类对象derived的地址 : %p\n", (long *)&derived); printf("虚表的地址 : %p\n", (long *)*(long *)&derived); printf("第一个虚函数的地址 : %p\n", (long *) * (long *)*(long *)&derived); cout << endl << endl; typedef void (*pFunc)(); pFunc func = (pFunc)*((long *)*(long *)&derived); func(); printf("第一个虚函数的地址 : %p\n", func); cout << endl; func = (pFunc)*((long *)*(long *)&derived + 1); func(); printf("第二个虚函数的地址 : %p\n", func);
-
带虚函数的多基派生
见内存布局。
-
多基派生的二义性
class A { public: virtual void a(){ cout << "A::a()" << endl; } virtual void b(){ cout << "A::b()" << endl; } virtual void c(){ cout << "A::c()" << endl; } }; class B { public: virtual void a(){ cout << "B::a()" << endl; } virtual void b(){ cout << "B::b()" << endl; } void c(){ cout << "B::c()" << endl; } void d(){ cout << "B::d()" << endl; } }; class C : public A , public B { public: virtual void a(){ cout << "C::a()" << endl; } void c(){ cout << "C::c()" << endl; } //虚函数 void d(){ cout << "C::d()" << endl; } }; void test() { cout << "sizeof(A) = " << sizeof(A) << endl; cout << "sizeof(B) = " << sizeof(B) << endl; cout << "sizeof(C) = " << sizeof(C) << endl << endl; C c; cout << endl; A *pa = &c; printf("pa : %p\n", pa); pa->a(); pa->b(); pa->c(); cout << endl; B *pb = &c; printf("pb : %p\n", pb); pb->a(); pb->b(); pb->c(); pb->d(); cout << endl; C *pc = &c; printf("pc : %p\n", pc); pc->a(); /* pc->b();//二义性 */ pc->A::b(); pc->B::b(); pc->c(); pc->d();//隐藏 }
通过不同的基类指针指向派生类对象时,其地址是不一样的。
多基派生的原理图:
类C继承A、B,类A有函数名为c的虚函数,类B有函数名为c的普通函数,类C实现了c函数,此时用C的指针调用c函数时,是什么机制?虚函数还是覆盖?答案是虚函数机制,如何得出:
class D : public C { public: void c(){ cout << "D::c()" << endl; } }; void test(){ D d; C *pc = &d; pc->c(); }
-
问题:虚函数表里存放的都是虚函数的入口地址吗?
不一定,当多重继承时,每个基类都有虚函数时,派生类覆盖了基类同名虚函数后,只有第一个虚函数表中存放的是虚函数入口地址,其他的虚函数表中存放的是一条跳转指令。
-
虚拟继承
virtual关键字的含义:存在、间接和共享
-
虚函数:
虚函数是存在的
虚函数必须要通过一种间接的运行时(而不是编译时)机制才能够激活(调用)的函数。
共享性表现在基类会共享被派生类重定义后的虚函数
-
虚拟继承:
存在即表示虚继承体系和虚基类确实存在
间接性表现在当访问虚基类的成员时同样也必须通过某种间接机制来完成(通过虚基表来完成)。
共享性表现在虚基类会在虚继承体系中被共享,而不会出现多份拷贝。
-
-
虚拟继承时派生类对象的构造和析构
在 C++ 中,如果继承链上存在虚继承的基类,则最底层的子类要负责完成该虚基类部分成员的构造。即我们[最底层的子类]需要显式调用虚基类的构造函数来完成初始化,如果不显式调用,则编译器会调用虚基类的缺省构造函数,不管初始化列表中次序如何,对虚基类构造函数的调用总是先于普通基类的构造函数。如果虚基类中没有定义的缺省构造函数,则会编译错误。因为如果不这样做,虚基类部分会在存在的多个继承链上被多次初始化。很多时候,对于继承链上的中间类,我们也会在其构造函数中显式调用虚基类的构造函数,因为一旦有人要创建这些中间类的对象,我们要保证它们能够得到正确的初始化。
析构函数的调用顺序与构造函数调用相反。
-
效率分析
多重继承和虚拟继承对象模型较单一继承复杂的对象模型,造成了成员访问低效率,表现在两个方面:对象构造时vptr的多次设定,以及this指针的调整。
继承情况 vptr是否设定 数据成员访问 虚函数访问 效率 单一继承 无 指针/对象/引用访问效率相同 直接访问 效率最高 单一继承 一次 指针/对象/引用访问效率相同 通过vptr和vtable访问 多态的引入,带来了设定vptr和间接访问虚函数等效率的降低 多重继承 多次 指针/对象/引用访问效率相同 通过vptr和vtable访问;通过第二或后继Base类指针访问,需要调整this指针 除了单一继承效率降低的情形,调整this指针也带来了效率的降低 虚拟继承 多次 指针/对象/引用访问效率相同 通过vptr和vtable访问;访问虚基类需要调整this指针 除了单一继承效率的降低的情形,调整this指针也带来了效率的降低 -
内存布局
-
在vs中查看某个类的内存布局,可视化操作:
/d1reportSingleClassLayout类名/d1 reportAllClassLayout
以上二选一,然后点生成,即可在输出状态栏里查看。
-
单一继承,带虚函数,派生类对象的存储布局会多一个虚函数指针。【吸收、改造的基类的虚函数指针】,当派生类有新增的虚函数时,会添加在虚函数指针所指的虚表中。
2>class B size(12): 2> +--- 2> 0 | +--- (base class A) 2> 0 | | {vfptr} 2> 4 | | _ia 2> | +--- 2> 8 | _ib 2> +--- 2> 2>B::$vftable@: 2> | &B_meta 2> | 0 2> 0 | &B::f 2> 1 | &B::fb2 2>
-
单一虚继承,派生类和基类都没有虚函数。
2>class B size(12): 2> +--- 2> 0 | {vbptr} 2> 4 | _ib 2> +--- 2> +--- (virtual base A) 2> 8 | _ia 2> +--- 2> 2>B::$vbtable@: 2> 0 | 0 2> 1 | 8 (Bd(B+0)A) 2>vbi: class offset o.vbptr o.vbte fVtorDisp 2> A 8 0 4 0
-
单一虚继承,派生类没有自己独立的虚函数
派生类部分会多一个虚基指针,虚基指针指向一张虚基表;
虚基类子对象位于派生类存储空间的最末尾;
派生类对象中吸收、改造的基类的虚函数指针所指向的虚函数表中指向的是B::f()[覆盖],而不是A的虚函数。
class A { public: A() : _ia(10) {} virtual void f(){ cout << "A::f()" << endl;} private: int _ia; }; class B : virtual public A { public: B() : _ib(20) {} void fb(){ cout << "A::fb()" << endl; } void f(){ cout << "B::f()" << endl; } void fb2(){ cout << "B::fb2()" << endl; } private: int _ib; };
2>class B size(16): 2> +--- 2> 0 | {vbptr} 2> 4 | _ib 2> +--- 2> +--- (virtual base A) 2> 8 | {vfptr} 2>12 | _ia 2> +--- 2> 2>B::$vbtable@: 2> 0 | 0 2> 1 | 8 (Bd(B+0)A) 2> 2>B::$vftable@: 2> | -8 2> 0 | &B::f 2> 2>B::f this adjustor: 8 2>vbi: class offset o.vbptr o.vbte fVtorDisp 2> A 8 0 4 0
vbtable中第一行偏移0是在派生类中偏移0是虚基指针的地址;
第二行是在派生类中在虚基指针的基础上再偏移8是虚基类的地址。
-
单一虚继承,派生类拥有自己独立的虚函数
如果派生类没有自己独立的虚函数,此时派生类对象不会产生虚函数指针;
如果派生类拥有自己独立的虚函数,此时派生类对象就会产生自己本身的虚函数指针,并且该虚函数指针位于派生类对象存储空间的开始位置。
class B : virtual public A { public: B() : _ib(20) {} void fb(){ cout << "A::fb()" << endl; } void f() override{ cout << "B::f()" << endl; } virtual void fb2(){ cout << "B::fb2()" << endl; } private: int _ib; };
2>class B size(20): 2> +--- 2> 0 | {vfptr} 2> 4 | {vbptr} 2> 8 | _ib 2> +--- 2> +--- (virtual base A) 2>12 | {vfptr} 2>16 | _ia 2> +--- 2> 2>B::$vftable@B@: 2> | &B_meta 2> | 0 2> 0 | &B::fb2 2> 2>B::$vbtable@: 2> 0 | -4 2> 1 | 8 (Bd(B+4)A) 2> 2>B::$vftable@A@: 2> | -12 2> 0 | &B::f 2> 2>B::f this adjustor: 12 2>B::fb2 this adjustor: 0 2>vbi: class offset o.vbptr o.vbte fVtorDisp 2> A 12 4 4 0
-
多基继承,带虚函数
每个基类都有自己的虚函数表。
内存布局中,其基类的布局按照基类被继承时的顺序进行排列。
派生类如果有自己的虚函数,会被加入到第一个虚函数表之中。
派生类会覆盖基类的虚函数,只有第一个虚函数表中存放的是真实的被覆盖的虚函数的地址;其它的虚函数表中存放的并不是真实的对应的虚函数的地址,而只是一条跳转指令。
class Base1 { public: Base1() : _iBase1(10) {} virtual void f(){ cout << "Base1::f()" << endl; } virtual void g(){ cout << "Base1::g()" << endl; } virtual void h(){ cout << "Base1::h()" << endl; } private: int _iBase1; }; class Base2 { public: Base2() : _iBase2(100) {} virtual void f(){ cout << "Base2::f()" << endl; } virtual void g(){ cout << "Base2::g()" << endl; } virtual void h(){ cout << "Base2::h()" << endl; } private: int _iBase2; }; class Base3 { public: Base3() : _iBase3(1000) {} virtual void f(){ cout << "Base3::f()" << endl; } virtual void g(){ cout << "Base3::g()" << endl; } virtual void h(){ cout << "Base3::h()" << endl; } private: int _iBase3; }; class Derived : /*virtual*/ public Base1 , /*virtual*/ public Base2 , /*virtual*/ public Base3 { public: Derived() : _iDerived(10000) {} void f(){ cout << "Derived::f()" << endl; } virtual void g1(){ cout << "Derived::g1()" << endl; } private: int _iDerived; };
//内存布局 1>class Derived size(28): 1> +--- 1> 0 | +--- (base class Base1) 1> 0 | | {vfptr} 1> 4 | | _iBase1 1> | +--- 1> 8 | +--- (base class Base2) 1> 8 | | {vfptr} 1>12 | | _iBase2 1> | +--- 1>16 | +--- (base class Base3) 1>16 | | {vfptr} 1>20 | | _iBase3 1> | +--- 1>24 | _iDerived 1> +--- 1>Derived::$vftable@Base1@: 1> | &Derived_meta 1> | 0 1> 0 | &Derived::f(虚函数的覆盖) 1> 1 | &Base1::g 1> 2 | &Base1::h 1> 3 | &Derived::g1(新的虚函数,直接放在基类之后,加快查找速度) 1>Derived::$vftable@Base2@: 1> | -8 1> 0 | &thunk: this-=8; goto Derived::f //虚函数表还可以存放跳转指令 1> 1 | &Base2::g 1> 2 | &Base2::h 1>Derived::$vftable@Base3@: 1> | -16 1> 0 | &thunk: this-=16; goto Derived::f 1> 1 | &Base3::g 1> 2 | &Base3::h
-
【多基继承,带虚函数】 + 【虚拟继承】 ( 混合情况 )
派生类如果有自己的虚函数,会被加入到第一个虚函数表之中。
且其他的虚函数表中的被派生类覆盖的虚函数那一行,存的是跳转指令,跳转到第一个虚函数表中的对应虚函数。
考虑前两个是虚拟继承,最后一个是直接继承的内存布局。(照猫画虎)
class Derived : virtual public Base1 , /*virtual*/ public Base2 , /*virtual*/ public Base3 { public: Derived() : _iDerived(10000) {} void f(){ cout << "Derived::f()" << endl; } virtual void g1(){ cout << "Derived::g1()" << endl; } private: int _iDerived; };
//内存布局 1>class Derived size(32): 1> +--- 1> 0 | +--- (base class Base2) 1> 0 | | {vfptr} 1> 4 | | _iBase2 1> | +--- 1> 8 | +--- (base class Base3) 1> 8 | | {vfptr} 1>12 | | _iBase3 1> | +--- 1>16 | {vbptr} 1>20 | _iDerived 1> +--- 1> +--- (virtual base Base1) 1>24 | {vfptr} 1>28 | _iBase1 1> +--- 1>Derived::$vftable@Base2@: 1> | &Derived_meta 1> | 0 1> 0 | &Derived::f 1> 1 | &Base2::g 1> 2 | &Base2::h 1> 3 | &Derived::g1 1>Derived::$vftable@Base3@: 1> | -8 1> 0 | &thunk: this-=8; goto Derived::f 1> 1 | &Base3::g 1> 2 | &Base3::h 1>Derived::$vbtable@: 1> 0 | -16 1> 1 | 8 (Derivedd(Derived+16)Base1) 1>Derived::$vftable@Base1@: 1> | -24 1> 0 | &thunk: this-=24; goto Derived::f 1> 1 | &Base1::g 1> 2 | &Base1::h
-
多重虚拟继承
class Derived : virtual public Base1 , virtual public Base2 , virtual public Base3 { public: Derived() : _iDerived(10000) {} void f(){ cout << "Derived::f()" << endl; } virtual void g1(){ cout << "Derived::g1()" << endl; } private: int _iDerived; };
//内存布局 1>class Derived size(36): 1> +--- 1> 0 | {vfptr} //以空间换时间 1> 4 | {vbptr} 1> 8 | _iDerived 1> +--- 1> +--- (virtual base Base1) 1>12 | {vfptr} 1>16 | _iBase1 1> +--- 1> +--- (virtual base Base2) 1>20 | {vfptr} 1>24 | _iBase2 1> +--- 1> +--- (virtual base Base3) 1>28 | {vfptr} 1>32 | _iBase3 1> +--- 1>Derived::$vftable@Derived@: 1> | &Derived_meta 1> | 0 1> 0 | &Derived::g1 1>Derived::$vbtable@: 1> 0 | -4 1> 1 | 8 (Derivedd(Derived+4)Base1) 1> 2 | 16 (Derivedd(Derived+4)Base2) 1> 3 | 24 (Derivedd(Derived+4)Base3) 1>Derived::$vftable@Base1@: 1> | -12 1> 0 | &Derived::f 1> 1 | &Base1::g 1> 2 | &Base1::h 1>Derived::$vftable@Base2@: 1> | -20 1> 0 | &thunk: this-=8; goto Derived::f 1> 1 | &Base2::g 1> 2 | &Base2::h 1>Derived::$vftable@Base3@: 1> | -28 1> 0 | &thunk: this-=16; goto Derived::f 1> 1 | &Base3::g 1> 2 | &Base3::h
-
菱形继承,非虚继承
class B { public: B() : _ib(10), _cb('B') {} virtual void f(){cout << "B::f()" << endl;} virtual void Bf(){cout << "B::Bf()" << endl;} private: int _ib; char _cb; }; class B1 : /*virtual*/ public B { public: B1() : _ib1(100), _cb1('1') {} virtual void f(){ cout << "B1::f()" << endl; } #if 1 virtual void f1(){ cout << "B1::f1()" << endl; } virtual void Bf1(){ cout << "B1::Bf1()" << endl; } #endif private: int _ib1; char _cb1; }; class B2 : /*virtual*/ public B { public: B2() : _ib2(1000), _cb2('2') {} virtual void f(){ cout << "B2::f()" << endl; } #if 1 virtual void f2(){ cout << "B2::f2()" << endl; } virtual void Bf2(){ cout << "B2::Bf2()" << endl; } #endif private: int _ib2; char _cb2; }; class D : public B1, public B2 { public: D() : _id(10000), _cd('3') {} virtual void f(){ cout << "D::f()" << endl; } #if 1 virtual void f1(){ cout << "D::f1()" << endl; } virtual void f2(){ cout << "D::f2()" << endl; } virtual void Df(){ cout << "D::Df()" << endl; } #endif private: int _id; char _cd; };
//内存布局 class D size(48): 1> +--- 1> 0 | +--- (base class B1) 1> 0 | | +--- (base class B) 1> 0 | | | {vfptr} 1> 4 | | | _ib 1> 8 | | | _cb //1 1> | | | <alignment member> (size=3) //内存对齐 1> | | +--- 1>12 | | _ib1 1>16 | | _cb1 1> | | <alignment member> (size=3) 1> | +--- 1>20 | +--- (base class B2) 1>20 | | +--- (base class B) 1>20 | | | {vfptr} 1>24 | | | _ib 1>28 | | | _cb 1> | | | <alignment member> (size=3) 1> | | +--- 1>32 | | _ib2 1>36 | | _cb2 1> | | <alignment member> (size=3) 1> | +--- 1>40 | _id 1>44 | _cd 1> | <alignment member> (size=3) 1> +--- 1>D::$vftable@B1@: 1> | &D_meta 1> | 0 1> 0 | &D::f 1> 1 | &B::Bf 1> 2 | &D::f1 1> 3 | &B1::Bf1 1> 4 | &D::Df 1>D::$vftable@B2@: 1> | -20 1> 0 | &thunk: this-=20; goto D::f 1> 1 | &B::Bf 1> 2 | &D::f2 1> 3 | &B2::Bf2
未采用虚拟继承时,对象d中有2份B对象的拷贝,存在存储二义性。
-
菱形继承,采用虚拟继承。
虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移
//内存布局 1>class D size(52): 1> +--- 1> 0 | +--- (base class B1) 1> 0 | | {vfptr} 1> 4 | | {vbptr} 1> 8 | | _ib1 1>12 | | _cb1 1> | | <alignment member> (size=3) //3字节的内存对齐 1> | +--- 1>16 | +--- (base class B2) 1>16 | | {vfptr} 1>20 | | {vbptr} 1>24 | | _ib2 1>28 | | _cb2 1> | | <alignment member> (size=3) 1> | +--- 1>32 | _id 1>36 | _cd 1> | <alignment member> (size=3) 1> +--- 1> +--- (virtual base B) 1>40 | {vfptr} 1>44 | _ib 1>48 | _cb 1> | <alignment member> (size=3) 1> +--- 1>D::$vftable@B1@: 1> | &D_meta 1> | 0 1> 0 | &D::f1 1> 1 | &B1::Bf1 1> 2 | &D::Df 1>D::$vftable@B2@: 1> | -16 1> 0 | &D::f2 1> 1 | &B2::Bf2 1>D::$vbtable@B1@: 1> 0 | -4 1> 1 | 36 (Dd(B1+4)B) 1>D::$vbtable@B2@: 1> 0 | -4 1> 1 | 20 (Dd(B2+4)B) 1>D::$vftable@B@: 1> | -40 1> 0 | &D::f 1> 1 | &B::Bf
-
在不同的编译器中,内存布局的实现可能会存在差异。在GCC中,当出现vbptr和vfptr有同时存在的时候,编译器会优化掉,只用一个指针就解决:将vbptr与vfptr合并了。
-