先来看这个问题:
- class Base
- {
- public:
- virtual void Func1()
- {
- cout << "Func1()" << endl;
- }
- private:
- int _b = 1;
- };
sizeof(Base)是多少?
答案是:8
因为Base中除了成员变量_b,还有一个虚函数表_vfptr(当类中有虚函数就会生成),虚函数表的本质是函数指针数组,用来存储虚函数的指针
- 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 = 1;
- };
-
- class Derive :public Base
- {
- public:
- void Func1()
- {
- cout << "Derive::Func1()" << endl;
- }
- private:
- int _b = 2;
- };
-
- void test1()
- {
- Base bs;
- Derive de;
- }
派生类虚函数表是这样生成的:先将基类的虚函数表内容拷贝过来,在把派生类中重写的虚函数的地址覆盖到被重写的虚函数地址上,最后将派生类自己增加的虚函数依次增加到虚表的末尾。
上面这个例子中,派生类和基类的虚函数表都存储有两个函数地址,即Func1和Func2,但是派生类只对Func1重写,没有对Func2重写,编译器会将派生类中的Func1的地址进行覆盖,所以Func1的地址不同,Func2的地址相同(0x009f1370)
注意:虚函数存在哪,虚函数表存在哪?
虚函数和普通的成员函数一样都存在公共代码段,虚函数表只是存了虚函数的指针;虚函数表存在哪,这个要看编译器,感兴趣可以自己验证。
我的编译器是将虚函数表存储在类对象的开始位置,使用32位系统虚表大小4byte
- void test()
- {
- int i = 0;
- printf("栈区:%p\n", &i);
-
- char* ch = new char;
- printf("堆区:%p\n", ch);
-
- static int ci = 1;
- printf("数据段:%p\n", &ci);
-
- const char* s = "hello";
- printf("代码段:%p\n", s);
-
- Derive de;
- printf("虚表:%p\n", *((int*)&de));
-
- }

虚表地址和代码段地址相差48byte,基本可以认为虚表存在代码段。大家可以参考这个方法,根据自己的编译器和环境来测试
说了这么多,虚函数表在多态中有什么用?
当我们有基类的指针或引用调用派生类的虚函数时,第一步是将派生类赋值给基类,这时派生类只会将属于基类的那部分交给基类的指针或引用,而属于基类的那部分中包含了派生类的虚函数表。这样造成的结果就是,调用虚函数时调用了派生类中重写过的虚函数,实现了多态。
为什么直接将派生类赋值给基类就不能实现多态呢?因为这种写法会在编译器编译时,直接从符号表确定函数地址,程序运行时直接调用。而多态调用在编译期间不会确定,只在程序运行时到对象中寻找函数。其实编译器并不知道这个对象是基类还是派生类,只是无脑从虚函数表中找。
下面这个测试程序,通过观察汇编代码,就可以直观看出普通调用和多态调用的区别:
- void test()
- {
- Base bs;
- Derive de;
- bs = de;
- Base& b = de;
-
- bs.Func1();
-
- b.Func1();
- }

前面都是以单继承为例讲虚函数表和多态原理,下面我们看多继承的虚函数表
- class Base1
- {
- public:
- virtual void Func1()
- {
- cout << "Base1::Func1()" << endl;
- }
- virtual void Func2()
- {
- cout << "Base1::Func2()" << endl;
- }
- private:
- int _a = 1;
- };
-
- class Base2
- {
- public:
- virtual void Func3()
- {
- cout << "Base2::Func3()" << endl;
- }
-
- virtual void Func4()
- {
- cout << "Base2::Func4()" << endl;
- }
- };
- class Derive :public Base1 ,public Base2
- {
- public:
- void Func1()
- {
- cout << "Derive::Func1()" << endl;
- }
- void Func3()
- {
- cout << "Derive::Func3()" << endl;
- }
- virtual void Func5()
- {
- cout << "Derive::Func5()" << endl;
- }
- private:
- int _b = 2;
- };
-
- void test()
- {
- Base1 b1;
- Base2 b2;
- Derive d;
- }
调试:


多继承后的派生类有两个虚函数表,Func2和Func4没有重写,与基类函数地址相同。Func1和Func3重写后进行覆盖。派生类新增加的虚函数地址存到了最先继承的基类Base1的虚表中(第一张图的监视窗口未显示,有bug,在内存窗口可以看到)
看下面程序的运行结果是什么:
- class A
- {
- public:
- virtual void func(int val = 1){ std::cout<<"A->"<< val <
- virtual void test(){ func();}
- };
- class B : public A
- {
- public:
- void func(int val=0){ std::cout<<"B->"<< val <
- };
- int main(int argc ,char* argv[])
- {
- B*p = new B;
- p->test();
- return 0;
- }
test函数没有重写,直接调用A::test();func函数被重写,虚表中是B中func的函数地址,又因为重写只是对函数体的重写,val缺省值是0,结果是B->0.
-
相关阅读:
5-1.(OOP)初步分析MCV架构模式
KnowStreaming贡献流程
视频学习|Springboot在线学习系统
centos如何配置永久ip
Vue3 toRaw 和 markRaw
MySQL——表的插入
node.js+校内废品回收管理 毕业设计-附源码140933
什么是视图
Android Gradle 学习笔记(一)概述
空气温湿度、光照度、二氧化碳传感器
-
原文地址:https://blog.csdn.net/weixin_74269833/article/details/133671062