引言
OOP的核心思想是多态性。多态性这个词源自希腊语,其含义是“多种形式”。我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的“多种形式”而无须在意它们的差异。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。
——《C++11 Primer》
在C++语言中,当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。
——《C++11 Primer》
上述内容表明,C++中多态的原理与动态绑定联系十分密切
静态绑定&动态绑定
静态绑定: 编译时确定函数地址
class Derive
{
public:
void func()
{
cout << "Derive::func()";
}
};
int main()
{
Derive d;
d.func();
return 0;
}
上述代码产生的汇编代码如图所示:
因为其在编译过程中就确定好了func()的地址,所以在运行时直接采用call指令的方式调转到func()的地址,这就是常见的静态绑定。
动态绑定: 运行时确定函数地址
class Base
{
public:
virtual void func()
{
cout << "Base::func()";
}
};
class Derive : public Base
{
public:
virtual void func()
{
cout << "Derive::func()";
}
};
int main()
{
Base* d=new Derive;
d->func();
return 0;
}
上述代码产生的汇编代码如图所示:
这里的call指令并没有直接给出要跳转的地址,因为他不知道是调用Derive,还是Base的,所以在运行时就要根据提供的指针(存放到eax中),来确定应该跳转的具体地址,这就是动态绑定。
而具体如何进行这一动态绑定(如何确定是谁提供的指针),其核心就是虚表(虚函数表)
虚表(虚函数表)
先通过一组对比图查看现象
class Base
{
public:
//virtual void func()
//void func()
{
cout << "Base::func()" << endl;
}
int _a = 0;
};
int main()
{
Base d;
return 0;
}
上述代码中,分别采用void func()和virtual void func()查看现象,其存储模型如下:
其中发现:
对于有虚函数的类,该类的对象的存储内容只有其成员变量(int _a)
对于有虚函数的类,该类的对象存储时,会额外存储一个指针(虚表指针),而该指针指向的内容就是其对应的虚表,这个虚表中的每行都是一个对应函数地址。
多态原理
原理1:虚表
如前可知,如果一个类中有虚函数,则这个类在创建的时候就会初始化一个虚表,该虚表存放的内容就是该类中所有虚函数的地址
同样,如果该类是派生类,继承了基类的虚函数,那么这个派生类也会初始化一个自己的虚表
class Base
{
public:
virtual void func()
{
cout << "Base::func()" << endl;
}
int _a = 0;
};
class Derive : public Base
{
public:
int _b = 1;
};
int main()
{
Base d;
Derive b;
return 0;
}
上图中:
派生类中没有重写基类的虚函数,所以他们各自的虚表中func()函数的地址是一样的,说明他们调用同一个func()函数。
如果派生类中重写基类的虚函数,则会出现以下状况
此时,他们虚表中的func()函数就不是同一个func()函数了,此时对于不同的对象,指针或引用调用时,可能就会调用到不同的func()函数
原理2:赋值兼容/切割/切片
有了如上的知识铺垫,那么剩下最后一步就可以实现多态
C++中,派生类对象赋值给基类对象时会发生赋值兼容/切割/切片,这就导致了派生类会将自己存储空间里面的基类的那一部分拿出来
被拿出来的这一部分,含有虚表指针、基类的成员变量
如果派生类重写了基类的虚函数,所以这一部分的虚表中存放的就是重写后的函数的地址。如此就完成了多态。
注意: 如前所说,当我们使用基类的引用或指针指向一个派生类对象时,调用他的虚函数才会执行动态绑定。所以单独进行赋值操作时,不会实现动态绑定
Derive b;
Base d=b;//不会产生动态绑定
Base *d=&b;//会产生动态绑定
Base &d=b;//会产生动态绑定