多态的作用
封装可以使得代码模块化,继承可以扩展已存在的代码,目的都是为了代码重用。而多态的目的则是为了接口重用。静态多态,根据传入不同的参数(个数或类型不同)调用不同的实现。动态多态,则不论传递过来的哪个类的对象,函数都能够通过同一个接口调用到各自对象实现的方法。
多态分类
C++中的多态性具体体现在编译和运行两个阶段。编译时多态是静态多态,在编译时就可以确定使用的接口。运行时多态是动态多态,具体引用的接口在运行时才能确定。
静态多态
静态多态往往通过函数重载和模版(泛型编程)来实现,具体可见下面代码:
#include <iostream>
using namespace std;
//两个函数构成重载
int add(int a, int b)
{
cout<<"in add_int_int()"<<endl;
return a + b;
}
double add(double a, double b)
{
cout<<"in add_double_doube()"<<endl;
return a + b;
}
//函数模板(泛型编程)
template <typename T>
T add(T a, T b)
{
cout<<"in func tempalte"<<endl;
return a + b;
}
int main()
{
cout<<add(1,1)<<endl; //调用int add(int a, int b)
cout<<add(1.1,1.1)<<endl; //调用double add(double a, double b)
cout<<add<char>('A',' ')<<endl; //调用模板函数,输出小写字母a
}
程序输出结果:
in add_int_int()
2
in add_double_doube()
2.2
in func tempalte
a
动态多态
动态多态是通过“继承+虚函数”来实现的,在使用基类指针或引用指向子类对象时,编译器在构造函数里将虚表指针指向子类的虚表,最终调用的是子类的函数,这样就实现了运行时函数地址的动态绑定。
Base *p = new Derived();
在构造时,子类对象的虚指针指向的是子类的虚表,接着由Derived到Base的转换并没有改变虚表指针,所以这时候p->VirtualFunction,实际上是p->vfptr->VirtualFunction,它在构造的时候就已经指向了子类VirtualFunction,所以调用的是子类的虚函数
虚函数表和虚函数指针
- 虚函数表是一个指针数组,保存了本类的虚函数地址。编译器为每个类的对象提供一个虚表指针,这个指针指向所属类的虚函数表(包含虚函数的对象会增加一个虚表指针的大小)。
- 在构造函数中进行虚表的创建和虚表指针的初始化。在构造子类对象时,要先调用父类的构造函数(此时编译器只“看到了”父类,并不知道后面是否后还有继承者),它初始化父类对象的虚表指针,该虚表指针指向父类的虚表。当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。
- 如果子类没有重写虚函数,子类虚表中仍然会有该函数的地址,指向的是基类的虚函数实现;如果重写了虚函数会指向自身虚函数的实现;如果子类有自己的虚函数,那么虚表中就会添加该项。
虚函数和纯虚函数
- 虚函数是为了多态特性引入的;纯虚函数是为了实现一个接口,起到一个规范的作用,实现纯虚函数的方法是在函数原型后加=0。带有纯虚函数的类是抽象类,只有纯虚函数的类是接口。
- 虚函数在基类中是实现的,在子类中可以重写也可以不重写;纯虚函数在基类中不实现,需要在子类中实现。
其他
- 虚函数表是属于类的,位于内存模型中的常量区,所有该类的实例化对象中都会有一个虚函数表指针去指向该类的虚函数表。而虚函数则位于内存模型中的代码区。
- 内联函数不能为虚函数,内联函数需要在编译阶段展开,而虚函数是运行时动态绑定的,编译时无法展开;
- 构造函数不能为虚函数,虚函数是在运行时确定类型的,而构造一个对象时由于对象还未构造成功无法知道其实际类型;但是构造函数中可以调用虚函数,不过并没有动态效果,只会调用本类中的对应函数;
- 静态成员函数不能为虚函数:静态成员函数是以类为单位的函数,与具体对象无关,虚函数是与对象动态绑定的。
- 析构函数一般写成虚函数,避免子类析构函数不被调用导致内存泄漏