37. }
38.};
39.int main(void)
40.{
41. Derived d;
42. Base *pb = &d;
43. Derived *pd = &d;
44. // Good :
behavior depends solely on type of the object
45. pb->f(3.14f); // Derived:
:
f(float) 3.14
46. pd->f(3.14f); // Derived:
:
f(float) 3.14
47.
48. // Bad :
behavior depends on type of the pointer
49. pb->g(3.14f); // Base:
:
g(float) 3.14
50. pd->g(3.14f); // Derived:
:
g(int) 3
51.
52. // Bad :
behavior depends on type of the pointer
53. pb->h(3.14f); // Base:
:
h(float) 3.14
54. pd->h(3.14f); // Derived:
:
h(float) 3.14
55. return 0;
56.}
令人迷惑的隐藏规则
本来仅仅区别重载与覆盖并不算困难,但是C++的隐藏规则使问题复杂性陡然增加。
这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。
此时,不论有无virtual
关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual
关键字。
此时,基类的函数被隐藏(注意别与覆盖混淆)。
上面的程序中:
(1)函数Derived:
:
f(float)覆盖了Base:
:
f(float)。
(2)函数Derived:
:
g(int)隐藏了Base:
:
g(float),而不是重载。
(3)函数Derived:
:
h(float)隐藏了Base:
:
h(float),而不是覆盖。
C++纯虚函数
一、定义
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。
在基类中实现纯虚函数的方法是在函数原型后加“=0”
virtualvoidfuntion()=0
二、引入原因
1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
2、在很多情况下,基类本身生成对象是不合情理的。
例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:
virtualReturnTypeFunction()=0;),则编译器要求在派生类中必须予以重写以实现多态性。
同时含有纯虚拟函数的类称为抽象类,它不能生成对象。
这样就很好地解决了上述两个问题。
三、相似概念
1、多态性
指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。
C++支持两种多态性:
编译时多态性,运行时多态性。
a、编译时多态性:
通过重载函数实现
b、运行时多态性:
通过虚函数实现。
2、虚函数
虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override)
3、抽象类
包含纯虚函数的类称为抽象类。
由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。
1.只有类的成员函数才能声明为虚函数
2.静态成员函数不能是虚函数
3.内联函数不能是虚函数
4.构造函数不能是虚函数
5.析构函数可以使虚函数而且通常声明为虚函数
一、知识点
1、一个操作随着所传递的对象类型的不同能够做出不同的反应,其行为模式成为多态。
(P413)
2、基类与派生类的同名操作,只要标记上virtual,则该操作便具有多态性。
(P416)
3、一旦标记基类的函数为虚函数,便有连锁反应,后面继承的类中一切同名成员函数都变成了虚函数。
如果是引发实际复制动作的传递,则子类对象完全变成基类对象了,这时候,便不会再有悬念了,即不会有多态了。
因为在参数传递的过程中已经将对象的性质做了肯定的转变。
而对于确定的对象,是没有选择操作可言的。
因此说白了,就是仅仅对于对象的指针和引用的间接访问,才会发生多态现象。
(P417)
4、虚函数机理:
(1)、通过预先设定其成员函数的虚函数性质,使得任何捆绑该成员函数的未定类型的对象操作在编译时,都以一个不确定的指针特殊地“引命待发”来编码,到了运行时,遇到确定类型的对象,才突然指定其真正的行为。
即滞后到运行时,根据具体类型的对象来捆绑成员函数。
(2)、多态性实现的原理:
当将函数声明为virtual时,编译器在编译的时候,发现类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表,该表是一个一维数组,在这个数组中存放着每个虚函数的地址。
那么如何确定虚表呢?
编译器还为每个类的对象提供了一个虚表指针,这个指针指向了对象所属类的虚表。
在程序运行时,根据对象的类型去初始化虚指针,从而让虚指针正确的指向所属类的虚表,从而在调用虚函数时,能够找到正确的函数。
在构造函数总,进行了虚表的创建和虚表指针的初始化。
在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否还有继承者,它初始化父类对象的虚表指针,该虚表指针指向父类的虚表。
执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。
对于虚函数调用来说,每个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。
所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以才能实现动态的对象函数调用,这就是C++多态性实现的原理。
(5)、总结(基类有虚函数):
a、每一个类都有虚表。
b、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。
如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。
如果派生类有自己的虚函数,那么虚表中就会添加该项。
c、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
6、编译器看见虚函数调用,就要做滞后处理。
由于间接访问比直接访问绕了一个弯,于是付出了时间代价和保存若干指针地址的空间代价。
为了在使用类的编程中随时随地体现多态性,只要是继承结构,应尽量将成员函数设计成虚函数。
(P418)
7、虚函数在继承层次结构中总是会自动地从基类传播下去的。
因此派生类中重载的虚函数的virtual可以省略。
(P419)
8、虚函数用于继承结构层次中的基类与子类。
除了基类与子类的函数名必须相同外,连参数类型、个数和顺序都要相同。
(P420)
9、若干规则:
(P423)
(1)、静态成员函数不能是虚函数,因为静态成员函数不受对象的捆绑。
多态是针对不同的对象,执行同一名称的操作,而能强健地做出不同的抉择的机制。
(2)、内联函数不能是虚函数,因为内联函数是不能在运行中动态地确定其位置的。
即使虚函数在类的内部定义,编译时,仍将其看做是非内联的。
(3)、构造函数不能是虚函数,对象还是一片未定型的处女地,还没有对象。
(4)、析构函数可以是虚函数且通常声明为虚函数。
10、对象的操作并不是先在容器外面都搞定然后再进入容器排队等待输出的,多态更多是直接在容器中显现出来,因为只有容器才适合于处理批量对象,更贴近问题所要的操作。
(P436)
11、dynamic_cast操作是专门针对有虚函数的继承结构来的,它将基类指针转换成想要的子类指针,以做好子类操作的准备,因为各个不同的子类,其操作可能是不同的。
dynamic_cast操作所针对的基类指针(即括号中的表达式),如果所指向的对象中不含有想要的子类对象,则将得到0值结果。
例如:
(P438)
Savingss("8288",1000);
Account*pa=&s;
Checking*pc=dynamic_cast(pa);
据此,判断转换后的指针是否为0,就能拍出不必要的操作错误而有把握地进行想要的操作。
当然,任何其他的不符多态类要求的对象,或者0指针,其转换结果也都将归0。
12、相对于动态类型转换,静态类型转换则做范围更广的转换,但前提必须是相关的类型,也就是说,编译器必须认为可理解。
如,研究生对象的指针到学生对象的指针,或反之。
由于void*到任何类型的指针都可以进行相融性转换,所以,void*到学生对象的指针转换也可以由static_cast来进行,还有从局部堆空间申请的空间转换为整型数组空间等。
使用static_cast可以带来类型安全检查帮助,比无根据地进行类型转换形式type(表达式)的“防盗性”要强,因为通过指针值的非0判断,static_cast可以避免该转换后的操作失常。
(P440)
13、写开禁操作可以去掉常量或者常对象的常量性:
(P441)
char*p=const_cast(max("hello","world"));