一、多态简介
C++中的多态(Polymorphism)是指同一个函数或方法能够在不同的对象上产生不同的行为。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++多态意味着调用成员函数时,会根据调用函数对象的类型来执行不同的函数。
- 静态多态(Static Polymorphism):通常也称为函数重载(Function Overload),是指在同一个作用域内定义了多个同名函数,但它们的参数类型或个数不同,从而可以根据调用时传入的参数类型或个数来选择正确的函数进行调用。静态多态的实现方式是在编译时进行类型检查和函数匹配,因此也被称为编译时多态。C++中的静态多态通过函数重载和运算符重载来实现。模板属于静态多态,在编译期间就能确定的多态。
- 动态多态(Dynamic Polymorphism):通常也称为运行时多态(Runtime Polymorphism),是指同一个函数或方法能够在不同的对象上产生不同的行为,实现方式是通过虚函数(Virtual Function)来实现。动态多态的实现方式是在运行时根据对象的类型来确定调用哪个函数,从而实现不同的行为。C++中的动态多态通过虚函数和基类指针/引用来实现。虚函数是C++中实现动态多态的重要机制,通过在基类中声明虚函数,在派生类中重写该函数,可以使不同的派生类对象调用同一函数时表现出不同的行为。简而言之:父类指针或引用指向子类对象,通过指针或引用调用子类重写的虚函数,在运行期间才能确定具体调用哪个函数,是动态多态。
- 启动动态多态的条件:有继承关系,子类重写父类虚函数并且父类指针调用子类重写的虚函数。
#include<iostream>
using namespace std;
/*
重写:子类和父类中,同名且同形参的虚函数能够发生重写,子类重写父类的虚函数(virtual)
*/
class A
{
public:
int a;
int b;
int c;
const int d = 1;
virtual void work()
{
cout << "A work()" << endl;
}
void fun()
{
cout << "A fun()" << endl;
}
};
class B :public A
{
public:
virtual void work()
{
cout << "B 重写了父类A的work函数" << endl;
}
void fun()
{
cout << "B fun()" << endl;
}
};
class C :public B
{
public:
void work() //是虚函数
{
cout << "C 重写了父类B的work函数" << endl;
}
void fun()
{
cout << "C fun" << endl;
}
};
int main()
{
//多态 父类指针指向子类对象
//父类指针指向子类对象,去调用虚函数的时候会调用被重写的虚函数
A *a = new C();
a->fun(); //函数隐藏:只需要函数名字(fun)相同即可,会去调用指针类型的函数
a->work();
return 0;
}
二、多态的实现
为了实现 C++ 的多态,C++ 使用了一种动态绑定的技术。这个技术的核心是虚函数表。下面介绍虚函数表是如何实现动态绑定的。
2.1 类的虚函数表
-
每个包含了虚函数的类都包含一个虚表(存放虚函数指针的数组)。
-
当一个类(B)继承另一个类(A)时,类 B 会继承类 A 的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。
-
以下的代码。类 A 包含虚函数vfunc1,vfunc2,由于类 A 包含虚函数,故类 A 拥有一个虚表。
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B : public A{//此时类B也拥有自己的虚表
};
类A的虚表如图所示:
图1 类A的虚表示意图
-
虚表是一个存放指针的数组,其内的元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是:普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。
-
虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了
2.2 虚表指针
-
虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。
-
为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
-
验证_vptr指针的方法(_vptr指针不可访问),就是先求一个普通类占的字节数大小sizeof(),然后将类中某一个函数(方法)前加virtual关键字变为虚函数,再求该类占的字节数大小sizeof(),会发现增加4个字节,这就验证了vptr的存在。
图2 对象与它的虚表
2.3 动态绑定
动态联编(动态绑定)是指编译程序在编译阶段并不能确切地知道将要调用的函数,只有在程序执行时才能确定将要调用的函数,为此要确切地知道将要调用的函数,要求联编工作在程序运行时进行,这种在程序运行时进行的联编工作被称为动态联编。C++规定:动态联编是在虚函数的支持下实现的。
动态联编必须包括以下方面:
- 成员函数必须声明为virtual
- 如果基类中声明了为虚函数,则派生类中不必再声明。
调用方式:
通过对象的指针或引用调用成员函数,或通过成员函数调用,反之就无法实现动态联编
特点:
灵活,问题抽象性和问题的易维护性。
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B : public A {
public:
virtual void vfunc1();
void func1();
private:
int m_data3;
};
class C: public B {
public:
virtual void vfunc2();
void func2();
private:
int m_data1, m_data4;
};
图3 类A、类B、类C的对象模型
-
由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类 A 的虚表(A vtbl),类 B 的虚表(B vtbl),类 C 的虚表(C vtbl)。类 A,类 B,类 C 的对象都拥有一个虚表指针,*__vptr,用来指向自己所属类的虚表。
-
类 A 包括两个虚函数,故 A vtbl 包含两个指针,分别指向A::vfunc1()和A::vfunc2()。
-
类 B 继承于类 A,故类 B 可以调用类 A 的函数,但由于类 B 重写了B::vfunc1()函数,故 B vtbl 的两个指针分别指向B::vfunc1()和A::vfunc2()。
-
类 C 继承于类 B,故类 C 可以调用类 B 的函数,但由于类 C 重写了C::vfunc2()函数,故 C vtbl 的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()。
-
虽然图 3 看起来有点复杂,但是只要抓住“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数”这个特点,便可以快速将这几个类的对象模型在自己的脑海中描绘出来。
-
非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。
假设我们定义一个类 B 的对象。由于 bObject是类 B 的一个对象,故bObject包含一个虚表指针,指向类 B 的虚表。
int main()
{
B bObject;
}
现在,我们声明一个类 A 的指针p来指向对象bObject。虽然p是基类的指针只能指向基类的部分,但是虚表指针亦属于基类部分,所以p可以访问到对象bObject的虚表指针。bObject的虚表指针指向类 B 的虚表,所以p可以访问到 B vtbl。如图 3 所示。
int main() {
B bObject;
A *p = & bObject; // 父类指针指向子类对象
}
当我们使用p来调用vfunc1()函数时,会发生什么现象?
int main()
{
B bObject;
A *p = & bObject;
p->vfunc1();
}
-
程序在执行p->vfunc1()时,会发现p是个指针,且调用的函数是虚函数,接下来便会进行以下的步骤。
-
首先,根据虚表指针p->vptr来访问对象bObject对应的虚表。虽然指针p是基类A类型,但是vptr也是基类的一部分,所以可以通过p->__vptr可以访问到对象对应的虚表。
-
然后,在虚表中查找所调用的函数对应的条目。由于虚表在编译阶段就可以构造出来了,所以可以根据所调用的函数定位到虚表中的对应条目。对于 p->vfunc1()的调用,B vtbl 的第一项即是vfunc1对应的条目。
-
最后,根据虚表中找到的函数指针,调用函数。从图 3 可以看到,B vtbl 的第一项指向B::vfunc1(),所以 p->vfunc1()实质会调用B::vfunc1() 函数。
#include<iostream>
using namespace std;
// 父类指针指向子类对象
class A
{
public:
virtual void vfun1()
{
cout << "father eat" << endl;
}
};
class B :public A
{
public:
virtual void vfun1() //在继承关系下, vfun1前面不写virtual,它也是虚函数
{
cout << "child eat" << endl;
}
};
int main()
{
A* a = new B();
a->vfun1();
}