多态的定义和实现
概念
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
多态的条件:
1、派生类要是实现对基类的虚函数的重写 —— 三同(函数名、参数、返回值)
2、父类指针或者引用去调用虚函数
如下, 在 Func 函数中,是父类引用去调用 BuyTicket() ,但是由于父类 BuyTicket() 并不是虚函数,所以, Func() 函数传入的参数是子类对象时,调用 Func() 函数并不会实现多态,因为虽然满足了条件2,但是并没有实现条件1(父类里面的 BuyTicket() 并不是虚函数)。
如下,如果是这样子的,那么就是实现了多态。首先,子类里面重写了父类中的 虚函数 BuyTicket(子类可以不用加 virtual );其次, Func 的参数是父类引用,传入子类的对象时,调用 BuyTicket 就会实现多态。
例外
但是,多态实现条件也有一个小小的例外,那就是——协变。基类与派生类虚函数返回值类型不同,这就是协变。
如下代码,子类 Student 里面的 f() 虽然是对父类里面虚函数 f() 的重写,但是其返回值是不同的,这样也可以满足多态的第一个条件。
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;
}
};
final 和 override
final 修饰一个虚函数,表明该虚函数不可以被重写。
如下,final 修饰了基类的虚函数 Drive() ,此时派生类依然对该函数进行了重写,编译时会报错。
class Car {
public:
virtual void Drive() final {
}
};
class Benz :public Car {
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
如下。
class Car {
public:
virtual void Drive() {
}
};
class Benz :public Car {
public:
virtual void Drive() override
{
cout << "Benz-舒适" << endl;
}
};
重载、覆盖(重写)、隐藏(重定义)
如下,三个概念进行区分,比较容易混淆。
多态实现的原理
虚函数表
如下代码,求一求 sizeof(Base) 是多少?答案是 8 !这是因为, Base 类里面右虚函数 Func1,那么就会产生虚表,虚表里面存储的是指向本类的虚函数的指针。
每一个实例化出的对象,需要找到虚表,才可以调用虚函数。实际上,会在每一个对象里面存储一个指针,指向虚表,而 32 位电脑的指针大小,就是 4 字节,加上一个 int 类型的数据,所以是 8 个字节!!
class Base {
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
如下,通过查看监视窗口,可以知道对象 bb 里面除了_b 成员,还多一个__vfptr 放在对象的前面(有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表指针也简称虚表指针。虚表指针指向一个函数指针数组,数组里面每一个元素都是指针,指向本类的虚函数。
当调用一个类里面的虚函数时,就会先根据虚表指针,找到虚表,然后根据虚表里面的指针,来调用对应的虚函数。
那么,派生类的虚表中存放的是什么呢?
我们对上面的代码进行以下改造,再进行监视查看派生类的虚表变化。
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
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;
}
如下,派生类对象 d 中,其基类对象 Base 的虚表里面有两个函数指针,分别指向 Derive 类的 Func1() 、 Base 类的 Func2() (监视窗口时 VScode 修饰过的,实际内存并不会将这些展示出来)。在派生类中,将 Func1() 这个虚函数进行了重写,但是并没有重写 Func2() 。
派生类对象 d 中也有一个虚表指针,d 对象由两部分构成,一部分是父类继承下来的成员(派生类的虚表指针也就是存在该部分),另一部分是自己的成员。
由此可以简单推断:对于 派生类的虚表 ,如果派生类重写了基类的虚函数,那么虚表里面的函数指针,指向的是派生类的虚函数;派生类没有重写的虚函数,虚表里的指针 指向的是基类的虚函数。
总结一下派生类虚表的形成:
- 先将基类中的虚表内容拷贝一份到派生类虚表中。
- 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 。
- 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
那么虚函数存在哪儿?虚表又存在哪儿?很多人会以为虚函数存在虚表中,虚表存在对象中。
实际上这是错的。但是很多人都是这样认为的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针!!
多态的原理
上面的内容,是为了理解多态的原理做准备!
如下代码, Func 函数传 Person 调用的 Person::BuyTicket,传Student调用的Student::BuyTicke 。
class Person {
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
运行时的调用过程如下:
- 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚
函数是Person::BuyTicket。 - 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中
找到虚函数是Student::BuyTicket。 - 这样就实现了不同对象去完成同一行为时,展现出不同的形态。
- 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调
用虚函数。
这,就是多态的实现原理!!对于基类对象而言,毫无疑问调用的是自己的虚函数。但是对于派生类对象而言,派生类对象的虚表已经改变(如果进行了重写的话),虚函数指针指向本类中固定虚函数,那么通过父类指针/引用 去调用虚函数的时候,就会先找到派生类的虚表,然后根据虚表里面的函数指针调用函数,由于派生类的虚表里是自己的虚函数指针,所以就实现了多态!!
- 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行
起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
void Func(Person* p)
{
p->BuyTicket();
}
int main()
{
Person mike;
Func(&mike);
mike.BuyTicket();
return 0;
}
// 以下汇编代码中跟这个问题不相关的都被去掉了
void Func(Person* p)
{
...
p->BuyTicket();
// p中存的是mike对象的指针,将p移动到eax中
001940DE mov eax,dword ptr [p]
// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
001940E1 mov edx,dword ptr [eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE mov eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的。
001940EA call eax
00头1940EC cmp esi,esp
}
int main()
{
...
// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址
mike.BuyTicket();
00195182 lea ecx,[mike]
00195185 call Person::BuyTicket (01914F6h)
...
}
动静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
多继承关系的虚函数
上面已经看过了单继承关系的虚函数表分布情况。所以这里就主要介绍多继承关系的虚函数表分布情况。
如下代码,是多继承关系。
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;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i) {
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。
文章评论