1、C+多态性C+编程语言是一款应用广泛,支持多种程序设计的计算机编程语言。我们今天就会为大家详细介绍其中C+多态性的一些基本知识,以方便大家在学习过程中对此能够有一个充分的掌握。多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。多态(polymorphisn),字面意思多种形状。C+多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。(这里我觉得要补充,重写的话可以有两种,直接重写成员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C+多态性)而重载则是允许有
2、多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。编译器会根据这些函数的不同列表,将同名的函数的名称做修饰,从而生成一些不同名称的预处理函数,来实现同名函数调用时的重载问题。但这并没有体现多态性。多态与非多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。那么多态的作用是什么呢,封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用
3、。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C+多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有多态性,函数调用的地址将是一定的,而固定的地址将始终调用到同一个函数,这就无法实现一个接口,多种方法的目的了。笔试题目:cppview plaincopy1. #include2. usingnamespacestd;3. c
4、lassA4. 5. public:6. voidfoo()7. 8. printf(1n);9. 10. virtualvoidfun()11. 12. printf(2n);13. 14. ;15. classB:publicA16. 17. public:18. voidfoo()19. 20. printf(3n);21. 22. voidfun()23. 24. printf(4n);25. 26. ;27. intmain(void)28. 29. Aa;30. Bb;31. A*p=&a;32. p-foo();33. p-fun();34. p=&b;35. p-foo();3
5、6. p-fun();37. return0;38. 第一个p-foo()和p-fuu()都很好理解,本身是基类指针,指向的又是基类对象,调用的都是基类本身的函数,因此输出结果就是1、2。 第二个输出结果就是1、4。p-foo()和p-fuu()则是基类指针指向子类对象,正式体现多态的用法,p-foo()由于指针是个基类指针,指向是一个固定偏移量的函数,因此此时指向的就只能是基类的foo()函数的代码了,因此输出的结果还是1。而p-fun()指针是基类指针,指向的fun是一个虚函数,由于每个虚函数都有一个虚函数列表,此时p调用fun()并不是直接调用函数,而是通过虚函数列表找到相应的函数的地址
6、,因此根据指向的对象不同,函数地址也将不同,这里将找到对应的子类的fun()函数的地址,因此输出的结果也会是子类的结果4。笔试的题目中还有一个另类测试方法。即B *ptr = (B *)&a; ptr-foo(); ptr-fun();问这两调用的输出结果。这是一个用子类的指针去指向一个强制转换为子类地址的基类对象。结果,这两句调用的输出结果是3,2。并不是很理解这种用法,从原理上来解释,由于B是子类指针,虽然被赋予了基类对象地址,但是ptr-foo()在调用的时候,由于地址偏移量固定,偏移量是子类对象的偏移量,于是即使在指向了一个基类对象的情况下,还是调用到了子类的函数,虽然可能从始到终都没
7、有子类对象的实例化出现。而ptr-fun()的调用,可能还是因为C+多态性的原因,由于指向的是一个基类对象,通过虚函数列表的引用,找到了基类中fun()函数的地址,因此调用了基类的函数。由此可见多态性的强大,可以适应各种变化,不论指针是基类的还是子类的,都能找到正确的实现方法。cppview plaincopy1. /小结:2. 1、有virtual才可能发生多态现象3. 2、不发生多态(无virtual)调用就按原类型调用4. #include5. usingnamespacestd;6. 7. classBase8. 9. public:10. virtualvoidf(floatx)11
8、. 12. coutBase:f(float)xendl;13. 14. voidg(floatx)15. 16. coutBase:g(float)xendl;17. 18. voidh(floatx)19. 20. coutBase:h(float)xendl;21. 22. ;23. classDerived:publicBase24. 25. public:26. virtualvoidf(floatx)27. 28. coutDerived:f(float)xendl;/多态、覆盖29. 30. voidg(intx)31. 32. coutDerived:g(int)xendl;/
9、隐藏33. 34. voidh(floatx)35. 36. coutDerived:h(float)xf(3.14f);/Derived:f(float)3.1446. pd-f(3.14f);/Derived:f(float)3.1447. 48. /Bad:behaviordependsontypeofthepointer49. pb-g(3.14f);/Base:g(float)3.1450. pd-g(3.14f);/Derived:g(int)351. 52. /Bad:behaviordependsontypeofthepointer53. pb-h(3.14f);/Base:h
10、(float)3.1454. pd-h(3.14f);/Derived:h(float)3.1455. return0;56. 令人迷惑的隐藏规则本来仅仅区别重载与覆盖并不算困难,但是C+的隐藏规则使问题复杂性陡然增加。这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。上面的程序中:(1)函数Derived:f(fl
11、oat)覆盖了Base:f(float)。(2)函数Derived:g(int)隐藏了Base:g(float),而不是重载。(3)函数Derived:h(float)隐藏了Base:h(float),而不是覆盖。C+纯虚函数一、定义 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” virtual void funtion()=0二、引入原因 1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。 2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类
12、,但动物本身生成对象明显不合常理。 为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。三、相似概念 1、多态性 指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C+支持两种多态性:编译时多态性,运行时多态性。 a、编译时多态性:通过重载函数实现 b、运行时多态性:通过虚函数实现。 2、虚函数 虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数
13、,可实现成员函数的动态覆盖(Override) 3、抽象类 包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。1. 只有类的成员函数才能声明为虚函数2. 静态成员函数不能是虚函数3. 内联函数不能是虚函数4. 构造函数不能是虚函数5. 析构函数可以使虚函数而且通常声明为虚函数一、知识点1、一个操作随着所传递的对象类型的不同能够做出不同的反应,其行为模式成为多态。(P413)2、基类与派生类的同名操作,只要标记上virtual,则该操作便具有多态性。(P416)3、一旦标记基类的函数为虚函数,便有连锁反应,后面继承的类中一切同名成员函数都变成了虚函数。如果
14、是引发实际复制动作的传递,则子类对象完全变成基类对象了,这时候,便不会再有悬念了,即不会有多态了。因为在参数传递的过程中已经将对象的性质做了肯定的转变。而对于确定的对象,是没有选择操作可言的。因此说白了,就是仅仅对于对象的指针和引用的间接访问,才会发生多态现象。(P417)4、虚函数机理:(1)、通过预先设定其成员函数的虚函数性质,使得任何捆绑该成员函数的未定类型的对象操作在编译时,都以一个不确定的指针特殊地“引命待发”来编码,到了运行时,遇到确定类型的对象,才突然指定其真正的行为。即滞后到运行时,根据具体类型的对象来捆绑成员函数。(2)、多态性实现的原理:当将函数声明为virtual时,编译
15、器在编译的时候,发现类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表,该表是一个一维数组,在这个数组中存放着每个虚函数的地址。那么如何确定虚表呢?编译器还为每个类的对象提供了一个虚表指针,这个指针指向了对象所属类的虚表。在程序运行时,根据对象的类型去初始化虚指针,从而让虚指针正确的指向所属类的虚表,从而在调用虚函数时,能够找到正确的函数。在构造函数总,进行了虚表的创建和虚表指针的初始化。在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否还有继承者,它初始化父类对象的虚表指针,该虚表指针指向父类的虚表。执行子类的构造函数时,子类对象的虚表指针被初始化
16、,指向自身的虚表。对于虚函数调用来说,每个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以才能实现动态的对象函数调用,这就是C+多态性实现的原理。(5)、总结(基类有虚函数):a、每一个类都有虚表。b、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中
17、就会添加该项。c、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。6、编译器看见虚函数调用,就要做滞后处理。由于间接访问比直接访问绕了一个弯,于是付出了时间代价和保存若干指针地址的空间代价。为了在使用类的编程中随时随地体现多态性,只要是继承结构,应尽量将成员函数设计成虚函数。(P418)7、虚函数在继承层次结构中总是会自动地从基类传播下去的。因此派生类中重载的虚函数的virtual可以省略。(P419)8、虚函数用于继承结构层次中的基类与子类。除了基类与子类的函数名必须相同外,连参数类型、个数和顺序都要相同。(P420)9、若干规则:(P423)(1)、静态成员函数不能
18、是虚函数,因为静态成员函数不受对象的捆绑。多态是针对不同的对象,执行同一名称的操作,而能强健地做出不同的抉择的机制。(2)、内联函数不能是虚函数,因为内联函数是不能在运行中动态地确定其位置的。即使虚函数在类的内部定义,编译时,仍将其看做是非内联的。(3)、构造函数不能是虚函数,对象还是一片未定型的处女地,还没有对象。(4)、析构函数可以是虚函数且通常声明为虚函数。10、对象的操作并不是先在容器外面都搞定然后再进入容器排队等待输出的,多态更多是直接在容器中显现出来,因为只有容器才适合于处理批量对象,更贴近问题所要的操作。(P436)11、dynamic_cast 操作是专门针对有虚函数的继承结构
19、来的,它将基类指针转换成想要的子类指针,以做好子类操作的准备,因为各个不同的子类,其操作可能是不同的。dynamic_cast操作所针对的基类指针(即括号中的表达式),如果所指向的对象中不含有想要的子类对象,则将得到0 值结果。例如:(P438)Savings s(8288, 1000);Account *pa =&s;Checking *pc = dynamic_cast(pa);据此,判断转换后的指针是否为0,就能拍出不必要的操作错误而有把握地进行想要的操作。当然,任何其他的不符多态类要求的对象,或者0指针,其转换结果也都将归0。12、相对于动态类型转换,静态类型转换则做范围更广的转换,但
20、前提必须是相关的类型,也就是说,编译器必须认为可理解。如,研究生对象的指针到学生对象的指针,或反之。由于void * 到任何类型的指针都可以进行相融性转换,所以,void *到学生对象的指针转换也可以由static_cast来进行,还有从局部堆空间申请的空间转换为整型数组空间等。使用static_cast可以带来类型安全检查帮助,比无根据地进行类型转换形式type (表达式)的“防盗性”要强,因为通过指针值的非 0 判断,static_cast可以避免该转换后的操作失常。(P440)13、写开禁操作可以去掉常量或者常对象的常量性:(P441)char *p = const_cast(max(hello, world);
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1