多态的概念:
不同的对象完成某个行为会产生不同的形态,比如我们在买票的情况,会有身高几米以下半价、学生半价等等这种特殊情况。
多态体现在继承体系中,子类继承父类,父类对象和子类对象调用相同的函数,但结果会是不一样,举个例子:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
int main()
{
Person p;
Student s;
p.BuyTicket();
s.BuyTicket();
return 0;
}
输出结果为:
买票-全价
买票-半价
其中,我们发现,类里面的函数实现,前面有个virtual,之前我们用过这个关键字事用来继承父类,这次放在函数前面是用来表示这个函数是虚函数,虚函数被继承后,子类里面如果有个函数原型相同的函数,那么就称为重写(注意,是函数原型,是指函数返回值,函数名,参数列表都相同),如果只是函数名相同,那么就构成重定义。
虚函数还有两个例外:
1、协变
协变就是父类虚函数的返回值是父类指针或引用,子类的虚函数返回值是子类的指针或引用,协变也构成多态。
2、析构函数的重写
父类和子类的析构函数函数名不相同,编译器对析构函数的名称做了特殊处理,在编译阶段会把析构函数函数名统一变成destructor,另外,只有子类重写了父类的析构函数才构成多态,这样才能正确的调用析构函数(在子类析构函数中,先析构子类对象中不属于父类的那一部分,然后自动调用父类的析构函数析构父类的那一部分)。
构成多态需要两个条件,其中一个条件就是被调用的函数必须是虚函数,且子类的虚函数必须对父类的虚函数进行重写。
那还有一个条件呢?
虚函数的参数必须是父类的指针或引用,如果用父类的对象作参数,那么当一个子类对象传过去,先进行拷贝,把子类对象中父类的那一部分拷贝过去,然后再调用函数,此时,调用的函数就一定是父类的虚函数,调用不了子类的虚函数。
对于虚函数还有一点补充:如果父类有虚函数,子类继承父类,重写的虚函数前面加不加virtual无所谓,但是最好还是加上。
虚函数的原理:
先看一段代码:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
/*void Func3()
{
cout << "Base::Func3()" << endl;
}*/
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
调试前在return 0的位置打上断点,然后按F5直接执行到断点处,观察调试面板
我们发现,在父类和子类对象里,除了一个成员变量,还有一个指针,这个指针就是虚函数表指针,简称虚表指针,这个指针指向的空间是一个表,这个表叫做虚函数表,简称虚表,虚表里面存的是虚函数的指针。
我们可以看到,子类只重写了父类的Func1函数,Func2函数没有重写,所以,在两个对象的虚表中,Func2函数指针是同一块空间。
虚函数表本质是以数组的形式存储,vs系列的编译器一般会在最后加一个nullptr,g++不会。
总结:编译器会先把父类的虚表拷贝一份给子类,子类如果有重写父类的虚函数,那么就用自己重写的虚函数指针覆盖子类虚表中原有的。
另外,编译器在执行调用虚函数这个命令时,先是到指针或引用所指向的对象空间里面找虚表,从而拿到要调用的虚函数指针,再通过虚函数指针找到函数最后调用。