-
图解虚函数的内存模型和继承方式,虚函数表指针、虚函数表、多继承、多重继承、菱形继承、虚继承
2020-10-29 22:49:02虚函数、虚函数表指针、虚函数表、多级继承、多重继承、菱形继承阅读本文之前想必你对虚函数已经有过一些了解~
最近面试被问过太多次虚函数了,遂写下本文以供相互学习和交流~
关键字:虚函数、虚函数表指针、虚函数表、多继承、多重继承、菱形继承、虚继承
一、单继承:
这种情况比较简单,把握住三点:
- 每个有虚函数的类都会为该类创建一个虚函数表,且在编译时已经决定了;
- 虚函数表属于类而不属于对象;
- 只要父类中有虚函数,子类就会有虚函数表,且父类虚函数表和子类虚函数表不同。
- 代码如下
#include <iostream> using namespace std; class A{ public: void test1(){cout << "A::test1" << endl;}; virtual void test2(){cout << "A::test2" << endl;}; virtual void test3(){cout << "A::test3" << endl;}; virtual ~A(){cout << "A::~A" << endl;}; }; class B:public A{ public: void test1(){cout << "B::test1" << endl;}; virtual void test2(){cout << "B::test2" << endl;}; virtual ~B(){cout << "B::~B" << endl;}; }; int main(){ A* A_ptr_2 = new B; A_ptr_2->test1(); //调用A::test1 A_ptr_2->test2(); //调用B::test2 A_ptr_2->test3(); //调用A::test3 delete A_ptr_2; //析构顺序为B、A }
- 运行结果
A::test1 B::test2 A::test3 B::~B A::~A
- 其对象模型
当使用父类(父类中有虚函数)指针创建子类的对象时,对象的前四个字节是虚函数表指针,虚函数表指针(对象拥有)指向虚函数表(虚函数表是类拥有而不是对象拥有),再根据虚函数表找到相应的虚函数。
二、多重继承:
这种情况为B类继承A类、C类继承B类,子类的虚函数表是在它的父类的虚函数表基础上改动的,即C类是在B类的虚函数表基础上覆盖,B类的虚函数表在A类的基础上覆盖。
- 代码如下
#include <iostream> using namespace std; class A{ public: void test1(){cout << "A::test1" << endl;}; virtual void test2(){cout << "A::test2" << endl;}; virtual void test3(){cout << "A::test3" << endl;}; virtual ~A(){cout << "A::~A" << endl;}; }; class B:public A{ public: void test1(){cout << "B::test1" << endl;}; virtual void test2(){cout << "B::test2" << endl;}; virtual ~B(){cout << "B::~B" << endl;}; }; class C:public B{ public: void test1(){cout << "C::test1" << endl;}; virtual void test3(){cout << "C::test3" << endl;}; virtual ~C(){cout << "C::~C" << endl;}; }; int main(){ A* A_ptr_1 = new A; A_ptr_1->test1(); //调用A::test1 A_ptr_1->test2(); //调用A::test2 A_ptr_1->test3(); //调用A::test3 delete A_ptr_1; //析构A A* A_ptr_2 = new B; A_ptr_2->test1(); //调用A::test1 A_ptr_2->test2(); //调用B::test2 A_ptr_2->test3(); //调用A::test3 delete A_ptr_2; //析构顺序为B、A A* A_ptr_3 = new C; A_ptr_3->test1(); //调用A::test1 A_ptr_3->test2(); //调用B::test2 A_ptr_3->test3(); //调用C::test3 delete A_ptr_3; //析构顺序为C、B、A return 0; }
- 运行结果
A::test1 A::test2 A::test3 A::~A A::test1 B::test2 A::test3 B::~B A::~A A::test1 B::test2 C::test3 C::~C B::~B A::~A
- 其对象模型
多重继承的原则就是子类只需要关心它上一级父类的虚函数表模型,然后一层层推导:
- A的虚函数表中有A::test2和A::test3虚函数的地址;
- B的虚函数表在A的基础上覆盖,即有A::test3和B::test2(A::test2被B::test2覆盖了);
- 同理,C的虚函数表在B的基础上覆盖,即有C::test3(A::test3被C::test3覆盖了)和B::test2。
三、多继承:
一个子类继承多个父类时:
- 父类析构的顺序决定于它被声明的顺序,例如class C:class B,class A,则析构顺序为C、A、B;若是class:class A,class B,则析构顺序为C、B、A。
- 子类对象的模型中虚函数表指针的数量为继承父类(父类中需要有虚函数)的数量,即C类的对象有两个虚函数表指针。
- 父类没有而子类有的虚函数,添加到第一个虚函数表中。
- 代码如下
#include <iostream> using namespace std; class A{ public: void test1(){cout << "A::test1" << endl;}; virtual void test2(){cout << "A::test2" << endl;}; virtual void test3(){cout << "A::test3" << endl;}; virtual ~A(){cout << "A::~A" << endl;}; }; class B{ public: void test1(){cout << "B::test1" << endl;}; virtual void test2(){cout << "B::test2" << endl;}; virtual ~B(){cout << "B::~B" << endl;}; }; class C:public A,public B{ public: void test1(){cout << "C::test1" << endl;}; virtual void test2(){cout << "C::test2" << endl;}; virtual void test4(){cout << "C::test4" << endl;}; virtual ~C(){cout << "C::~C" << endl;}; }; int main(){ A* A_ptr = new C; A_ptr->test1(); //调用A::test1 A_ptr->test2(); //调用C::test2 A_ptr->test3(); //调用A::test3 delete A_ptr; //析构顺序为C、B、A 注意这里~A、~B的执行仅与在C类中声明的顺序有关 B* B_ptr = new C; B_ptr->test1(); //调用B::test1 B_ptr->test2(); //调用C::test2 delete B_ptr; //析构顺序为C、B、A 注意这里~A、~B的执行仅与在C类中声明的顺序有关 C* C_ptr = new C; C_ptr->test1(); //调用C::test1 C_ptr->test2(); //调用C::test2 C_ptr->test3(); //调用A::test3 C_ptr->test4(); //调用C::test4 delete C_ptr; //析构顺序为C、B、A 注意这里~A、~B的执行仅与在C类中声明的顺序有关 return 0; }
- 运行结果
A::test1 C::test2 A::test3 C::~C B::~B A::~A B::test1 C::test2 C::~C B::~B A::~A C::test1 C::test2 A::test3 C::test4 C::~C B::~B A::~A
- 其对象模型
多继承的虚函数表建立方式和单继承类似,即对每个父类进行比较并分别创建虚函数表。
特殊情况:父类没有而子类有虚函数时,将其放到第一个虚函数表中。例如,A类和B类中都没有test4(),将其放入第一个虚函数表中,即虚函数指针3指向C::test4。
四、菱形继承
多继承时很容易产生命名冲突,其中最典型的就是菱形继承。类A的成员变量和成员函数到达D时变成了两份,分别来自A->B->D和A->C->D,产生了命名冲突。其解决办法为虚继承,虚继承使得派生类中只保留一份间接基类的成员。无论虚基类在继承体系中出现了多少次,派生类都只包含一份虚基类成员。
五、总结
使用父类指针指向子类对象时:
-
单继承
a、父类有虚函数而子类没有同名虚函数:调用父类的虚函数;
b、子类有的虚函数而父类没有同名虚函数:父类指针不可指向子类的虚函数;
c、子类有父类同名的虚函数:调用子类虚函数。
-
多重继承
其子类虚函数表模型是根据它的直接继承父类的虚函数表模型而创建的。
-
多继承
有多少个父类,子类的对象就有多少个虚函数表和虚函数表指针,若是父类中没有的虚函数而子类有,则放入第一个虚函数表中。
-
菱形继承
属于多继承的一种,一般采用虚继承的方法解决,即为把基类声明为虚基类。
-
单继承、多继承、菱形继承的虚函数表
2019-12-29 20:19:49最近被问到一个关于多继承虚函数表的问题,当时回答是可能存在多个虚函数表,应该是顺序排列的,但具体怎么排列还是有些疑惑的,回答的时候到有点儿心虚。之后查了资料,做了简单的实验,可以确定的是对于继承了多个...前言
最近被问到一个关于多继承虚函数表的问题,当时回答是可能存在多个虚函数表,应该是顺序排列的,但具体怎么排列还是有些疑惑的,回答的时候到有点儿心虚。之后查了资料,做了简单的实验,可以确定的是对于继承了多个含有虚函数基类的子类来说,指向虚函数表的指针应该不止一个。
问题
虚函数表的问题是从C++多态的概念引出的,要想实现多态有3个条件:
- 存在继承:没有继承就没有多态(运行时),在多态中必须存在有继承关系的父类和子类。
- 重写函数:父类中需要定义带有
virtual
关键字的函数,而在子类中重写一个名字和参数与父类中定义完全相同的函数。 - 向上转型:将父类的指针和引用指向子类的对象。
满足以上三个条件,当使用父类的指针调用带有
virtual
关键字的函数时,就会产生多态行为。实现这种多态表现的核心内容就是虚函数表,对于带有
virtual
关键字的函数地址会被放入一个表格,而在类中会有一个指向虚函数表的指针指向这个表格,表明这个表格属于类的一部分。对于父类来说,这个表格中都是自己类的虚函数,而对于子类来说,首先这个虚函数表包含父类中所有的虚函数,当子类重写某个虚函数时就会用子类重写后的函数地址替换原来父类中定义的函数地址,同时在子类的虚函数表中还会包含子类独有的虚函数。
由此可见虚函数表的不同和复杂性还是体现在子类上,所以之后会分别测试单继承、多继承、菱形继承三种情况下虚函数表的不同,主要看一下虚函数表的个数和内存布局情况。
测试环境
首先来说明一下测试环境,测试工具是
VS2013
,对于int *p; sizeof(p)
的结果是4,说明编译环境是32位的,这个对后面查看内存结构非常关键。开始测试
使用VS2013查看类的内存布局非常方便,因为类的大小在编译期间就已经确定了,不用运行就可以通过添加编译选项知道类的大小和布局,而指向虚函数表的指针也会占用类的大小,如果说编译的时候确定了类的大小,那从侧面也说明了在编译期间虚函数表实际上也确定了。
使用VS2013查看类的布局时,可以在项目的属性页:“配置属性”–>“C/C++”–>“命令行”中输入以下任意一个命令,
/d1reportAllClassLayout
:这个选项可以在VS的输出窗口显示所有相关联的类结构,因为一些外部类也会显示,最终的内容会非常多,需要自己辨别有用的信息。/d1reportSingleClassLayoutXXX
:这个选项只会在输出窗口显示指定的类结构,只需要将XXX
替换成想显示的类的名字即可,缺点就是无法同时显示多个想查看的类。
无虚函数简单类结构
在查看虚函数表的结构之前,先使用之前的编译参数来查看一下简单的类结构,排除虚函数的干扰,能更清楚的了解类成员在类中的布局情况,有一点需要提一下,成员变量会占用类的大小,但是成员函数不会,如果有虚函数,所有的虚函数会被放入一个表格,而在类中放置一个指向虚函数表的指针,来看一下简单代码:
class CBase { public: void func() {} public: int m_var1; }; class CDerived : public CBase { public: void func() {} public: int m_var2; };
编译输出的类的内存布局为:
1> class CBase size(4): 1> +--- 1> 0 | m_var1 1> +--- 1> 1> class CDerived size(8): 1> +--- 1> | +--- (base class CBase) 1> 0 | | m_var1 1> | +--- 1> 4 | m_var2 1> +---
从上面的输出内容来看,很清楚的可以看到基类
CBase
的大小size(4)
占用4个字节,只有一个成员变量m_var1
,在类中偏移量为0的位置,而派生类CDerived
占用8个字节大小,第一个成员继承自基类CBase
的m_var1
,在类中偏移量为0的位置,还有一个子类独有的成员变量m_var2
,在类中偏移量为4的位置。掌握着这种简单类的查看类结构的方法,接下来开始看一下包含虚函数的类的内存布局。
包含虚函数的类结构
查看包含虚函数的类结构相对来说麻烦一点,先来说两个符号,免得一会看见结构发懵,
vfptr
表示类中指向虚函数表的指针,通常放在类的起始位置,比成员变量的位置都要靠前,vftable
表示类中引用的虚函数表,在具体分析是还有有一些修饰符,用来表明是谁的虚函数表。单继承
这种情况的下的子类的虚函数表很简单,在该子类的内存布局上,最开始的位置保存了一个指向虚函数表的指针,虚函数表中包含了从父类继承的虚函数,当子类中重写父类虚函数时会将虚函数表中对应的函数地址替换,最后添加上自己独有的虚函数地址,下面上代码分析一下:
class CBase { public: void func0() {} virtual void func1() {} virtual void func2() {} public: int m_var1; }; class CDerived : public CBase { public: virtual void func2() {} virtual void func3() {} void func4() {} public: int m_var2; };
上面这两个类的内存布局情况如下:
1> class CBase size(8): 1> +--- 1> 0 | {vfptr} 1> 4 | m_var1 1> +--- 1> 1> CBase::$vftable@: 1> | &CBase_meta 1> | 0 1> 0 | &CBase::func1 1> 1 | &CBase::func2 1> 1> CBase::func1 this adjustor: 0 1> CBase::func2 this adjustor: 0 1> 1> 1> class CDerived size(12): 1> +--- 1> | +--- (base class CBase) 1> 0 | | {vfptr} 1> 4 | | m_var1 1> | +--- 1> 8 | m_var2 1> +--- 1> 1> CDerived::$vftable@: 1> | &CDerived_meta 1> | 0 1> 0 | &CBase::func1 1> 1 | &CDerived::func2 1> 2 | &CDerived::func3 1> 1> CDerived::func2 this adjustor: 0 1> CDerived::func3 this adjustor: 0
看起来是不是比没有虚函数时复杂多了,不过不要着急,从上到下慢慢分析就好了,这次的基类
CBase
大小是8个字节,首先是{vfptr}
这个指向虚函数表的指针,在类中的偏移量是0,接下来是成员变量m_var1
,在类中偏移量是4。然后是
CBase::$vftable@
表示基类CBase
的虚函数表,其中第一行&CBase_meta
看起来怪怪的,这里我们不展开(因为我也没弄太懂),应该是和虚函数表相关的元数据,第二行是一个0,看起来是一个偏移量,这里没有偏移,当出现偏移时我们再试着分析(相信我,马上就会出现),第三行内容&CBase::func1
是自己类的虚函数,前面有一个0,应该是指该虚函数在虚函数表中索引,第四行也是相同的情况。接下来出现了两行非常相似的内容,看一下
CBase::func1 this adjustor: 0
,这句代码中的关键是adjustor
,其实有是一个偏移量,据说涉及到thunk技术,据说“thunk其实就是一条汇编指令,操作码是0xe9,就是jmp,后面紧跟操作数”,这里我们就不展开了,如果后面弄明白了可以单独写一篇总结,到此为止基类的内存结构就分析完了。继续看派生类
CDerived
,它的大小是12个字节,内部结构首先是{vfptr}
一个指向虚函数表的指针,偏移量为0,m_var1
是从父类继承的成员变量,偏移量为4,而m_var2
是自己类独有的成员变量,偏移量是8。然后看派生类对应的虚函数表
CDerived::$vftable@
,跳过前两行直接看一下后面几个函数,发现只有func1
是基类的,而函数func2
和func3
都是派生类的,出现这种情况的原因是子类重写了函数func2
和func3
,所以用重写后的函数地址替换了从基类继承的虚函数,造成了目前看到的状况。最后又出现了两行
adjustor
,很奇怪为什么func1
函数没有adjustor
,貌似这个adjustor
只对当前类有效,先留个疑问,接下来看一下多继承。多继承
当多个父类中都包含虚函数的时候,和子类关联的虚函数表就不止一个了,这个情况是可以通过使用sizeof(子类)来简单验证的:
这一部分是在没有VS的情况下预先写下的,本来考虑使用VS展开布局后,这一段就没有什么必要了,但是后来想想还是留着吧,因为这一段使用的g++编译器,64位环境,每个指针占用8个字节,通过不同的环境调试,更加可以证明,多继承下的多个虚函数表的存在性:
class W { public: long n; public: void func(){} };
对于这样的一个简单类,sizeof(W) = 8,类的大小等于成员变量的大小。
class W1 { public: long n1; public: virtual void func1(){} }; class W2 { public: long n2; public: virtual void func2(){} };
对于上面这两个简单的包含虚函数的类,sizeof(W1) = 16,sizeof(W2) = 16,因为每个类都除了一个
long
类型的成员变量以外,还包含了指向虚函数的一个指针,所以类的大小是16个字节。class WW : public W1, public W2 { public: long nn; public: virtual void func(){} };
而继承了
W1
和W2
这两个父类的子类WW
在继承了两个成员变量n1
和n2
之外,还有自己的成员变量nn
,三个变量占用字节24个,而计算类WW
的的大小 sizeof(W1) = 40,也就是说除了成员变量24个字节,还剩余了16个字节的空间没有着落,我们知道它至少包含一个指向虚函数表的指针,占用8个字节的大小,还剩8个字节没有找到用处,从此处分析应该还有一个指向虚函数表的指针,具体的情况可以看一下内存分布。接下来和单继承的分析方法一样,写代码编译查看布局:
class CBase0 { public: void func0() {} virtual void func1() {} virtual void func2() {} virtual void func3() {} public: int m_var0; }; class CBase1 { public: void func0() {} virtual void func2() {} virtual void func3() {} virtual void func4() {} public: int m_var1; }; class CDerived : public CBase0, public CBase1 { public: virtual void func1() {} virtual void func2() {} virtual void func4() {} virtual void func5() {} void func6() {} public: int m_var2; };
上面3个类描述了一个简单的多继承的情况,之所以写这么多函数就是构建一种,既有虚函数覆盖,又有单独不被覆盖的情况,下面展示了这段代码的内存布局。
1> class CBase0 size(8): 1> +--- 1> 0 | {vfptr} 1> 4 | m_var0 1> +--- 1> 1> CBase0::$vftable@: 1> | &CBase0_meta 1> | 0 1> 0 | &CBase0::func1 1> 1 | &CBase0::func2 1> 2 | &CBase0::func3 1> 1> CBase0::func1 this adjustor: 0 1> CBase0::func2 this adjustor: 0 1> CBase0::func3 this adjustor: 0 1> 1> 1> class CBase1 size(8): 1> +--- 1> 0 | {vfptr} 1> 4 | m_var1 1> +--- 1> 1> CBase1::$vftable@: 1> | &CBase1_meta 1> | 0 1> 0 | &CBase1::func2 1> 1 | &CBase1::func3 1> 2 | &CBase1::func4 1> 1> CBase1::func2 this adjustor: 0 1> CBase1::func3 this adjustor: 0 1> CBase1::func4 this adjustor: 0 1> 1> 1> class CDerived size(20): 1> +--- 1> | +--- (base class CBase0) 1> 0 | | {vfptr} 1> 4 | | m_var0 1> | +--- 1> | +--- (base class CBase1) 1> 8 | | {vfptr} 1> 12 | | m_var1 1> | +--- 1> 16 | m_var2 1> +--- 1> 1> CDerived::$vftable@CBase0@: 1> | &CDerived_meta 1> | 0 1> 0 | &CDerived::func1 1> 1 | &CDerived::func2 1> 2 | &CBase0::func3 1> 3 | &CDerived::func5 1> 1> CDerived::$vftable@CBase1@: 1> | -8 1> 0 | &thunk: this-=8; goto CDerived::func2 1> 1 | &CBase1::func3 1> 2 | &CDerived::func4 1> 1> CDerived::func1 this adjustor: 0 1> CDerived::func2 this adjustor: 0 1> CDerived::func4 this adjustor: 8 1> CDerived::func5 this adjustor: 0
内容很多,前面两个基类
CBase0
和CBase1
的布局很简单,参照之前的分析很容易看懂,直接从派生类看起吧。我们发现派生类
CDerived
中确实有两个指向虚函数表的指针,接下来看一下这两个虚函数表,这个虚函数表和前面遇到的格式一样,除了第一行的元数据,第二行的诡异偏移量0,剩下的虚函数指针有的是从基类继承来的,有的是被当前派生类覆盖的,还有派生类自己独有的。而第二个虚函数表就有点意思了,首先是少了
&CDerived_meta
这一行,然后偏移量终于不是0了,而是-8,从派生类CDerived
的内存布局上来看,以下开始大胆假设,至于小心求证的部分放到以后来做(看自己的进步状态了)。第二个指向虚函数表的指针是不是距离类的起始偏移量是8,我猜这个-8的意思就是指的这个偏移量,这个值有可能被后面使用,第二行出现了
&thunk: this-=8; goto CDerived::func2
,其中包含thunk
字样,表示这个func2
不归我管,你去-8偏移量的那个虚函数表里找一找。还有一点你有没有发现
func5
这个函数只在第一个虚函数表中出现,而没有出现在第二个虚函数表中,这也是一个规则,自己独有的虚函数放到第一个虚函数表中,这可能也是为什么只有第一个虚函数表包含元数据行。最后一点,我们发现对于函数
func4
来说adjustor
终于不是0了,而值变成了8,仿佛在说这个虚函数只在偏移量的为8的位置。菱形继承
对于这一部分,并没有太多新的内容,只是简单的菱形继承中,最初的基类在最终的子类中会包含两份,而虚函数的样子并没有太大的不同,接下来简单看一下代码和对应的内存布局即可,因为菱形继承并不被提倡,所以也不用花太多时间来分析这个问题。
class CSuper { public: virtual void func0() {} virtual void func1() {} public: int m_var; }; class CBase0 : public CSuper { public: virtual void func1() {} virtual void func2() {} public: int m_var0; }; class CBase1 : public CSuper { public: virtual void func1() {} virtual void func3() {} public: int m_var1; }; class CDerived : public CBase0, public CBase1 { public: virtual void func1() {} virtual void func3() {} virtual void func4() {} public: int m_var2; };
1> class CSuper size(8): 1> +--- 1> 0 | {vfptr} 1> 4 | m_var 1> +--- 1> 1> CSuper::$vftable@: 1> | &CSuper_meta 1> | 0 1> 0 | &CSuper::func0 1> 1 | &CSuper::func1 1> 1> CSuper::func0 this adjustor: 0 1> CSuper::func1 this adjustor: 0 1> 1> 1> class CBase0 size(12): 1> +--- 1> | +--- (base class CSuper) 1> 0 | | {vfptr} 1> 4 | | m_var 1> | +--- 1> 8 | m_var0 1> +--- 1> 1> CBase0::$vftable@: 1> | &CBase0_meta 1> | 0 1> 0 | &CSuper::func0 1> 1 | &CBase0::func1 1> 2 | &CBase0::func2 1> 1> CBase0::func1 this adjustor: 0 1> CBase0::func2 this adjustor: 0 1> 1> 1> class CBase1 size(12): 1> +--- 1> | +--- (base class CSuper) 1> 0 | | {vfptr} 1> 4 | | m_var 1> | +--- 1> 8 | m_var1 1> +--- 1> 1> CBase1::$vftable@: 1> | &CBase1_meta 1> | 0 1> 0 | &CSuper::func0 1> 1 | &CBase1::func1 1> 2 | &CBase1::func3 1> 1> CBase1::func1 this adjustor: 0 1> CBase1::func3 this adjustor: 0 1> 1> 1> class CDerived size(28): 1> +--- 1> | +--- (base class CBase0) 1> | | +--- (base class CSuper) 1> 0 | | | {vfptr} 1> 4 | | | m_var 1> | | +--- 1> 8 | | m_var0 1> | +--- 1> | +--- (base class CBase1) 1> | | +--- (base class CSuper) 1> 12 | | | {vfptr} 1> 16 | | | m_var 1> | | +--- 1> 20 | | m_var1 1> | +--- 1> 24 | m_var2 1> +--- 1> 1> CDerived::$vftable@CBase0@: 1> | &CDerived_meta 1> | 0 1> 0 | &CSuper::func0 1> 1 | &CDerived::func1 1> 2 | &CBase0::func2 1> 3 | &CDerived::func4 1> 1> CDerived::$vftable@CBase1@: 1> | -12 1> 0 | &CSuper::func0 1> 1 | &thunk: this-=12; goto CDerived::func1 1> 2 | &CDerived::func3 1> 1> CDerived::func1 this adjustor: 0 1> CDerived::func3 this adjustor: 12 1> CDerived::func4 this adjustor: 0
虚继承
解决菱形继承的一个常用的办法就是改为虚继承,实际上虚继承中就是将从最基类中继承的公共部分提取出来放在最子类的末尾,然后在提取之前的位置用一个叫做
vbptr
的指针指向这里。之前看到过一种说法:
虚继承内部实现也相当复杂,似乎破坏了OO的纯洁性
至于复杂不复杂,看看后面的内存布局就很清楚了,那是相当复杂,其中出现了各种偏移,简单了解下就行了,如果不是维护老代码,谁现在还写这样的结构。
class CSuper { public: virtual void func0() {} virtual void func1() {} public: int m_var; }; class CBase0 : virtual public CSuper { public: virtual void func1() {} virtual void func2() {} public: int m_var0; }; class CBase1 : virtual public CSuper { public: virtual void func1() {} virtual void func3() {} public: int m_var1; }; class CDerived : public CBase0, public CBase1 { public: virtual void func1() {} virtual void func3() {} virtual void func4() {} public: int m_var2; };
1> class CSuper size(8): 1> +--- 1> 0 | {vfptr} 1> 4 | m_var 1> +--- 1> 1> CSuper::$vftable@: 1> | &CSuper_meta 1> | 0 1> 0 | &CSuper::func0 1> 1 | &CSuper::func1 1> 1> CSuper::func0 this adjustor: 0 1> CSuper::func1 this adjustor: 0 1> 1> 1> class CBase0 size(20): 1> +--- 1> 0 | {vfptr} 1> 4 | {vbptr} 1> 8 | m_var0 1> +--- 1> +--- (virtual base CSuper) 1> 12 | {vfptr} 1> 16 | m_var 1> +--- 1> 1> CBase0::$vftable@CBase0@: 1> | &CBase0_meta 1> | 0 1> 0 | &CBase0::func2 1> 1> CBase0::$vbtable@: 1> 0 | -4 1> 1 | 8 (CBase0d(CBase0+4)CSuper) 1> 1> CBase0::$vftable@CSuper@: 1> | -12 1> 0 | &CSuper::func0 1> 1 | &CBase0::func1 1> 1> CBase0::func1 this adjustor: 12 1> CBase0::func2 this adjustor: 0 1> 1> vbi: class offset o.vbptr o.vbte fVtorDisp 1> CSuper 12 4 4 0 1> 1> 1> class CBase1 size(20): 1> +--- 1> 0 | {vfptr} 1> 4 | {vbptr} 1> 8 | m_var1 1> +--- 1> +--- (virtual base CSuper) 1> 12 | {vfptr} 1> 16 | m_var 1> +--- 1> 1> CBase1::$vftable@CBase1@: 1> | &CBase1_meta 1> | 0 1> 0 | &CBase1::func3 1> 1> CBase1::$vbtable@: 1> 0 | -4 1> 1 | 8 (CBase1d(CBase1+4)CSuper) 1> 1> CBase1::$vftable@CSuper@: 1> | -12 1> 0 | &CSuper::func0 1> 1 | &CBase1::func1 1> 1> CBase1::func1 this adjustor: 12 1> CBase1::func3 this adjustor: 0 1> 1> vbi: class offset o.vbptr o.vbte fVtorDisp 1> CSuper 12 4 4 0 1> 1> 1> class CDerived size(36): 1> +--- 1> | +--- (base class CBase0) 1> 0 | | {vfptr} 1> 4 | | {vbptr} 1> 8 | | m_var0 1> | +--- 1> | +--- (base class CBase1) 1> 12 | | {vfptr} 1> 16 | | {vbptr} 1> 20 | | m_var1 1> | +--- 1> 24 | m_var2 1> +--- 1> +--- (virtual base CSuper) 1> 28 | {vfptr} 1> 32 | m_var 1> +--- 1> 1> CDerived::$vftable@CBase0@: 1> | &CDerived_meta 1> | 0 1> 0 | &CBase0::func2 1> 1 | &CDerived::func4 1> 1> CDerived::$vftable@CBase1@: 1> | -12 1> 0 | &CDerived::func3 1> 1> CDerived::$vbtable@CBase0@: 1> 0 | -4 1> 1 | 24 (CDerivedd(CBase0+4)CSuper) 1> 1> CDerived::$vbtable@CBase1@: 1> 0 | -4 1> 1 | 12 (CDerivedd(CBase1+4)CSuper) 1> 1> CDerived::$vftable@CSuper@: 1> | -28 1> 0 | &CSuper::func0 1> 1 | &CDerived::func1 1> 1> CDerived::func1 this adjustor: 28 1> CDerived::func3 this adjustor: 12 1> CDerived::func4 this adjustor: 0 1> 1> vbi: class offset o.vbptr o.vbte fVtorDisp 1> CSuper 28 4 4 0
总结
- 虚函数表是用来实现多态的核心内容。
- 多继承很强大但是不要滥用,当多个基类都含有虚函数时,派生类会有多个指向虚函数表的指针。
- 忘记菱形继承吧,为了取消二义性引入虚继承,结果造成内存分布复杂而又难以理解,大道至简,回归本质吧!
-
菱形继承和虚函数
2017-10-11 17:28:37此篇博客主要介绍菱形虚拟继承和虚函数的混合情况下的模型分析。 一首先介绍一下虚函数的实质内容 代码如下 class A { public: int a; virtual void fun1() { cout ; } }; class B :public A { public: int b...此篇博客主要介绍菱形虚拟继承和虚函数的混合情况下的模型分析。
一.首先介绍一下虚函数的实质内容
1.没有虚继承的虚函数
首先我们来看对象a的虚函数机制class A { public: int a; virtual void fun1() { cout << "A::fun1 " << endl; } }; class B :public A { public: int b; virtual void fun1()//函数重写 { cout << "B::fun1 " << endl; } virtual void fun2() { cout << "B::fun2" << endl; } }; int main() { B b;//创建一个B的对象 A a; b.b = 20; a.a = 10; b.a = 30; system("pause"); return 0; }
接下里我们对B对象虚表里面的 函数进行验证,对象自己验证typedef void(*fun)();//这里定义fun为函数指针类型 fun *p = (fun *)*(int*)(&b); (*p)();//打印B::fun1 p++;//指向B::fun2 (*p)();//打印B::fun2
2.有虚继承的虚函数
首先看这个类的大小:
class A { public: int a; virtual void fun1() { cout << "A::fun1 "<< endl; } }; class B :virtual public A { public: int b; virtual void fun1()//函数重写 { cout << "B::fun1 " << endl; } virtual void fun2() { cout << "B::fun2" << endl; } }; int main() { B b; A a; b.b=20; a.a=10; sizeof(B); system("pause"); return 0; }
刚才无虚继承的类大小是12怎么加了个vurtual虚继承以后就变成了20呢?一下多了八个字节,接下来我们详细分析。
接下来打印验证:
typedef void(*fun)();//这里定义fun为函数指针类型 fun *p = (fun *)*(int*)(&b); (*p)(); p += 4;; (*p)();
3.菱形继承➕虚函数
首先给出基本代码:class A { public: int _a; virtual void fun1() { cout << "A::fun1" << endl; } virtual void fun2() { cout << "A::fun2" << endl; } }; class B1 : virtual public A { public: int _b1; virtual void fun1() { cout << "B1::fun1" << endl; } virtual void fun3() { cout << "B1::fun3" << endl; } }; class B2 :virtual public A { public: int _b2; virtual void fun1() { cout << "B2::fun1" << endl; } virtual void fun4() { cout << "B2::fun4" << endl; } }; class C :public B1,public B2 { public: int _c; virtual void fun1() { cout << "C::fun1" << endl; } virtual void fun3() { cout << "C::fun3" << endl; } virtual void fun4() { cout << "C::fun4" << endl; } virtual void fun5() { cout << "C::fun5" << endl; } }; int main() { cout <<"A:"<< sizeof(A) << endl; cout << "B1:"<<sizeof(B1) << endl; cout << "B2:"<<sizeof(B2) << endl; cout << "C:"<<sizeof(C) << endl; A a; B1 b1; B2 b2; C c; a._a = 10; b1._a = 10; b1._b1 = 20; b2._b2 = 30; b2._a = 10; c._c = 40; c._a = 10; c._b1 = 20; c._b2 = 30; system("pause"); return 0; }
先来看看每个对象的大小然后我在逐个对象分析其模型
注意这里的B是B1,图上写的是B
接下来打印验证
typedef void(*fun)();//这里定义fun为函数指针类型 fun *p = (fun *)*(int*)(&c); (*p)();//C::fun3 (*(p + 1))();//C::fun5 (*(p + 3))();//C::fun4 (*(p + 5))();//C::fun1 (*(p + 6))();//A::fun2
总结:1.无虚继承时在虚函数中,派生类中的虚函数非基类重写,则将其放置在继承来的虚函数表后面,不重新开辟虚函数表,并且将派生类中重写的虚函数替换基类中的虚函数
2.有虚继承虚函数时,派生类中的虚函数如果不是基类的重写的话,新开辟一个虚函数表将未重写虚函数放进去,将继承来的虚函数表放在其后面,并且将重写的替换
3.菱形继承+虚函数时,最重要的一点就是在,最低层的派生类中(上述例子就是C),只有一份基类(A)的虚表,并且在其模型(C模型)的最下面。并且将其没有重写的虚函数放在第一个继承模型(B1)的虚函数表下面,而且将其重写的也要替换为自己的。
-
带有虚函数的菱形继承和带有虚函数的菱形虚继承
2017-04-12 19:10:11对于某些函数来说,基类希望它的派生类定义适合自身的版本,此时基类就将这些函数声明为虚函数。...【带有虚函数的菱形继承】 以下图的模型为例进行分析: class A { public : A() :a(1) {}对于某些函数来说,基类希望它的派生类定义适合自身的版本,此时基类就将这些函数声明为虚函数。在存在虚函的类,创建对象时会产生虚表指针,虚表指针指向一个虚表,这时就可以通过虚表访问自己定义的函数。通过下面两种继承进行分析:【带有虚函数的菱形继承】以下图的模型为例进行分析:
我们观察C类对象在内存中的结构:class A { public : A() :a(1) {} virtual void add() { cout << "A::add()" << endl; } int a; }; class B1 : public A { public: B1() :b1(2) {} virtual void add() { cout << "B1::add()" << endl; } virtual void FunB1() { cout << "FunB1()" << endl; } int b1; }; class B2 : public A { public: B2() :b2(3) {} virtual void add() { cout << "B2::add()" << endl; } virtual void FunB2() { cout << "FunB2()" << endl; } int b2; }; class C : public B1,public B2 { public: C() :c(4) {} virtual void add() { cout << "C::add()" << endl; } int c; };
当发生继承时,如果派生类重写了基类的虚函数,那么派生类的对象中会修改基类的虚表,虚表中的函数指针会指向派生类自己重写的函数,如果派生类没有重写基类的虚函数,那么派生类不会改变那个虚函数的指向只是把它继承下来。通过上图发现,B1类的虚表指针中add()函数被替换为C类的add()函数指针,B2类的虚表指针中add()函数也被替换为C类的add()函数指针,但是却发现这两个指针不相同,这可以说明两个函数不是同一个函数么?这是不可以的!通过上图发现,所说的函数指针其实指向的是一条跳转语句,在跳转之后会做一些修正最后使他们访问的是同一个函数。在发生动态绑定时,运行的过程:【带有虚函数的菱形虚拟继承】对于菱形虚拟继承我们知道,派生类在继承时加上virtual关键字就说明,它可以共享它的基类,当多个派生类都虚继承一个基类,并且有其他的类继承于派生类,那么在派生类的子类的对象中只会有一份基类的成员,在每个派生类成员中都会有一个指针指向一块空间,空间里面存放了派生类相对于自己的偏移量和相对于基类的偏移量。我们同通过实例来进一步研究(以下图的模型为例):【带有虚函数的菱形虚拟继承】对于菱形虚拟继承我们知道,派生类在继承时加上virtual关键字就说明,它可以共享它的基类,当多个派生类都虚继承一个基类,并且有其他的类继承于派生类,那么在派生类的子类的对象中只会有一份基类的成员,在每个派生类成员中都会有一个指针指向一块空间,空间里面存放了派生类相对于自己的偏移量和相对于基类的偏移量。我们同通过实例来进一步研究(以下图的模型为例):
我们观察C类对象在内存中的结构:class A { public : A() :a(1) {} virtual void add() { cout << "A::add()" << endl; } int a; }; class B1 : virtual public A { public: B1() :b1(2) {} virtual void add() { cout << "B1::add()" << endl; } virtual void FunB1() { cout << "FunB1()" << endl; } int b1; }; class B2 :virtual public A { public: B2() :b2(3) {} virtual void add() { cout << "B2::add()" << endl; } virtual void FunB2() { cout << "FunB2()" << endl; } int b2; };
总结:- 当一个类有虚函数时,在其对象的开始会生成一个虚表指针,指向的虚表中会存放虚函数的地址。
- 当一个派生类继承一个带有虚函数的基类时,派生类对象成员中基类部分中的虚表指针会被派生类所修改,成为派生类自己的虚表指针。在虚表中,如果虚函数在派生类中被重写那么就会存放被重写过的虚函数指针,如果没有重写基类的虚函数,就会单纯的继承下来存放基类的虚函数指针。
- 当一个派生类继承多个带有虚函数的基类时,派生类对象成员中基类部分中的虚表指针会被派生类所修改,成为派生类自己的虚表指针(相当于派生类自己有多个虚表指针)。如果重写了某个基类的虚函数,那么对应的去修改继承于这个基类的成员中的虚表,使指针指向派生类重写过得虚函数,如果没有重写就单纯的继承。
-
菱形继承的虚函数表
2017-02-22 20:21:25虚函数:类的成员函数前面加virtual 虚函数的主要作用就是实现多态 那么多态:顾名思义就是一种事物的多种形态。c++中,指具有不同功能的函数...菱形继承举例:#include using namespace std; class AA { public: -
菱形继承(虚函数)->菱形虚拟继承(虚函数)->多态系列问题
2017-07-27 13:53:21读者注意:阅读这篇文章时,对继承中的对象模型要有一定了解;觉得自己不确定的话单击下面的“继承”。继承多态多态按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。直白点理解... -
C++继承之菱形继承与虚函数
2018-05-03 12:26:50菱形继承单继承与多继承 单继承:一个子类只有一个直接父类时称这个继承关系为单继承 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承菱形继承菱形继承引发的问题:数据冗余例:B、C中的_a都是来自... -
C++继承(多继承)之菱形继承与虚函数
2020-10-24 15:52:24一、单继承&多继承&菱形继承 单继承与多继承 单继承:一个子类只有一个直接父类时称这个继承关系为单继承 多继承:一个子类有两个或以上直接...二、虚继承-------解决菱形继承的数据冗余与二义性 将B和C的继承 -
C++之菱形继承与虚继承(含虚函数)
2018-05-06 10:21:07下面我们先接着上次讲有虚函数的菱形虚继承首先什么是虚函数。?虚函数:在类里面,函数前面有virtual关键字的成员函数就是虚函数。代码块:class base { public: base() { cout << "base()&... -
C++之继承,菱形继承,虚继承,虚函数,纯虚函数
2018-04-22 20:31:40继承,访问限定 概念:当一个类被其他的类继承时,被继承的类称为基类,又称父类;继承其他的类叫做派生类,又称子类。基类定义了所有派生类的共有属性,派生类在继承共有属性的同时,又增加了自己特有的属性。 ... -
虚函数的内存结构,菱形继承的虚函数内存结构
2019-05-14 22:04:27https://blog.csdn.net/u010235142/article/details/78307022
-
织梦磨矿球磨机类机械设备网站织梦模板(带手机端)
-
2B获客新技巧:官网+功能表单妙用三部曲(一)
-
精通编译Makefile,Nina, 从底层uboot到Android
-
PowerBI重要外部工具详解
-
Java生成随机码效率对比
-
54. 螺旋矩阵
-
Rocket.Chat.PWA:使用Angular构建的带宽高效,简化的客户端-源码
-
校外测试-源码
-
MySQL 高可用工具 DRBD 实战部署详解
-
Cmd 命令提示符
-
lukaszkostrzewa.github.io-源码
-
【Python-随到随学】 FLask第一周
-
运行级别 - 如何判断当前系统是图形化安装还是命令行标准安装?
-
java在使用JSON.toJSONString时出现了$ref问题解决方案
-
MySQL 四类管理日志(详解及高阶配置)
-
python-for-loops-lab-ds-apply-000-源码
-
Galera 高可用 MySQL 集群(PXC v5.6 + Ngin
-
Delani-studio:Delani工作室网站显示其服务以及如何通过表格与他们联系。 使用下面的链接去网站-源码
-
织梦广告品牌设计机构网站织梦模板
-
织梦响应式中小学早教教育机构类网站织梦模板(自适应手机端)