多态
多态,不同对象完成同一个行为时会产生不同的状态。
如对于Move行为,不同的动物对象具有不同的行为实现,狗run,鱼swim
定义及实现
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
虚函数
虚函数:被virtual关键字修饰的类成员函数
特性:如果一个函数在基类中被定义为虚函数,则它在所有的派生类中都是虚函数(不用加virtual)
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
虚函数重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
虚函数重写两个例外:
协变:基类与派生类虚函数返回值类型不同。
基类返回基类对象的指针和引用,派生类返回派生类对象的指针和引用
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
析构函数的重写:基类和派生类析构函数的函数名不同。
只要定义了基类virtual析构函数,只要派生类定义了析构函数,无论是否加virtual,都默认构成析构函数的重写,即使函数名不同。因为编译器对析构函数做了统一处理,编译后名称统一处理成destructor
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
多态的构成条件
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
overload overwrite override区别
overload重载
- 相同的范围(同一个类中)
- 函数名相同
- 参数不同
- virtual关键字可有可无
class A
{
public:
void show(int n){...}
void show(float n){...}
}
overwrite重写(覆盖)
- 不同的范围(分别位于基类和派生类)
- 函数名相同
- 参数相同
- 函数返回值相同(协变除外)
- 基类函数必须要virtual关键字
class A
{
public:
virtual void show(int n){...}
}
class B:public A
{
public:
void show(int n){...}
}
override重定义(隐藏)
- 不同的范围(分别位于基类和派生类)
- 函数名相同
- 两个基类与派生类的同名函数如果不构成重写,就是重定义
class A
{
public:
void show(){...}
}
class B:public A
{
public:
void show(int n){...}
}
多态的原理
动态绑定和静态绑定
静态绑定:在编译阶段确定了程序的行为,也称为静态多态,比如: 函数重载
动态绑定:在运行阶段确定了程序的行为,也称为动态多态
虚函数表
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
通过监视窗口,可以看到b对象中除了包含成员变量_b之外,还包含了__vfptr指针,它就是虚表指针,虚表指针指向一个虚函数表(简称虚表),包含虚函数的类头4个字节存放续表指针
虚表当中存储的就是虚函数的地址,因为父类当中的Func1和Func2都是虚函数,所以父类对象b的虚表当中存储的就是虚函数Func1和Func2的地址。
继承关系中:
子类Derive对父类Base的虚函数Func1进行了重写,因此虚表中Func1的地址发生了变化。虚函数Func2没有进行重写,直接继承了父类的,故虚表中Func2的地址未发生变化。这就是为什么虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数地址的覆盖,重写是语法的叫法,覆盖是原理层的叫法。
注:
虚表中只存放虚函数地址,若类中含有或继承了非virtual函数,则虚表中不会存放该函数地址。
虚表的生成步骤
a.拷贝:先将基类中的虚表内容拷贝一份到派生类虚表中
b.覆盖:如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.增补:派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
案例
来看一个买票的案例,
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket(); //调用p对象的BuyTicket函数
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
对象在编译过程中会初始化它的成员(重写的虚函数会覆盖它从父类继承下来的虚函数–>虚表中内容发生改变)
对象调用函数时,对象到它的虚表中查找调用的虚函数地址,而虚表内容已经是更新过的了,因此,当people指向Mike对象时,people.BuyTicket在Mike 的虚表中找到虚函数是People::BuyTicket,同理当people指向Johnson对象时,people.BuyTicket在Johnson的虚表中找到虚函数是Student::BuyTicket。这样就实现出了不同对象去完成同一个行为时,展现出不同的形态。
通过汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行
起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
多继承中虚函数表
验证方法——打印虚函数表
typedef void(*VFPTR) (); //虚函数指针
void PrintVTable(vector<VFPTR> v)
{
for(int i=0;v[i]!=nullptr;i++)
{
printf("第%d 个虚函数的地址是 %p",i,v[i]);
VFPTR f=v[i]; //保存当前虚函数指针
f(); //调用该虚函数
}
}
class Base1 {
public:
virtual void func1() {cout << "Base1::func1" << endl;}
virtual void func2() {cout << "Base1::func2" << endl;}
private:
int b1;
};
class Base2 {
public:
virtual void func1() {cout << "Base2::func1" << endl;}
virtual void func2() {cout << "Base2::func2" << endl;}
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() {cout << "Derive::func1" << endl;}
virtual void func3() {cout << "Derive::func3" << endl;}
private:
int d1;
};
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d); //第一个虚表指针,在对象的前4个字节
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
//第二个虚表指针,在对象距离第一个虚表指针(Base成员大小)偏移量的位置
PrintVTable(vTableb2);
return 0;
}
结论:
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中