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