• C++多态


    📋 个人简介

    • 💖 作者简介:大家好,我是菀枯😜

    • 🎉 支持我:点赞👍+收藏⭐️+留言📝

    • 💬格言:不要在低谷沉沦自己,不要在高峰上放弃努力!☀️

      v2-af3cfbda99d44f8eb114cc6c74c92253_720w

    前言

    之前我们已经将面向对象三大特性中的封装和继承讲了,接下来剩下最后一个环节了,那就是 多态

    多态概念

    通俗来说,就是去做相同一件事时,不同的人有不同的状态。比如买火车票,普通人是一个价格,而但学生去买就有一定的折扣,这就是一种多态。

    多态定义

    多态是在不同继承关系的类对象,去调用同一函数,产生不同的行为。而要形成多态,必须满足下面这两个条件。

    1. 必须通过基类的指针或者引用调用虚函数

    2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

    首先解释一下上面的一些名词,基类就是父类。

    虚函数,我们在上篇文章讲虚拟继承时使用了一个关键字叫virtual,而虚函数就是用这个关键字修饰的函数,比如下面的 BuyTicket函数就是一个虚函数。

    class Person
    {
        virtual void BuyTicket()
        {
            cout << "全价票" << endl;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那么什么是虚函数的重写(覆盖)呢?

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

    还是以买票为例子,我们创建两个类People和Student,二者都有BuyTicket这个行为,但二者两个函数的实现不同,此时我们可以称Student重写了Person类的BuyTicket函数。

    class Person
    {
    public:
        virtual void BuyTicket()
        {
            cout << "全价票" << endl;
        }
    };
    
    class Student : public Person
    {
    public:
        virtual void BuyTicket()
        {
            cout << "半价票" << endl;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    此时,BuyTicket这个行为就构成了多态,当我们使用不同对象的父类引用去调用这些函数时,就会产生不同的行为。

    void Buy(Person& p)
    {
        p.BuyTicket();
    }
    
    int main()
    {
        Student s;
        Person p;
        Buy(s);
        Buy(p);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    image-20220803102342446

    此时我们可以看到,当我们传子类对象给Buy函数时调用的为子类中的函数,而父类传过去,调用的为父类函数。

    虚函数重写两个例外

    我们之前提到在C++中构成函数重写的两个条件:

    1. 必须通过基类的指针或者引用调用虚函数

    2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

    大部分情况下,都必须遵循这两个条件,但C++不愧是C艹,没有意外的话,它一定会出意外。

    88c57d2eb7ae153b2bd407207dbc5754f8dc78578bba54ee7

    1. 协变(父类与子类返回值类型不同)

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

      在上面这段代码中,我们可以发现两个f()函数的返回值不同,一个为A&,一个为 B&,但此时二者仍然构成函数重写,这种情况我们称之为协变,即父类虚函数返回父类的指针或引用,子类虚函数返回父类的指针或引用。

    2. 析构函数的重写

      class Person
      {
      public:
      	virtual ~Person()
      	{
      		cout << "~Person()" << endl;
      	}
      };
      class Student : public Person
      {
      public:
      	virtual ~Student()
      	{
      		cout << "~Student()" << endl;
      	}
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16

      同样是这两个类,其中Student类是从Person类处,继承而来。虽然Person和Student类的两个析构函数名字并不相同,但也构成函数的重写。

      因为~Person() 和 ~Student是我们给析构函数起的名字,而不是它实际的名字,当程序真正去编译时,这两个函数的名字都会被destructor所替换,所以实际上二者名字是一样的。

    三个概念的辨析

    目前为止,我们已经学了重载,重写,重定义(隐藏),那么三者有什么区别呢?

    image-20220803105745204

    抽象类

    在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

    再继续以买票为例子,首先我们将Person类定义为一个抽象类。

    class Person
    {
    protected:
        std::string _name;
        int _age;
    
    public:
        void virtual BuyTicket() = 0;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们将Person类的BuyTicket函数设置为纯虚函数,此时Person类就是一个抽象类,我们无法使用Person类去实例化一个对象。只有当我们的类去重写这个虚函数之后,才能去实例化对象。

    class Student : public Person
    {
    private:
        int _code;
    
    public:
        virtual void BuyTicket()
        {
            cout << "半价票" << endl;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    此时我们就可以用Student类去实例化一个对象。

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

    多态实现原理

    接下来,我们就来看看底层,去揭开多态的神秘面纱,看看多态究竟是如何去实现的。

    88b017fe88a921bbbc8bd14e614722563ee2a5e7f6c64741d

    虚函数表

    概念

    首先我们先来看看下面这道题目,看看大家能否答对?

    //32位平台下
    class a
    {
    public:
    	virtual void f()
    	{
    		int c = 0;
    		++c;
    	}
    private:
    	int _b = 0;
    };
    
    int main()
    {
        a tmp;
    	cout << sizeof(tmp);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    大家觉得这个程序应该输出什么呢?

    我相信肯定会有人马上喊:答案是4,因为函数不存在类中,类中只存成员变量,这个类中只有b成员占4个字节,所以答案是4。好巧不巧,我一开始也以为是这个样子,并且觉得这个题这么简单有什么好考的。可当程序运行起来,我傻眼了。

    image-20220911144555252

    答案是8,这是为什么? 接下来我们使用调试来看看,这个类中到底有什么东西,使它的大小变成了8字节。

    image-20220911144836159

    我们可以发现,在tmp对象中还偷偷藏了一个指针,那么这个指针又是用来干什么的呢?

    image-20220911145102055

    我们发现了一个熟悉的东西,这个不正是我们所写的虚函数的名字吗

    这个隐藏在暗处的指针,我们称作虚函数指针,它指向了一片空间,这片空间称之为虚函数表,这个表存放的就是类中虚函数的地址。

    存在原因

    现在我们知道了什么是虚函数指针,以及虚函数表的概念,那么为什么需要这个表和这个指针呢?我们接下来再往后看

    class A
    {
    public:
    	virtual void Func1()
    	{
    		cout << "A::f1()" << endl;
    	}
    
    	virtual void Func2()
    	{
    		cout << "A::f2()" << endl;
    	}
    
    	void Func3()
    	{
    		cout << "A::f3()" << endl;
    	}
    private:
    	int _a;
    };
    
    class B : public A
    {
    private:
        int _b;
    };
    
    int main()
    {
    	A a;
    	B b;
    	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

    我们分别写了两个类,一个A类作为父类,其中有三个不同的函数,一个B类作为子类,从A类继承而来。首先我们并不对B类进行任何操作,来看看A类和B类中成员的情况。

    image-20220911152054478

    我们可以看到二者的虚函数指针指向同一片空间,存的虚函数表相同,我们还可以发现,虚函数表中并没有Func3,说明在虚函数表中存放的只有虚函数的地址。

    接下来我们在B类中对Func1进行一个重写,来看看是否会发生什么变化。

    class B : public A
    {
    public:
    	virtual void Func1()
    	{
    		cout << "B::f1()" << endl;
    	}
    private:
    	int _b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    image-20220911153007963

    此时我们发现b对象中的虚函数指针发生了变化,它不再与a对象的虚函数指针指向相同空间,同时b对象的虚函数表中的第一个函数也发生了变化,变为了它自己的成员函数。

    根据目前的现象,我们可以得出一个结论:所谓重写实际上是对虚函数指针所指向的位置进行一个重写

    为了实现重写,我们需要这个指针和这个表的存在

    动态链接和静态链接

    虚函数表和虚函数指针的事我们先告一段落,等会我们再来看它,接下来呢,我们来看看普通调用和多态调用,二者在汇编代码上有何区别。

    我们把刚刚的A类和B类拿过来,再次使用。

    class A
    {
    public:
    	virtual void Func1()
    	{
    		cout << "A::f1()" << endl;
    	}
    
    	virtual void Func2()
    	{
    		cout << "A::f2()" << endl;
    	}
    
    	void Func3()
    	{
    		cout << "A::f3()" << endl;
    	}
    
    private:
    	int _a = 0;
    };
    
    class B : public A
    {
    public:
    	virtual void Func1()
    	{
    		cout << "B::f1()" << endl;
    	}
    
    	void Func2()
    	{
    		cout << "B::f2()" << endl;
    	}
    
    private:
    	int _b = 0;
    };
    
    int main()
    {
    	B b;
    	A* a = &b;
    	a->Func1();
    	a->Func3();
    	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
    • 45
    • 46
    • 47

    我们来看看Func1()和Func3()二者汇编代码的区别。

    image-20220911155919337

    我们可以很明显的发现多态调用一个函数,比普通调用一个函数多了很多的汇编代码,多出来的汇编代码其实就是一个查表的过程。多态调用其实就是查虚函数表,然后从虚函数中取出要调用的函数的地址,再去call。而普通函数的调用就是直接去call这个函数的地址。

    这种在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,称为动态链接。

    既然有动态链接,当然也有静态链接**,静态链接是在程序编译期间就确定了程序的行为,调用具体的函数**。比如:函数重载。

    总结

    有了上面的虚函数表和动态链接的知识后,我们再来补上多态原理的最后一环,来解释一下为什么多态需要使用父类的指针或引用才能实现。

    大家记不记得我们在聊继承的时候,讲过一个切片。不记得的朋友可以移步去看看(56条消息) C++继承_。菀枯。的博客-CSDN博客

    在进行切片时,我们也会将子类的虚函数指针给父类。因此函数的重写原理如下:

    1. 子类首先会从父类处继承原本的虚函数指针。

    2. 子类改变继承下来的虚函数指针,指向新的虚函数表的地址。

    3. 当我们使用父类的指针或引用时,会发生切片,虚函数表为原来对象的虚函数表

    4. 虚函数的调用实际上是一个查虚函数表的过程,根据不同对象传递的不同的虚函数表,我们就能实现不同对象调用同一函数时产生不同的效果。

    结语

    1647941444633

    欢迎各位参考与指导!!!

  • 相关阅读:
    Nmap识别目标机器上服务的指纹
    MySQL中使用函数会使索引失效?
    C++模拟实现——红黑树
    读书-人生算法
    六月集训(24)线段树
    DDoS攻击与CC攻击:网络安全的两大挑战
    羽夏看Linux内核——启动那些事
    快用云科阎志涛:现代数据栈中的数据建模
    114. 如何通过单步调试的方式找到引起 Fiori Launchpad 路由错误的原因
    数据结构 - 跳表
  • 原文地址:https://blog.csdn.net/m0_60447315/article/details/126806414