前言
多态又分为静态多态和动态多态:
(1)静态多态:函数重载和函数模板实例化出多个函数(本质也是函数重载)即静态多态也称为编译期间的多态。
(2)动态多态,也称为动态绑定或后期绑定(晚绑定):运行时的多态性可通过虚函数实现。
此文章只进行动态多态的讲解
一、什么是多态?
简单点来说,多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果。
多态的产生必须具备两个条件:
-
必须通过基类的指针或者引用调用虚函数。
-
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
二、举个例子
class A {
public:
A() {
}
virtual void print(int num) {
cout << "num:---" << num<< endl;
}
}
class B :public A{
public:
B(int cb) :n_(cb) {
}
void print(int num) {
int re = num * n_;
cout << "num:+++++++" << re << endl;
}
private:
int n_;
};
int main(){
A *temp = new B(2);
temp->print(3);
delete temp;
return 0;
}
子类B继承了基类A,并且重写了基类A中虚函数print;在main函数中,A的指针指向了B的对象,这便满足了多态的条件。所以打印出来的结果应该是B中print函数打印的结果。
三、虚函数表
为何会产生多态这种现象,原因来自虚函数表。
首先我们先看一下classA的内部结构
class A {
public:
A() {
}
virtual void print(int num) {
cout << "num:---" << num<< endl;
}
}
我们知道,一个空类在内存中的大小为1,所以如果去掉virtual修饰,A的大小其实为1。这里我们可以通过VS自带的Developer Command Prompt去验证。
但如果重新加上virtual修饰后,A的大小就变为了4,并且还多了一个vftable。
这是因为,当一个类的非静态成员函数前添加virtual之后,该类会产生一个指针vfptr(virtual function pointer),即虚函数指针(4个字节)去指向一个vftable(virtual function table) 虚函数表,虚函数表中保存了该成员函数的地址&A::print。
那如果子类B继承一下基类A会产生什么结果?我们同样使用Developer Command Prompt去查看一下。
首先我们只继承,不去重写基类的虚函数。
class B :public A{
public:
B(int cb) :n_(cb) {
}
//virtual void print(int num) {
// int re = num * n_;
// cout << "num:+++++++" << re << endl;
//}
private:
int n_;
};
此时我们注意到,B的大小是8,即继承的A的虚函数指针,还有B自己的int n_,但此时B中虚函数表中存放的地址却是继承过来的&A::print。
然后我们去重写基类虚函数函数
class B :public A{
public:
B(int cb) :n_(cb) {
}
virtual void print(int num) {
int re = num * n_;
cout << "num:+++++++" << re << endl;
}
private:
int n_;
};
此时我们发现B中虚函数表中的&A::print被覆盖为&B::print。
当满足多态条件以后,父类的指针或引用调用虚函数时,在运行时到指向的对象中的虚表中去找对应的虚函数调用,引用的底层也是由指针实现,父类在指向子类时会发生切片。所以指针指向父类的对象,调用的就是父类的虚函数,指向的是子类对象,调用的就是子类的虚函数。
四、一些小知识
在进行继承时,子类的虚函数不加virtual关键字也可以构成重写,因为继承后基类的虚函数被继承下来了,在派生类中依旧保持虚函数属性。但不建议这样写,因为子类也有可能被继承,所以我们在重写时就老老实实写上罢。
虚函数要求返回值类型相同、函数名相同以及参数列表完全相同,但是协变是个例外,子类重写基类虚函数时,与基类虚函数返回值类型可以不同。但是返回值类型也必须满足父子关系。
总结
本人第一次写文章,难免有疏漏,如果你觉得有用,那你就觉得有用,如果你觉得我写的不好,我看见了会改的,谢谢铁子们。