5. 单继承和多继承关系的虚函数表
1.单继承中的虚函数表
class Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func2() { cout << "Base::Func2()" << endl; } void Func3() { cout << "Base::Func3()" << endl; } private: int _a; }; class Derive : public Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func3() { cout << "Base::Func3()" << endl; } virtual void Func4() { cout << "Base::Func4()" << endl; } private: int _b = 2; }; void Test4() { Base b; Derive d; }
按照以往的管理,这个时候我们应该打开监视窗口来查看虚表情况
可以看到,在d对象的虚表中本应该有四个虚函数,但是只能看到两个,这里可以认为是VS下的一个bug,那我们要怎么看到虚表中的内容呢?
这里推荐大家写一个接口用来打印虚表中的内容,完整代码如下:
class Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func2() { cout << "Base::Func2()" << endl; } void Func3() { cout << "Base::Func3()" << endl; } typedef void(*vfptr)();//这里将上面虚函数的指针类型重定义成vfptr,方便使用 void print_vftptr() { print((vfptr*)(*(int*)this)); } private: void print(vfptr table[]) { //依次拿到虚表中的所有函数指针 for (int i = 0; table[i] != nullptr; ++i)//在VS下,虚表以nullptr结束,所以可以用此方式作为循环条件 { printf("[%d]:%p->", i, table[i]);//打印序号和地址 table[i]();//调用函数,虚函数与预期相符 } cout << endl; } int _a = 1; }; class Derive : public Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func3() { cout << "Base::Func3()" << endl; } virtual void Func4() { cout << "Base::Func4()" << endl; } private: int _b = 2; }; void Test4() { Base b; Derive d; b.print_vftptr(); d.print_vftptr(); }
2. 多继承中的虚函数表
class Base1 { public: virtual void func1() { cout << "Base1::func1" << endl; } virtual void func2() { cout << "Base1::func2" << endl; } private: int b1; }; class Base2 { public: virtual void func1() { cout << "Base2::func1" << endl; } virtual void func2() { cout << "Base2::func2" << endl; } private: int b2; }; class Derive : public Base1, public Base2 { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } private: int d1; }; typedef void(*vfptr)(); void print(vfptr table[]) { for (int i = 0; table[i] != nullptr; ++i) { printf("[%d]:%p->", i, table[i]); table[i](); } cout << endl; } void Test6() { Derive d; vfptr* vTableb1 = (vfptr*)(*(int*)&d); print(vTableb1); vfptr* vTableb2 = (vfptr*)(*(int*)((char*)&d + sizeof(Base1))); print(vTableb2); }
3.菱形继承和菱形虚拟继承
实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。因此这里就不堆其进行详细的探讨,如果有想要了解的,这里推荐两篇博客
C++ 虚函数表解析
C++ 对象的内存布局
6. 一些考察题
❓inline函数可以是虚函数吗?
✅虚函数可以使用inline修饰,但是编译器会忽略,因为虚函数将会放到虚表中
❓静态成员可以是虚函数吗?
✅不可以,静态函数没有this指针,所以没办法存放虚表
❓构造函数可以是虚函数吗?
✅不可以,因为对象中的虚表指针实在构造函数走初始化列表的时候才初始化的,所以构造函数没办法放进虚表
❓对象访问普通函数快还是虚函数更快?
✅如果不构成多态的话,是一样快,如果构成了多态,那就是普通函数更快,因为运行时虚函数需要到虚表中找到地址再调用
❓虚函数表是在什么阶段生成的,存在哪的?
✅虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的
❓析构函数被处理成destructor的必要性/是否推荐析构函数加上virtual关键字?
✅我们来看下面一段代码:
class person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } ~person() { cout << "~person()" << endl; } }; class student : public person { public: virtual void BuyTicket() { cout << "买票-半价" << endl; } ~student() { cout << "~student()" << endl; } }; void Test() { person* p1 = new person; person* p2 = new student; delete p1; delete p2; }
可以看到,在析构的时候,只调用了person的析构函数,这里如果student的析构函数需要释放资源,按照这种写法就会出现内存泄漏的情况,为了构成多态,让其能够根据指向的对象的实际类型决定调用基类还是派生类的析构函数,所以这里需要将所有的析构函数处理成相同函数名(析构函数没有返回值和参数)。