C++中的多态
一、多态的概念
多态的概念:去完成某个行为,当不同的对象去完成时会产生出不同的状态。(通俗来说,就是多种形态)
多态的优点优点:
- 灵活性:多态性允许同一个接口用于不同的对象,从而使得代码更加灵活。
- 可扩展性:可以在不修改现有代码的情况下,通过添加新的类来扩展程序的功能。
- 代码重用:通过多态性,可以编写更加通用和可重用的代码。
多态性是面向对象编程中的一个重要特性,它允许对象以多种形式出现,从而使得代码更加灵活和可扩展。通过编译时多态(如函数重载和运算符重载)和运行时多态(如虚函数和接口),可以实现不同的多态性行为。
二、多态的定义及实现
虚函数:即被virtual修饰的类成员函数称为虚函数。
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)(三同),称子类的虚函数重写了基类的虚函数。
构成多态的两个条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
多态调用看的是指向的对象、不同对象传递过去,调用不同函数
虚函数重写的一些细节:
重写的条件本来是虚函数+三同,但是有一些例外
- 派生类的重写虚函数可以不加virtual--(建议加上)

- 协变(基类与派生类虚函数返回值类型不同),返回的值可以不同,但是要求返回值必须是父子关系指针或引用

析构函数的重写(基类与派生类析构函数的名字不同):
析构函数加virtual是虚函数重写,因为析构函数都被处理成destructor这个统一的名字
这样写会造成内存泄漏
正确写法
派生类的重写虚函数可以不加virtual
C++11 override 和 final:
final:修饰虚函数,表示该虚函数不能再被重写
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
三、抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
抽象类没有实体,所以不能实例化,包含纯虚函数的类都是抽象类
重写一下纯虚函数就能实例化
四、多态的原理
虚函数表
Base对象中除了_b成员还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表
派生类中的虚函数表:
- 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
- 基类b对象和派生类d对象虚表是不一样的,Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
虚表是不能被修改的,虚表存在常量区
多态的原理
满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
观察上图的红色箭头我们看到,p引用p对象时,p.BuyTicket在p的虚表中找到虚
函数是Person::BuyTicket。p引用s对象时,p.BuyTicket在s的虚表中找到虚函数是Student::BuyTicket。
这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
五、多继承关系中的虚函数表
多继承中的虚函数表
class Base1
{
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1 = 1;
};
class Base2
{
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2 = 2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1 = 3;
};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;cout << sizeof(d) << endl;int vft1 = *((int*)&d);Base2* ptr = &d;int vft2 = *((int*)ptr);PrintVTable((VFPTR*)vft1);PrintVTable((VFPTR*)vft2);return 0;
}
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中(粉色框选的)
上图中的Derive::func2两个地址不一样,但却调用的同一个函数(两个红色框选)
看汇编找一下原因
相当于ptr2对func1函数的调用进行了封装,在上层使用不会感觉到差别
这么做的目的是为了确保运行时根据对象的实际类型(而非指针的静态类型)调用正确的函数版本。
菱形继承、菱形虚拟继承
只有A有虚函数
class A
{
public:virtual void func1(){cout << "A::func1" << endl;}int _a;
};class B : public A
{
public:int _b;
};class C : public A
{
public:int _c;
};class D : public B,public C
{
public:int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
各自都重写func1(如果D没有对func1进行重写会报错)
class A
{
public:virtual void func1(){cout << "A::func1" << endl;}int _a;
};class B : public A
{
public:void func1(){cout << "B::func1" << endl;}int _b;
};class C : public A
{
public:void func1(){cout << "C::func1" << endl;}int _c;
};class D : public B,public C
{
public:void func1(){cout << "D::func1" << endl;}int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
菱形虚拟继承,各自都有单独的虚函数
class A
{
public:virtual void func1(){cout << "A::func1" << endl;}int _a;
};class B : virtual public A
{
public:virtual void func1(){cout << "B::func1" << endl;}virtual void func2(){cout << "B::func1" << endl;}int _b;
};class C : virtual public A
{
public:virtual void func1(){cout << "C::func1" << endl;}virtual void func2(){cout << "C::func1" << endl;}int _c;
};class D : public B,public C
{
public:virtual void func1(){cout << "::func1" << endl;}virtual void func3(){cout << "D::func1" << endl;}int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
六、简答继承和多态常见的问题
1.什么是多态?
静态多态:函数重载
动态多态: 继承中虚函数重写+父亲指针调用
更方便和灵活的多种形态调用
2.多态的实现原理?
函数名修饰规则+虚函数表
3.inline函数可以是虚函数吗?
可以,不过编译器就忽视inline属性,这个函数就不再是inline属性,因为虚函数要放到虚表中。
4.静态成员可以是虚函数吗?
不能,因为静态成员函数没有this指针,使用类型:: 成员函数的调用方式无法访问虚函数表,所以静态成员你函数无法放进虚函数表。
5.构造函数可以是虚函数吗?
不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
6.对象访问普通函数快还是虚函数更快?
首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
7.虚函数表时在什么阶段生成的,存在哪的?
虚函数表时在编译阶段就生成的,一般情况下存在代码段(常量区)的
8.C++菱形继承的问题?
数据冗余和二义性
9.什么是抽象类?抽象类的作用?
抽象类强制重写了虚函数,另外抽象类体现了接口继承关系。
