c++是一门面向对象的语言,具有继承、封装和多态三大特性,本文我们总结一下c++中多态的特性及其实现方法。多态指的是调用同一接口,表现出不同的特性,分为静态多态和动态多态。静态多态指在编译期就能确定其特性的多态,而动态多态需要在运行时确定其特性。
静态多态
静态多态包括重载和模板两种方式。c++重载是指函数在调用时,即使调用的函数名相同,也可以根据参数列表来确定其调用对象,比如下面的代码中有四个函数,main()函数中的调用,根据参数列表的不同,可以找到对应的函数入口。
//函数1
void foo(){}
//函数2
void foo(int x){}
//函数3
void foo(int x, float y){};
//函数4
void foo(float x, int y){};
int main(){
foo(); //调用函数1
foo(3); //调用函数2
foo(3, 5.7); //调用函数3
foo(3.8, 5); //调用函数4
}
需要说明的是,重载仅能识别参数列表不同的情况,这里也包括列表中参数类型的顺序不同,但不包括返回值的不同,比如下面的两个函数,编译器无法确定应该调用哪个函数,会出现如下图的报错。
void foo(float x, int y){};
int foo(float x, int y){return 0;};
c++支持重载是因为c++程序在编译后,这些原本同名的函数被编译成了不同名的函数,而相应的调用也修改为了新的函数名,因此重载在编译期就确定了其特性。
c++的模板元编程也是静态多态的一种,这在stl中有着大量应用,通过传入模板元来实现相同函数的不同处理。
动态多态
c++的动态多态主要通过虚函数和虚表来实现。所有包含需函数的类,在类的头部都有一个虚函数表指针,这个指针指向一个虚函数表,虚函数表中存储的是虚函数指针,通过虚函数指针,程序就可以调用相应的函数,如下图描述了下面代码的内存布局。
#include <iostream>
using namespace std;
class Base
{
virtual void foo(){cout<<"Base::foo"<<endl;}
};
typedef void(*Fun)(void);
int main()
{
Base d;
cout << "虚函数表指针地址:" << (long long*)(&d) << endl;
cout << "虚函数指针地址:" << (long long*)*(long long*)(&d) << endl;
Fun pFun = (Fun)*((long long*)*(long long*)(&d));
pFun();
return 0;
}
上面的代码用于验证图中虚函数的内存布局,下面是代码运行的输出结果。
可以看到通过两次指针的消耗,应用程序成功调用了foo()函数,那么虚函数又是怎样实现多态的呢?
#include <iostream>
using namespace std;
class base
{
public:
virtual void foo(){cout<<"Base::foo"<<endl;}
};
class derive:public base{
public:
void foo(){cout<<"derive::foo"<<endl;}
};
int main()
{
base* p = new derive();
p->foo();
return 0;
}
上面代码的输出结果是"derive::foo",这是因为如果虚函数是子类继承自父类的虚函数,那么会对应覆盖虚表中的虚函数指针,如果是子类的虚函数,则在虚表中新增一个虚函数指针。因此在子类中重写foo成员方法会将虚表中父类的foo方法覆盖,这时即使我们通过base类实例调用,虚表中的函数也已经被重写。需要说明的是,虽然虚函数需要在运行时才确定调用的函数,虚表却早在编译阶段就已经生成。
在多继承,菱形继承等复杂情况下,内存模型还会有更复杂的变化,我们将在后续文章中继续讨论。