1.多态的定义与作用
同一个操作作用于不同对象,可以有不同的解释,产生不同的效果。
广义上的多态
多态分为静态多态(编译时多态、静态联编、早绑定)和动态多态(运行时多态、动态联编、晚绑定)
静态多态包括泛型编程、函数重载等,编译器会根据额函数调用多个对象类型,在编译阶段就确定函数的调用地址。
动态多态通过指针或引用表示对象调用虚函数实现,在运行阶段才确定调用那个函数。
狭义上的多态
动态多态
C++语言支持多态性的根本所在:指针和引用的静态类型和动态类型的不同。 —《C++PRIMER》
多态的作用
- 解决项目中的紧耦合问题,提供程序的可扩展性。
- 应用程序不必为每一个子类的功能调用编写代码,只需要对抽象的父类处理。
2.静态类型与动态类型
静态类型
表达式的静态类型在编译时总是已知的,是变量类型或表达式生成类型。
动态类型
是变量或表达式表示的内存中的对象的类型,直到运行时才可知。
两者的关系
基类的指针和引用的静态类型可能与动态类型不一致,若表达式既不是指针也不是引用,则静态类型和动态类型永远一致。
3. 动态多态(运行时多态、动态联编、晚绑定)
当且仅当通过指针或引用调用虚函数时才会在运行时解析该调用,因为只有这种情况下对象的动态类型和静态类型才会不同。
例子1
#include<iostream>
using namespace std;
class Maker {
public:
virtual void speak() {
cout << "Maker" << endl;
}
};
class sonOfMaker :public Maker {
public:
void speak() {
cout << "sonOfMaker" << endl;
}
};
void dynamicbinding(Maker *bd) {
bd->speak();
}
int main()
{
sonOfMaker *sm = new sonOfMaker;
dynamicbinding(sm);
delete(sm);
return EXIT_SUCCESS;
}
此时输出sonOfMaker;若去掉virtual则输出Maker。
解释1
上面的例子中通过用指针调用virtual函数实现了动态联编,运行时确定调用的对象是sonOfMaker类型的, 所以输出的是sonOfMaker;
而去掉virtual之后,非虚函数的调用都是编译时确定的,编译时dynamicbinding(Maker *bd)调用对象是Maker类型的指针, 所以最后输出的是Maker。
例子2
#include <iostream>
using namespace std;
class shape{
public:
void virtual draw(){cout<<"I am shape"<<endl;}
void fun(){draw();}
};
class circle:public shape{
public:
void draw(){cout<<"I am circle"<<endl;}
};
void main(){
circle oneshape;
oneshape.fun();
}
————————————————
版权声明:本文为CSDN博主「Miibotree」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https:
这个例子中输出的是I am circle;若去掉virtual 输出的是I am shape(这里很奇怪,类似情况java中会输出I am circle)。
解释2
这个例子是从博主「Miibotree」那里转过来的,博主认为上面有virtual时就是动态联编,但我认为这并不是动态联编,而依然是静态联编。
去掉virtual的时候
博主的解释如下:
由于没有另外的数据结构来保存draw的地址,所以程序所知道的,必然只有在shape类中的draw地址了,仅仅用一个跳转指令。
这里我赞成博主的解释,将函数地址打印如下: 子类继承过来的circle::fun()完全就是父类的shape::fun(),连函数地址都一样
而在基类shape的fun中,根本就找不到子类的draw()的地址,因为子类的draw必然是定义在基类shape之后的,即使子类可以在基类之前声明。
有virtual的时候
先说结论,这个例子中用了virtual实现了fun调用到派生类circle::draw()
但是仍然是静态联编,不是动态联编。
因为这里是直接用对象去调用fun()进而调用draw()
依据是:
From 《C++PRIMER》:“通过对象进行的函数(虚函数或非虚函数)调用在编译时确定。对象的类型确定不变,无论如何都不可能令对象的动态类型与静态类型不一致。” From 《C++PRIMER》 “当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有这种情况下对象的动态类型才有可能和静态类型不同。”
这里之所以可以通过静态联编实现派生类的对象调用到派生类的draw()是因为声明oneshape的时候已经确切说明了它就是circle的对象,一点也不含糊,所以编译器直接调用的就是派生类的draw。
如果非要在例子2上写出动态绑定的方式:
例子3
#include <iostream>
using namespace std;
class shape {
public:
void virtual draw() { cout << "I am shape" << endl; }
void fun() { draw(); }
};
class circle :public shape {
public:
void draw() { cout << "I am circle" << endl; }
};
void main() {
circle a;
shape * b = &a;
shape & c = a;
b->draw();
c.draw();
}
解释3
这里面的b是基类类型的指针,去调用draw()函数时,编译器根据b指针指向的内存空间中的虚函数表指针找到相应的draw()函数。
换句话说,b的静态类型是基类shape,而由于指向的内存空间中的虚函数表是circle类的,所以b的动态类型是派生类circle,所以b调用的就是circle::draw()。
由此b完成了动态联编。
引用c也是同样的道理。
例子1和例子3的区别—向上类型转换(upcasting)
FROM 《C++编程思想》:在一个设计风格良好的OOP程序中,大多数甚至所有的函数都沿用tune()模型【博主:tune()就是例子1中的dynamicbinding()】,只与基类接口同行,这样的程序是可拓展的。因为可以通过从公共基类继承新数据类型而增加新功能。操作基类接口的函数完全不需要改变就可以适合于这些新类。
4.虚函数
虚函数的原理–虚函数表
如果一个类有虚函数,那么这个类的对象就有一个虚函数表指针
表中存放虚函数的入口地址且派生类继承这个虚函数表
若派生类重写/覆盖/修改了基类的基函数,编译器就会把虚函数表中的函数入口地址改为派生类中对应的虚函数入口地址。
在例子2和3中,利用Developer Command Prompt可以看到虚函数表。
有virtual的时候,shape的虚函数表如下: vfptr就是一个虚函数表指针 ,下面vftable就是虚函数表
其中0 | &shape::draw 表示有一个虚函数为shape::draw。
再来看circle的虚函数表: 子类circle首先继承了基类的虚函数表有了自己的虚函数表,就是外层的±–
然后将同样从基类继承的虚函数表指针指向自己的虚函数表
当编译器发现派生类circle重写了父类的虚函数,子类重写的函数就会覆盖掉虚函数表对应的父类的函数。
关于虚函数的规定
- 任何构造函数之外的非静态函数都可以是虚函数
- virtual只能出现在类内的函数声明,不能出现在类外的函数定义
- 所有虚函数都必须由定义,因为如果用了动态绑定,只有在运行时才知道会调用哪个虚函数。如果运行时调用到了一个没有被覆盖重写的虚函数,那么编译器会去自动调用继承层次中最近的定义。
- 如果基类把一个函数声明称虚函数则该函数在派生类中隐式地也为虚函数
(派生类中地虚函数可以显式地写出virtual不影响,但没必要,不方便阅读。)
5.注意区分【virtual覆盖基类函数】与【派生类隐藏基类同名函数】
- virtual只有参数和函数名均相同才可以覆盖(可以显式地加上override防止代码写错)。而后者不管参数是否相同,只要派生类中函数名与基类相同,那么就隐藏基类所有使用该名地函数,即包括所有重载函数。
- virtual覆盖之后可以动态联编更加灵活,后者隐藏基类同名函数之后,虽然仍然可以通过基类作用域来调用被隐藏的基类函数,灵活性没有提高。
6.隐藏(重定义)、覆盖、重载
隐藏的规则
- 派生类的函数与基类函数同名,但参数不同,无论有无virtual,基类同名函数都被隐藏-----区别于重载,重载发生在同类中。
- 派生类的函数和基类函数同名,参数相同,但没有virtual,此时基类同名函数被隐藏-----区别于覆盖,覆盖有virtual。
覆盖的条件
- 分别位于派生类和基类
- 函数名相同
- 参数相同
- 基类函数有virtual
重载的条件
- 同一类中
- 函数名相同
- 参数不同
- virtual可有可无
|