• C++ —— 多态



    前言: 多态灰常重要,简单说就是,同一件事,不同的对象会拿到不同的结果,这是好理解的。本文就带大家来认识多态,使用多态,理解多态。


    1. 认识多态以及使用多态

    1.1 多态的概念

    在编程语言和类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口。 多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上。

    举个例子:有一个父类叫做人,子类有学生,有军人。分别用父类,子类,实例出三个对象:普通人,学生A,军人B。这三个对象去做同一件事->买票,拿到的结果不一样,普通人是成人票,学生是学生票,军人是优先买票。这就是多态,同一件事,不同的对象去做,会有不同的结果(也就是调用不同的函数)。


    1.2 多态的分类

    多态有两大类:

    • 静态多态:比如函数重载,不同的传参会实列出不同的函数,这在编译时就能够实现
    • 动态多态:以父类的指针或引用,去调用同一个函数,传递子类对象和传递父类对象,会调用不同的函数,这在运行时实现

    1.3 构成多态的条件

    构成多态需要满足两个条件:

    • 通过基类的指针或引用,去调用虚函数
    • 被调用的函数必须是虚函数,而且派生类必须对基类的虚函数进行重写

    需要了解:
    (1) 虚函数

    在成员函数前加上关键字virtual,就为虚函数。

    (2) 重写

    虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类
    型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

    重写需要满足:

    • 必须是虚函数
    • 派生类的虚函数和基类的虚函数的返回值类型,函数名称,参数列表完全相同

    注意:重写的两个例外

    1. 协变:基类返回值是基类的指针或引用;派生类返回值是派生类的指针或引用。除了返回值类型不同,其余条件不变,这也构成重写。所以说重写返回值类型一定是相同的吗?不一定。
    class person
    {
    public:
    	virtual person* buy_ticekt()
    	{
    		cout << "买的票,是全价" << endl;
    
    		return this;
    	}
    };
    
    class student : public person
    {
    public:
    	virtual student* buy_ticekt()
    	{
    		cout << "买的票,是半价" << endl;
    
    		return this;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    1. 析构函数的重写

    析构函数的格式为:~类名(),那么基类和派生类的类名一定不同,所以它们可以对析构函数重写吗?答案是可以重写的。这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。也就是说,基类和派生类的析构函数名称,编译时会按照同名处理。

    class person
    {
    public:
    	virtual ~person()
    	{
    		cout << "~person()" << endl;
    	}
    };
    
    class student : public person
    {
    public:
    	virtual ~student()
    	{
    		cout << "~student()" << endl;
    	}
    };
    
    int main()
    {
        person* p1 = new person;
    
    	person* p2 = new student;
    
    	delete p1;
    
    	delete p2;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    很明显,基类对象调用基类析构;派生类对象调用派生类析构,派生类中继承的基类数据由基类析构。
    在这里插入图片描述
    还有一点需要强调

    只在基类将想要实现多态的函数设为虚函数就可以了,子类重写的函数前可以不加virtual,但是这种写法不够规范,我建议只要是重写那么就设为虚函数,什么情况下,可以这样使用呢?那就是析构函数的重写。


    1.4 多态简易实现

    有一个父类叫做人,子类有学生。去完成一件事:买票,人去买票是全价,学生去买票是半价。

    #include
    using namespace std;
    
    class person
    {
    public:
    	virtual void buy_ticekt()
    	{
    		cout << "买的票,是全价" << endl;
    	}
    };
    
    class student : public person
    {
    public:
    	virtual void buy_ticekt()
    	{
    		cout << "买的票,是半价" << endl;
    	}
    };
    
    
    int main()
    {
    	person common;
    
    	student A;
    
    	// 基类指针去调用
    	person* ptr1 = &common;
    	person* ptr2 = &A;
    
    	ptr1->buy_ticekt();
    	ptr2->buy_ticekt();
    
    	// 基类引用去调用
    
    	person& y1 = common;
    	person& y2 = A;
    
    	y1.buy_ticekt();
    	y2.buy_ticekt();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    上面的完全是满足多态的条件的,我们来看看运行结果: 有人可能对重写还是不太理解,后面讲原理时会细讲的。

    在这里插入图片描述


    1.5 c++的两个关键字 override 和 final

    从上面重写的学习可以看出,c++对于重写的要求还是很高的。所以引入这两个关键字,进一步的规范重写。

    (1) override :检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

    它是用于检查,派生类是否对某个虚函数进行了重写,只需要在子类的重写函数后面加上 override。

    父类:
    virtual void buy_ticekt()
    	{
    		cout << "买的票,是全价" << endl;
    
    	}
    子类:
    virtual void buy_ticekt(int a=1) override
    	{
    		cout << "买的票,是半价" << endl;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    很明显以上,并没有完成对父类成员函数的重写,但是我加上了关键字 override,来看看运行情况:

    报错了:

    在这里插入图片描述
    (2) final :放在类后,表示该类不能被继承 / 放在虚函数后,表示该虚函数不能再被重写

    • 在c++11前,我们想要一个类不被继承,可能用的方式是将基类的构造函数设置为私有,那么派生类继承后,会导致派生类构造对象时,无法初始化基类,而导致派生类对象构造不成功。这是一种间接的限制,这种限制也很麻烦:使得基类构造只能用基类中的函数封装一下,才能使用。

    所以引入了一个关键字: final ,只需要在类名后加上 final ,此类就不能被继承了。

    class person final
    {
    
    }
    
    • 1
    • 2
    • 3
    • 4

    如果还要继承此类,毫无疑问会报错:
    在这里插入图片描述

    • 放在基类的虚函数后,使得派生类不能对此虚函数进行重写:
    基类:
    virtual void buy_ticekt() final
    	{
    		cout << "买的票,是全价" << endl;
    
    	}
    派生类:
    virtual void buy_ticekt() 
    	{
    		cout << "买的票,是半价" << endl;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    基类的虚函数后已经加上了关键字:final,但是子类依旧对其重写,毫无疑问会报错:

    在这里插入图片描述


    1.6 抽象类
    1.6.1 抽象类的概念

    抽象类:包括纯虚函数的类,纯虚函数:虚函数的后面加上 =0,纯虚函数不需要定义,只声明就好了。这种类被称为抽象类。

    抽象类有什么作用?它是一种接口类,不需要实例化出对象,它内部的纯虚函数,需要被其子类重写后才能有价值,否则没有意义。

    比如:我定义一个抽象类->car,其中的纯虚函数是显示车的品牌。昂,车的品牌多了去了,一个抽象类car,能够显示出其车牌吗?所以只需要声明此函数,没必要实现。

    #include
    using namespace std;
    
    class car
    {
    public:
    	virtual void Car_brand() = 0;
    };
    
    class BMW : public car
    {
    public:
    	virtual void Car_brand()
    	{
    		cout << "my_car_brand is BMW" << endl;
    	}
    };
    
    class Benz : public car
    {
    public:
    	virtual void Car_brand()
    	{
    		cout << "my_car_brand is Benz" << endl;
    	}
    };
    
    int main()
    {
        car* ptr1 = new BMW;
    
    	car* ptr2 = new Benz;
    
    	ptr1->Car_brand();
    
    	ptr2->Car_brand();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    在这里插入图片描述

    1.6.2 接口继承和实现继承

    普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的
    继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所
    以如果不实现多态,不要把函数定义成虚函数。

    抽象类,更加体现了接口继承,它的内部直接搞一个纯虚函数,只有被子类重写的命,如果不被重写那么其毫无存在意义。

    纯虚函数强制要求子类对其重写,如果不被重写,那么直接报错:因为不能实例抽象类
    在这里插入图片描述
    这一点有点像刚讲的override ,不过override是语法层面上的检查。上面那个是本质强烈要求。


    2. 多态的原理

    通过上面的学习,我们认识了多态,并且简单的使用了多态,那么现在我们来讲讲多态的实现原理。上车了,同学们坐稳扶好。

    2.1 虚函数表

    先出一道迷惑的小题:
    求:下面的类的大小

    class base
    {
    public:
    	virtual void test()
    	{
    		cout << "how are you" << endl;
    	}
    
    private:
    	int _a;
    	char _b;
    	int _c;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    一看这题简单呀,类的成员函数在代码区,所以只看成员变量,利用学过的内存对齐知识,很轻松的得到答案:12字节。好的,我们用sizeof()来 验证一下吧:
    在这里插入图片描述
    答案是:16 字节。昂,怎么回事?通过调试,来看看base 对象里都有什么?

    在这里插入图片描述
    下面的三个成员变量:_a,_b,_c不用说,主要是那个_vfptr是什么?它就是虚函数表,存的是虚函数的地址,可以将其理解成一个函数指针数组,所以它的类型是 void ** 一个二级指针,内部存的是 void *。


    2.2 利用虚函数再次理解重写

    重写又被称为覆盖,什么是覆盖?覆盖的是谁?如何覆盖?我们来通过虚函数表,来理解重写。

    举例:我可以定义一个基类,派生类对基类中的一个虚函数进行重写

    class base
    {
    public:
    	virtual void fun1()
    	{
    		cout << "how are you" << endl;
    	}
    
    	virtual void fun2()
    	{
    		cout << "i am fine" << endl;
    	}
    
    	virtual void fun3()
    	{
    		cout << "thanks" << endl;
    	}
    
    protected:
    	int _a;
    };
    
    class derive : public base
    {
    public:
    	virtual void fun1()
    	{
    		cout << "are you ok" << endl;
    	}
    };
    
    int main()
    {
        base b;
    	derive d;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    可以看到,基类总共有三个虚函数,派生类对第一个虚函数进行了重写。通过调试查看重写是怎么肥事?
    在这里插入图片描述
    派生类对fun1(),进行了重写,所以虚函数表的第一个函数指针的值是不同的,也就是说派生类并没有继承父类的fun1(),对此fun1()进行了覆盖,本质上,子类的fun1()已经是一个新的函数,子类的虚函数表的第一个函数指针不再指向从基类继承的函数位置,而是指向了子类重写的虚函数位置。

    我们再来看虚函数表的下面俩个位置,发现基类和派生类指向的虚函数位置是一样的。

    所以总结:派生类 拷贝 基类的虚函数表 -> 没有重写的虚函数指向同一函数地址,进行重写的虚函数,派生类会指向它重写的函数的地址。

    讲到这里我提一个问题:虚函数存在哪里?虚函数表存在哪里?

    虚函数和普通成员函数一样都存在代码段,只不过虚函数的指针会存在虚函数表中;虚函数表存在具体的对象中,每个有虚函数的对象都有虚函数表。

    注意:

    1. 同一类的对象的虚函数表是相同的,这个好理解,因为虚函数表存的是虚函数的地址,同一类型对象的虚函数地址是相同的,所以虚函数表也相同。
      在这里插入图片描述
      这个可以验证一下:
    int main()
    {
        derive d;
    	derive d1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    1. 虚函数表的末尾存的是null。
      如果在派生类中继续声明定义虚函数,派生类虚函数表中会有新定义的虚函数指针吗?
      我们可以试一下:
    class derive : public base
    {
    public:
    	virtual void fun1()
    	{
    		cout << "are you ok" << endl;
    	}
    
    	virtual void fun4()
    	{
    		cout << "not ok" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我在派生类新增了一个虚函数fun4(),通过调试查看一下:
    在这里插入图片描述
    关谷神奇发现:没有显示的看到新增的虚函数在虚表中,阿尤头大了,到底存没存进去虚函数的地址,上面不是说过虚函数表中一定会有虚函数的地址吗?这是编译器在搞怪,其实是存进去了,通过内存可以看一下,虚函数表末尾存的是空指针嘛。
    在这里插入图片描述
    毫无疑问:红圈的地方就是我们新存的虚函数地址,我上面画到的青色框,内存从后往前读,可发现两个框的内容是一致的。用内存验证了,虚函数表存的虚函数的地址。


    2.3 多态的原理

    有了上面知识的铺垫,我们来正式说道多态的实现原理:多态就是不同类型的对象去做同一件事,拿到不同的结果。满足多态需要有俩个条件:基类的指针或者引用去调用虚函数,虚函数必须被派生类重写。为什么要满足这两个条件?

    1. 如果不是基类的指针或引用去调用,而是传的派生类的指针或引用,基类的对象有越界的风险,并且引用的话,基类的对象都无法传过去。我们要的就是基类的指针或引用,因为多态的实现,看的是虚函数部分,只需要看子类对基类重写的那个函数就可以了,而这个函数必然在从基类拷贝下来的虚函数表中。
    2. 虚函数必须被重写也好理解,不重写那就不叫多态,实现和基类一样的功能,那还有设计成虚函数的必要吗?

    多态的实现靠的就是虚函数表,通过虚函数表找到,重写的虚函数,从而实现不同于基类的功能。

    我们回到最开始举的例子->不同人买票,这样大家就更懂了:

    class person 
    {
    public:
    	virtual void buy_ticekt() 
    	{
    		cout << "买的票,是全价" << endl;
    
    	}
    };
    
    class student : public person
    {
    public:
    	virtual void buy_ticekt() 
    	{
    		cout << "买的票,是半价" << endl;
    	}
    };
    
    int main()
    {
        person common;
    
    	student A;
    
    	// 基类指针去调用
    	person* ptr1 = &common;
    	person* ptr2 = &A;
    
    	ptr1->buy_ticekt();
    	ptr2->buy_ticekt();
    
    	// 基类引用去调用
    
    	person& y1 = common;
    	person& y2 = A;
    
    	y1.buy_ticekt();
    	y2.buy_ticekt();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    通过调试,来看看多态的实现过程:

    (1) 重写后,也可以看到,重写的虚函数也显示的标明出了类域
    在这里插入图片描述
    (2) 不同的对象会通过虚表,从而调到不同的函数

    因为是用的基类的指针或引用,所以派生类和基类的指针或引用都可以使用多态,编译器一看是基类的指针或引用,根本不管你是基类赋值的指针,还是派生类赋值的指针,我一视同仁,都只看基类继承下来的部分。

    画图讲一下吧,干讲难理解:
    在这里插入图片描述

    1. ptr1指向的是基类对象,编译器管你是啥对象,你就是个person指针,你只能以person的视角来调用,要实现多态,我就去你指向的对象的虚函数表中找到对应的虚函数地址:
      在这里插入图片描述

    2. ptr2指向的派生类对象,编译器同样不管你是啥对象,依旧看出person指针,只能以person视角来调用,要实现多态,我就去你指向的对象的虚函数表中找到对应的虚函数地址:
      在这里插入图片描述

    3. 然后有了函数地址,当然就会调用对应的函数
      在这里插入图片描述
      在这里插入图片描述
      讲到这里,不知道大家看出来没,动态多态的实现是运行时,才完成的,它是运行时才完成的对应调用。


    3. 多继承的多态(了解即可)

    多继承的多态,啧啧,多继承我就有点头大了,里面的菱形继承更是头大,你还搞一个多继承的多态,啊,没关系,简单的讲讲就ok了,都得要稳稳的幸福,知识不能有太大的缺口。

    我举一个例子:

    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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    base1中有func1(),base2中也有func1(),并且多继承的派生类对func1(),进行了重写,是对从哪个继承下来的fun1()进行的重写呢?派生类中没有进行重写的继承下来的虚函数如何存储呢?还有派生类中新生的虚函数又是如何存在那呢?

    • 首先解决第一个问题:

    对base1以及base2中的func1()都进行了重写,而且重写后的虚函数是同一个。
    在这里插入图片描述
    看base1,base2中虚函数表,第一个位置就是我们重写后的func1(),发现地址尽然不同,但是它俩通过跳转最后是跳转到了同一个函数地址上,所以我们可以得到一个结论:虚函数表里存的不一定是虚函数的地址,它可能会存jmp跳转前的地址。

    • 然后第二个问题:

    继承下来但没有进行重写的虚函数,会存放在继承下来对应的虚函数表中。

    在这里插入图片描述
    这个问题解决得比较简单。

    • 最后的问题:

    派生类新增的虚函数会放在第一个继承的基类的虚函数表中,不过是最后一个虚函数指针罢了。这个我们可以通过内存来看,虚函数表的默认是null,内存就是00000,这个上面讲过。

    在这里插入图片描述
    对于多继承的多态,掌握这些也不少了,但如果,还是很好奇各种多继承的多态,我建议可以查阅相关的C++文献,要理解菱形继承的多态,不是一件容易的事。大家感兴趣可以自己再去研究,不过一般情况下用的少。


    结尾语: 以上就是多态的相关知识,大家有问题可以评论或者私信,还有欢迎大佬来此斧正。

  • 相关阅读:
    记录一次VS编译失败: 由于.editorconfig 无法找到 XXX 文件的一部分. 导致编译不成功;
    python 桌面软件开发-matplotlib画图鼠标缩放拖动
    深度学习参数初始化(二)Kaiming初始化 含代码
    Ps:选区的布尔运算
    WiFi在Settings中的热点开启流程小结
    msvcp140.dll丢失怎么办?msvcp140.dll重新安装的解决方法
    通信-CAN-01 总线拓扑
    Html 引入element UI + vue3 报错Failed to resolve component: el-button
    【scikit-learn基础】--模型持久化
    chartgpt+midjourney
  • 原文地址:https://blog.csdn.net/lyzzs222/article/details/126911379