分析多继承下的动态多态。
Posted shy_BIU
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分析多继承下的动态多态。相关的知识,希望对你有一定的参考价值。
一、首先我们先了解一下三个概念:
1.重载。2.隐藏。3.覆盖(重写)
如何实现重载?——2个条件:
1-在同一作用域内。
2-两个函数函数名相同,参数不同,返回值可以不同。
此时两个函数就实现了重载,当然这是C++对于C特有的,因为C的时候对参数并没有太多的考虑,C++的编译器在编译时对函数进行了重命名,所以就算是函数名相同的函数,如果参数不同,就会是不同的函数,对应不同的情况。
如何实现隐藏/重定义?——2个条件:
1-在不同作用域下,大多在继承上体现。
2-函数名相同即可。
例如在 B类公有继承了A类,此时B类和A类都声明并定义了一个fun()公有成员函数,虽然B类也能获得A中的fun()函数,但是因为B中也有一个同名函数,所以对fun()函数进行了重定义,A类的fun()函数依然存在,但是如果通过B类实例化对象来调用fun()函数,只能得到B类fun()函数。
如何实现覆盖/重写?——3个条件:
1-不在同一作用域下。(分别在基类和继承类)
2-函数名相同,参数相同。访问修饰可以不相同
3-基类函数必须与virtual关键字。
如果加了virtual关键字,那么此时A类就会生成一个虚函数表。他的继承类也会生成一个虚函数表,每个对象里都会有一个虚函数表指针,虚函数表指针指向每个对象对应的虚函数表。从而达到A类调用A的fun()函数,B类调用B的fun()函数的效果。但是此时每个虚函数都发生了函数覆盖,也就是说通过A类对象调用B类的fun()函数是实现不到的,反之亦然。
二、接下来分析第二个概念:
动态多态和静态多态。
何谓静态多态?
静态多态就是通过函数重载实现,静态多态是在编译期完成,所以其效率也很高。在同一函数名的条件下,通过赋予不同参数来达到不同效果。这就是静态多态,很重要的一点是静态多态通过模板编程为C++带来了泛型设计的概念(全特化,偏特化),例如STL库。
何谓动态多态?
动态多态是通过虚函数表和继承实现的。关于继承需要记得赋值兼容规则里的一条重要原则:父类指针可以指向子类对象。虚函数表是属于类的,存在虚函数表的类会在实例化对象的时候给对象分配一个虚函数表指针(这也可以接受为什么只有构造函数和定义为纯虚函数的析构函数的类大小为4——切记,纯虚函数的类无法实例化出对象)。
编译器能够通过对象内部得虚函数表指针找到对应的虚函数表,并调取相对应的函数。
三、开始我们今天的主题——多继承下的动态多态:
1-重复继承
要理解重复继承,说不如看代,先看如下代码:
1 class First 2 { 3 public: 4 int if_; 5 char cf_; 6 public: 7 First() 8 :if_(1) 9 , cf_(\'F\') 10 { 11 cout << "First" << endl; 12 } 13 virtual void f() 14 { 15 cout << "First_f()" << endl; 16 } 17 virtual void Bf() 18 { 19 cout << "First_Bf()" << endl; 20 } 21 };
这里我们定义了一个First类当做基类,包含一个构造函数和两个虚函数,让我们接着看:
1 class Second_1 :public First 2 { 3 public: 4 int is_1; 5 char cs_1; 6 public: 7 Second_1() 8 :is_1(11) 9 , cs_1(\'S\') 10 { 11 cout << "Second_1" << endl; 12 } 13 virtual void f() 14 { 15 cout << "Second_1_f()" << endl; 16 } 17 virtual void f1() 18 { 19 cout << "Second_1_f1()" << endl; 20 } 21 virtual void Bf1() 22 { 23 cout << "Second_1_Bf1()" << endl; 24 } 25 }; 26 27 class Second_2 :public First 28 { 29 public: 30 int is_2; 31 char cs_2; 32 public: 33 Second_2() 34 :is_2(21) 35 , cs_2(\'e\') 36 { 37 cout << "Second_2" << endl; 38 } 39 virtual void f() 40 { 41 cout << "Second_2_f()" << endl; 42 } 43 virtual void f2() 44 { 45 cout << "Second_2_f2()" << endl; 46 } 47 virtual void Bf2() 48 { 49 cout << "Second_2_Bf2()" << endl; 50 } 51 };
之后我们分别定义了一个Second_1类和Second_2类来继承First类,分别包含各自的构造函数,一个与基类同名的虚函数,两个各自特有的虚函数,好嘞,接下来:
1 class Third :public Second_1,public Second_2 2 { 3 public: 4 int it; 5 char ct; 6 public: 7 Third() 8 :it(31) 9 , ct(\'T\') 10 { 11 cout << "Third" << endl; 12 } 13 virtual void f() 14 { 15 cout << "Third_f()" << endl; 16 } 17 virtual void f1() 18 { 19 cout << "Third_f1()" << endl; 20 } 21 virtual void f2() 22 { 23 cout << "Third_f2()" << endl; 24 } 25 virtual void Bf3() 26 { 27 cout << "Third_f3()" << endl; 28 } 29 };
最后我们定义一个Third类,包含一个构造函数,一个与基类同名的虚函数,一个与Second_1类同名的虚函数,一个与Second_2类同名的虚函数,一个自己特有的虚函数。
为了研究方便,我把所有的继承和成员属性都设定为了public。让我们用一个图来解释一下上述的继承关系:
可以很清晰的看到,Third类所继承的Second_1类与Second_2类分别包含了一份First类,其中一份或者两份可能为拷贝,那么此时如果我Third的对象想要调用First类的成员就会产生二义性,因为编译器无法分辨你究竟想要调用哪个派生类里的基类成员,这,就是重复继承。
针对上述代码,我编写了如下main()函数:
1 int main() 2 { 3 Third th; 4 Second_1 se_1; 5 Second_2 se_2; 6 7 Print(*((int *)&th)); 8 9 getchar(); 10 return 0; 11 }
Print函数我们先放一边,接着会讲到。
我分别实例化了三个对象:Third类对象th,Second_1类对象se_1,Second_2类对象se_2。让我们按F10,程序跑起来。
首先,我们定义完三个对象,我们得到:
这里的显示不用过多解释,首先实例化Third类的对象,调用Third的构造函数,当Third有父类时,先调用父类构造函数,首先调用Second_1,Second_1又要向上调用First,再回来Third还要调用Second_2,Second_2调用First,从而得到如上结果。
我们打开内存,直接来看对象th对象内部得东西:
这里面我们能看到一些熟悉的值,根据大小端得到它们分别为0x00000001=1,0x0000000b=11,0x00000015=21,0x0000001f=31。再回过头来看我们上面的构造函数,1,11,21,31分别是First类中的if_、Second_1类中的is_1、Second_2类中的is_2、Third类中的it。
再让我们打开监视,来看一下th内部__vfptr虚函数表指针的地址:
不妨猜测一下,这Third类对象th中的内容是什么,根据我们上述得到的规律:
这里__vfptr代表虚函数表指针,因为是重复继承,我们可以得到两个上图中的两个虚函数表指针分别指向不同的两个虚函数表,而且虚函数表中函数如同我们猜测。
如何验证我们的猜测?这里就用到了我们的Print()函数,我们根据虚函数表指针是对象的第一个成员的规律来写出如下代码:
1 typedef void(*PFUN)(); 2 void Print(int &p) 3 { 4 int *pf = (int *)p; 5 while (* pf) 6 { 7 PFUN pfun = (PFUN)*pf; 8 pfun(); 9 pf++; 10 } 11 }
我们利用一个函数指针来指向我们的虚函数表,从而输出虚函数表中的函数,我们利用这个规律,首先检验&th处,也就是Second_1类中__vfptr指向的地址——Second_1的虚函数表。
得到如下结果:
事实证明是对的,Second_1类的虚函数表中的 f() 函数和 f1() 函数都发生了覆盖。
我们再加上如下代码:
1 cout << "-------------------" << endl;
2 Print(*((int *)&th+5));
进一步验证了我们的猜想:重复继承下下虚函数表不再是放到基类里,因为这样会存在二义性,而是放在Second_1类和Second_2类中,他们分别发生了部分函数的覆盖。
我们也看到了C++的不安全,如果给我一个虚函数表位置,我就可以访问到父类的自己拥有的虚函数,这是非常不安全的。
2-菱形继承
要想研究菱形继承,我们只需将上述的Second_1类和Second_2类的继承方式前加上virtual即可:
1 class Second_1 :virtual public First 2 { 3 public: 4 int is_1; 5 char cs_1; 6 public: 7 Second_1() 8 :is_1(11) 9 , cs_1(\'S\') 10 { 11 cout << "Second_1" << endl; 12 } 13 virtual void f() 14 { 15 cout << "Second_1_f()" << endl; 16 } 17 virtual void f1() 18 { 19 cout << "Second_1_f1()" << endl; 20 } 21 virtual void Bf1() 22 { 23 cout << "Second_1_Bf1()" << endl; 24 } 25 }; 26 27 class Second_2 :virtual public First 28 { 29 public: 30 int is_2; 31 char cs_2; 32 public: 33 Second_2() 34 :is_2(21) 35 , cs_2(\'e\') 36 { 37 cout << "Second_2" << endl; 38 } 39 virtual void f() 40 { 41 cout << "Second_2_f()" << endl; 42 } 43 virtual void f2() 44 { 45 cout << "Second_2_f2()" << endl; 46 } 47 virtual void Bf2() 48 { 49 cout << "Second_2_Bf2()" << endl; 50 } 51 };
此时让我们再打开看th的内存:
这里我们还是能看到我们熟悉的身影:0x0000000b=11,0x00000015=21,0x00000001f=31,0x00000001=1。
继续打开我们的监视,我们可以看到th对象内部的__vfptr虚函数表指针的位置:
我们继续根据这些来得到我们的内存猜测:
我们先把那两个地址以及那个0x0000000放到一边,首先那三个__vfptr指针是否属实——
继续利用Print()函数和如下代码:
1 Print(*(int *)&th); 2 cout << "--------------" << endl; 3 Print(*((int *)&th + 4)); 4 cout << "--------------" << endl; 5 Print(*((int *)&th + 11));
得到如下结果:
接下来,我们继续探讨那两个地址,我将地址输入到内存窗口,得到如下结果:
不难看出,第一个数据都为-4,第二个数据一个为0x00000028=40,一个为0x00000018=24,这两个数字代表什么?
我们再回到内存,不难发现在第一个地址处动40个字节,也就是10个地址就能找到属于First类的虚函数表指针,再第二个指针处动24个字节,也就是6个地址就能找到属于First类的虚函数表指针。
那我们就可以完善我们的猜测:(0x0000000在我估计是一个结束标识,并未深究,输在抱歉)
所以可以看出,虚继承在消除二义性上起到了关键的作用,从直接给派生类两份备份的简单粗暴变成了分别给了一个地址让派生类可以通过地址来找到基类并且调用相关的成员函数和数据成员,还减少了数据冗余,这也是C++令人着迷的地方。
文笔不好,感谢审阅。
以上是关于分析多继承下的动态多态。的主要内容,如果未能解决你的问题,请参考以下文章
C++ 多态 : 虚函数静态绑定动态绑定单/多继承下的虚函数表
C++ 多态 : 虚函数静态绑定动态绑定单/多继承下的虚函数表