• 【C++技能树】多态解析


    在这里插入图片描述
    Halo,这里是Ppeua。平时主要更新C++,数据结构算法,Linux与ROS…感兴趣就关注我bua!

    在这里插入图片描述

    0.多态的概念

    试想下这个场景,不同身份的人去买票,相同的函数会执行不同的行为.这就需要多态去完成

    多态:顾名思义一个类中函数的多种状态.

    先来看看下面这个例子:

    class Person{
    public:
        virtual void BuyTicket()
        {
            cout<<"买票全价"<<endl;
        }
        virtual ~Person()
        {
            cout<<"~person()"<<endl;
        }
    };
    class Student:virtual public Person
    {
    public:
        virtual void BuyTicket()
        {
            cout<<"买票半价"<<endl;
        }
        virtual ~Student()
        {
            cout<<"~student()"<<endl;
        }
    };
    void buyticket(Person *p1)
    {
        p1->BuyTicket();   
    }
    
    • 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

    当在buyticket中传入student类型的地址与传入Person类型的地址会执行不同的行为.

    传入Person:会输出买票全价

    传入Student:会输出买票半价

    这就是多态的具体行为.会根据传入对象的不同执行不同的行为.

    0.1 多态的定义

    在基类需要重写的函数前加上virtual.

    在派生类中想要达到重写的函数前也加上virtual.(可加可不加),之后保持与基类中函数相同的返回值,函数名,参数列表即可完成重写.

    在调用时需要通过基类的指针或者引用来调用(将想要调用的类赋值到父类的指针或者引用,调用相同的函数.即可完成多态)

    有一个例外:返回值可不一定需要相同,可以为父类或子类对象的指针或引用(要同为指针,或者同为引用),称为协变

    所以多态就是:不同对象传递,调用不同的函数.多态调用看指向的对象.具体是什么内容 ,而不是看当前类型.

    48410c494a8f224dac0bd406a27a6dd

    1. 重写

    析构函数无论加不加virtual都完成重写

    class Person{
    public:
        virtual Person& BuyTicket()
        {
            cout<<"买票全价"<<endl;
        }
         ~Person()
        {
            cout<<"~person()"<<endl;
        }
    };
    class Student:virtual public Person
    {
    public:
        virtual Student& BuyTicket() 
        {
            cout<<"买票半价"<<endl;
        }
         ~Student()
        {
            cout<<"~student()"<<endl;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这是因为编译器对析构函数进行了处理,在编译阶段都重命名为了Destructor,所以他们为同名函数.

    为什么要进行这么处理呢?

    当用父类指针去调用子类对象时,使用delete时,若无多态则只会把父类的成员属性删除.并不会删除子类的.

    Person* p=new Person;
    delete p;
    p=new Student;
    //析构错误 不多态则没有调到派生类的析构
    delete p;  //p->destructor+operator delete p
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.Final与Override

    不想让一个函数被重写时可以在其后加上final,此时会从语法来检查该函数是否被重写.

    检查一个函数是否满足重写的条件可以在其后加上override,用来检查是否满足重写的条件

    class Person{
    public:
        // virtual void BuyTicket() final
        // {
        //     cout<<"买票全价"<
        // }
        virtual Person& BuyTicket()
        {
            cout<<"买票全价"<<endl;
        }
         ~Person()
        {
            cout<<"~person()"<<endl;
        }
    };
    class Student:virtual public Person
    {
    public:
    
        virtual Student& BuyTicket() override
        {
            cout<<"买票半价"<<endl;
        }
         ~Student()
        {
            cout<<"~student()"<<endl;
        }
    };
    
    • 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

    3.抽象类

    多态也叫接口继承,也就是只继承基类的函数接口,内容自己重新写.普通函数的继承是一种实现继承

    那么我们也可以设计一个只提供接口的类.那么就是抽象类

    在一个虚函数中最后加上=0 则成为 纯虚函数,包含纯虚函数的类则称为抽象类.

    class Car
    {
    public:
    		virtual void Drive() = 0;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    抽象类不能用来实例化对象,只能用来当作基类提供接口

    4.多态中的内存分布.

    根据上面的介绍,我们对多态的使用已经有了初步的了解.

    即在重定义的基础上加上一个virtual 以及满足三同(同名 同参数 同返回值)

    那么在内存中多态是如何存储的?

    class Person{
    public:
    	virtual void fun()
    	{
    		cout << "hello person";
    	}
    };
    class Student :public Person{
    	virtual void fun()override {
    		cout << "hello student";
    	}
    	virtual void fun3() {
    		cout << "hello student3";
    	}
    };
    int main()
    {
    	Person* p1;
    	Student s1;
    	p1 = &s1;
    	p1->fun();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    这是一个多态调用的例子.我们通过vs2022来看看其在内存中是如何存储的.

    image-20230903123709063

    可以看到,其有一个vfptr(virtual fun ptr)虚表指针.其和我们之前继承中的虚基表有点类似.当中存储了一个完成重写的函数.fun

    我们在内存中输入这个地址,可以发现其存储了两段地址.

    image-20230903124449931

    1. 第一个为完成重写的fun的地址

    2. 第二个为自己的虚函数fun3的地址,但这在上图中的结构模型中并没有被看到.所以结构模型有时并不是完全可信的

      所以,自己的虚函数会直接放在第一个虚表的最后.

    3. 第三个表示虚表的结束(在vs2022中是这样表示的)

      综上可以看出,在实例化的时候,会将基类的虚表复制一份到派生类当中,若有重写的函数,则用重写完的函数地址去覆盖虚表中原函数的地址.所以在原理层中:也叫做 覆盖.

    4.1虚表存在哪里?

    虚表是存储在 栈区 堆区 静态区 还是常量区呢?

    我们可以通过以下这个函数来验证

    int main()
    {
    	Person p1;
    	Student s1;
    	int a = 0;
    	printf("栈:%p", &a);
    	cout << endl;
    	int* b = new int[10];
    	printf("堆:%p", b);
    	cout << endl;
    
    	const char* c = "hello world";
    	printf("常量区%p", c);
    	cout << endl;
    
    	static int d = 10;
    	printf("静态区%p", &d);
    	cout << endl;
    
    	printf("虚表1:%p", *((int*)&p1));
    	cout << endl;
    	printf("虚表2:%p", *((int*)&s1));
    	cout << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    为什么这样区能取到虚表地址呢?通过取对应对象的地址,之后在进行强制转换为int,此时访问宽度为前四个字节(因为一个指针大小为四个字节).之后在对这个指针进行解引用就为虚表的地址*

    我们运行这段代码,就可以发现,虚表存储在常量区

    image-20230903125353911

    5.多态调用原理

    上文我们已经知道了,如何去调用多态.以及虚表存储的模型.下面是一个多态调用例子:

    class Person{
    public:
    	virtual void fun()
    	{
    		cout << "hello person";
    	}
    };
    class Student :public Person{
    	virtual void fun()override {
    		cout << "hello student";
    	}
    };
    int main()
    {
    	Person* p1;
    	Student s1;
    	p1 = &s1;
    	p1->fun();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    根据前面所学可以看出,当我调用p1->fun()时,由于p1里面存的是s1的地址,所以这里就会取到s1的虚表中f1的地址,完成多态调用

    所以多态是在运行的时候动态确定需要执行的函数

    5.1 动态绑定与静态绑定

    1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态.比如:函数重载

    2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

    6.继承中的虚函数表

    下面将从两个部分:多继承与单继承中的虚函数表来划分

    6.1单继承中的虚函数表

    在上文中可以知道,自己定义的虚函数,还没被重写之前是不会被放进虚表的.所以我们要研究这个存储就必须手动访问函数地址.

    我们先重命名下函数的指针,方便后期调用:

    typedef void(*FUNC_PTR)();
    
    • 1

    注意:这里的函数返回值为void,参数为空.将其重命名为FUNC_PTR

    所以我们通过这样的方式去强行访问类中虚表存储的函数

    class Person{
    public:
    	virtual void fun1()
    	{
    		cout << "Person::fun1";
    	}
    	virtual void fun2()
    	{
    		cout << "Person::fun2";
    	}
    	virtual void fun3()
    	{
    		cout << "Person::fun3" ;
    	}
    };
    class Student :public Person{
    	virtual void fun1()override {
    		cout << "Student::fun1()";
    	}
    	
    
    	virtual void fun3()
    	{
    		cout << "Student::fun3()";
    	}
    	virtual void fun4()
    	{
    		cout << "Student::fun4()";
    	}
    };
    typedef void(*FUNC_PTR)();
    void printvft(FUNC_PTR* table)
    {
    	for (int i = 0; table[i] != nullptr; i++)
    	{
    		printf("%d->%p", i, table[i]);
    		FUNC_PTR f=table[i];
    		f();
    		cout << endl;
    	} 
    }
    int main()
    {
    	Student s1;
    	Person p1;
    	cout << "person:" << endl;
    	int vft = *((int*)&p1);
    	printvft((FUNC_PTR*)vft);
    	cout << "student:" << endl;
    	vft = *((int*)&s1);
    	printvft((FUNC_PTR*)vft);
    }
    
    • 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
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    其中vft的赋值原理是,我们知道虚表地址是存在对象中的前四个位,所以我们用int*去取.之后解引用就是他的地址,存入到int当中,此时的 vft就是存储的地址,如何用这个地址去访问其中的函数即可.上面我们知道,在虚表的结束位置,会设置为0.所以我们可以以此来判断

    运行结果:

    image-20230905161534648

    在上面的函数中,只有fun1和fun3被Student完成了重写.

    所以我们可以得出一个结论:

    在单继承模型中,派生类会复制一份基类的虚表到自己中,若有重写函数,则用新的重写函数地址覆盖原函数地址 而不是直接对基类虚表直接进行修改,自己新的虚函数则跟在后面

    6.2多继承中的虚函数表

    class Person{
    public:
    	virtual void fun1()
    	{
    		cout << "Person::fun1";
    	}
    	virtual void fun2()
    	{
    		cout << "Person::fun2";
    	}
    	virtual void fun3()
    	{
    		cout << "Person::fun3" ;
    	}
    };
    class People {
    public:
    	virtual void fun1()
    	{
    		cout << "People::fun1";
    	}
    	virtual void fun2()
    	{
    		cout << "People::fun2";
    	}
    	virtual void fun3()
    	{
    		cout << "People::fun3";
    	}
    };
    
    class Student :public Person,public People
    {
    	virtual void fun1() {
    		cout << "Student::fun1()";
    	}
    	
    	virtual void fun4()
    	{
    		cout << "Student::fun4()";
    	}
    };
    typedef void(*FUNC_PTR)();
    void printvft(FUNC_PTR* table)
    {
    	for (int i = 0; table[i] != nullptr; i++)
    	{
    		printf("%d->%p", i, table[i]);
    		FUNC_PTR f=table[i];
    		f();
    		cout << endl;
    	} 
    }
    int main()
    {
    	Student s1;
    	Person p1;
    	People peo;
    	cout << "person:" << endl;
    	int vft = *((int*)&p1);
    	printvft((FUNC_PTR*)vft);
    	cout << "people:" << endl;
    	vft = *((int*)&peo);
    	printvft((FUNC_PTR*)vft);
    	cout << "student:" << endl;
    	vft = *((int*)&s1);
    	printvft((FUNC_PTR*)vft);
    }
    
    • 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
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    运行结果:

    image-20230905163221242

    我们在内存中看一下模型:

    student中的person:

    image-20230905163302648

    student中的people:

    image-20230905163312561

    我们可以很容易发现,student是对person进行了重写,并把自己的未重写虚函数放在了第一张虚表的最后.

    也就是:多继承模型中,派生类会在第一个继承的基类上进行重写,并且将自己未重写的虚函数放在其表尾
    image-20230905164632777

  • 相关阅读:
    一篇文章就能学会的 Redis 的事务
    OpenCV快速入门:窗口交互
    Python实现ABC人工蜂群优化算法优化支持向量机分类模型(SVC算法)项目实战
    2023秋招—大数据开发面经—杰创智能科技
    YOLO7 姿势识别实例
    《昇思25天学习打卡营第25天 | 昇思MindSporeResNet50迁移学习》
    【精品必知】Pod生命周期
    基于JavaWeb的宿舍管理系统设计与实现
    Spark集成hudi创建表报错
    指静脉当前遇到的问题
  • 原文地址:https://blog.csdn.net/qq_62839589/article/details/132695534