简介
多态可以分为静态和动态。
静态多态是指当调用名字相同的函数时,编译器在编译期就能根据函数签名的不同推导出所调用的函数。
注释:函数签名包括了函数名,参数列表,类名,命名空间,const关键字;
其实现也称为函数重载(overload),将类似功能的函数统一命名,增强代码的可读性。
动态多态一般体现在基类的指针可以指向一个派生类的对象,当该指针在运行期调用某一个虚函数时,根据所指向的对象类型的不同而调用相应的虚函数(《c++ primier》 pp.536 “被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个”)。
当利用设计模式来解决问题时,会利用动态多态来实现,如达到“对扩展开放,对修改封闭”的目标。
下面将主要介绍动态多态的几个方面:虚函数表,虚函数表指针,动态绑定。
虚函数表指针和虚函数表
多态的实现是基于虚函数表指针和虚函数表。
举例来说:
基类有两个虚函数vf1和vf2,在编译时编译器会将这两个虚函数的地址放在基类的虚函数表(vtbl)内,供该类型的对象公用。然后编译器给基类增加一个成员变量vptr,即虚函数表指针,然后在基类的构造函数中增加一句赋值的命令,当实例化对象时,通过调用构造函数令vptr指向vtbl。
派生类由于继承自该基类,编译器判断基类有虚函数,那么派生类也会有一个vptr成员变量指向派生类自己的vtbl。如果派生类没有重写基类的虚函数,则其vtbl中的虚函数地址同基类的vtbl中虚函数的地址。
当派生类重写了某个虚函数时,派生类的vtbl中相应的函数地址就会更新为所重写的虚函数的地址。
另外,某个类的虚函数表是该类所共有的,因此会放在数据段,而类对象的虚函数表指针作为成员变量存在。
上述文字可以用下图表示(来自侯捷老师的视频):
另外,如果在派生类里面又定义了一个虚函数vf3,而vf3不在基类中,那么指向基类的指针并不会调用vf3,因为其不在基类的继承体系中。
动态绑定
要理解动态绑定,自然要理解静态绑定。
静态绑定是指编译器在编译阶段就能确定函数的地址,因此函数调用的汇编代码是call func_address(如图中的14行,CALL直接绑定了Base的print()函数)。
而由于要实现多态,那么在编译阶段,编译器是不知道所调用的函数的地址的,那么需要通过虚函数表指针和虚函数表来确定函数地址(如上图的21-27行,CALL的地址是经过偏移的),这个过程就是动态绑定。
动态绑定需要满足三个条件:
- 通过指针调用函数
Base* p = new Derived();
- 指针指向的对象必须支持向上转型(up_casting),即指针是基类的类型,指向派生类的对象
- 所调用的函数是虚函数
p->vfprint()
此时,编译后的汇编代码不再指定具体的函数地址,而是先指向派生类的vptr,然后根据函数名来读取vtbl中对应的函数地址
可以将虚函数调用过程用代码简化表示为
(*p->vptr[n])(p);
(*(p->vptr)[n])(p);
即p指向了派生类对象的虚函数表,然后通过定位找到对应的虚函数。注意括号内的p相当于传入函数的this。
根据测试,虚函数的地址是有序的,可以从vfprint1()和vfprint2()的汇编代码看出,调用vfprint2()时地址add了8个字节(这里用的64位)
应用场景
假设我们要画不同的形状,可以将基类的draw设置为纯虚函数,然后派生类去各自实现。
class Shape
{
public:
Shape() = default;
virtual ~Shape() = default;
public:
virtual void draw() = 0;
};
class Rectangle : public Shape
{
public:
Rectangle() = default;
virtual ~Rectangle() = default;
public:
virtual void draw() override { std::cout << "draw a rectangle" << std::endl; };
};
class Circle : public Shape
{
public:
Circle() = default;
virtual ~Circle() = default;
public:
virtual void draw() override { std::cout << "draw a circle" << std::endl; };
};
除了矩形和圆形之外,我们可能会在后续增加各种各样的形状,同时我们绝对不想改变画出所有图形的代码。那么在调用时可以写成下面,这样for循环中的代码是不会变的,只要将不同的图形加入到容器中就行。
int main()
{
std::vector<Shape *> shapes;
shapes.push_back(new Rectangle);
shapes.push_back(new Circle);
for (const auto &s : shapes)
{
s->draw();
delete s;
}
}