3.多态构成的条件
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,而且派生类必须对虚函数进行重写
class person { public: virtual void BuyTicket() { cout << "person::买票-全价" << endl; } }; class student : public person { public: virtual void BuyTicket() { cout << "student::买票-半价" << endl; } }; class soldier : public person { public: virtual void BuyTicket() { cout << "soldier::买票-优先" << endl; } }; void func(person& p) { p.BuyTicket(); } void Test2() { person pn; student st; soldier sr; func(pn); func(st); func(sr); }
可以看到,三个不同类对象调用同一个函数,最终执行的结果不同,这就是多态
当没有通过基类的指针或者引用调用时:不构成多态
当没有虚函数重写的时候:不构成多态
4.重载、重写(覆盖)、重定义(隐藏)的对比
重载:两个在同一作用域的函数,其函数名相同且参数不同,构成函数重载
重定义(隐藏):两个分别在基类和派生类中的同名函数,构成隐藏
重写(覆盖):两个分别在基类和派生类中的函数名、返回值、参数列表都相同的虚函数构成重写(协变例外)
3. 抽象类
在虚函数后面加上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class person { public: virtual void BuyTicket() = 0 { cout << "person::买票-全价" << endl; } }; class student : public person { public: virtual void BuyTicket(int a) { cout << "student::买票-半价" << endl; } }; void Test3() { person pn; }
接口继承与实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数
4. 多态的原理
首先,我们来看一道题
//(在x86环境下)这里sizeof(Base)是多少 class Base { public: virtual void func() { cout << "func" << endl; } private: int _b = 1; };
可以看到,结果是8个字节,这是为什么?一个Base对象中不是只有一个_b成员吗?我们来看一下监视窗口
可以发现,在一个Base对象中,除了_b成员之外,还有一个_vfptr(在VS2022平台下是放在前面的),看类型可知这是一个指针。这个指针我们叫他虚函数表指针(其中v表示virtual,f表示function)。一个含有虚函数的类至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表(也叫虚表)中。
现在我们来改写一下这个代码
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; }; void Test4() { Base b; Derive d; }
现在,我们通过监视窗口来观察一下b和d对象
有以下几点发现:
- 可以看到d对象继承了Base类中的成员,所以也就理所当然的继承了一个虚表指针
- 在代码中,我们让Func1完成了重写的过程,所以看到d对象中的虚表中的Func1是Drive中的Func1,也可以理解成覆盖,就是指虚表中的虚函数的覆盖。这里重写是语法层上的叫法,覆盖是原理层上的叫法。
- 由于在基类中,Func2也是虚函数,所以也放在虚表里面,但是在派生类中没有被重写,所以在b对象和d对象中虚表的Func2地址是相同的。
对上述现象的疑问与分析
✅虚函数表指针vfptr本质上是一个函数指针数组指针,这个指针指向了一个函数指针数组,这个数组的名字叫做虚函数表(虚表),表里面存放的是该类中的所有虚函数 ==> 类中的所有虚函数都会进入虚表
❓派生类中的虚表是怎么生成的?
✅首先,将基类中的虚表内容拷贝一份到派生类虚表中;然后,将派生类中所有的虚函数放进虚表中,如果发现其中有虚函数是重写基类中的,那就覆盖掉;最后,对于新增虚函数的顺序问题:按照在派生类中的声明顺序排列
✅在VS下测试,虚表以一个nullptr结尾。
❓虚表是存放在什么位置的?
✅我们不太清楚虚表存放的位置,那么现在用一个方法来测试一下,我们看下面一段代码:
void print() { Base b; cout << (void*)(*(int*)&b) << endl;//拿到b对象的地址,强转成int*,拿到前四个字节的地址,解引用就是虚表的地址 } void Test5() { int a = 10; int* b = new int(20); static int c = 30; const char* d = "aaaa"; cout << "栈:" << &a << endl; cout << "堆:" << b << endl; cout << "静态区:" << &c <<endl; cout << "代码段:" << (void*)d << endl; cout << "虚表地址"; print(); }
可以看到虚表地址和代码段是最接近的,所以虚表是存放在代码段的
在g++下测试也是在代码段。
所以多态的原理就是在在类中定义了虚函数,然后在运行时虚函数会进入虚表中,在构造对象的时候,会构造一个函数指针数组指针,用于存放虚表的地址,这里的虚表本质上是一个函数指针数组,数组内存放的就是虚函数的地址。如果在派生类中对虚函数进行了重写,那么派生类对象中的基类成员的虚表中对应的地址会被重写之后的虚函数地址覆盖。此时通过切片得到的派生类对象指针或引用与基类对象指针或引用调用虚函数时,就会通过虚表去找到需要调用的虚函数,从而就实现了调用同一个函数名,却产生了不同的结果的情况,即多态行为。
动态绑定和静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态