C++ 面向对象三大特性——多态

简介: 面向对象三大特性的,封装,继承,多态,今天我们研究研究C++的多态。

 

✅<1>主页:我的代码爱吃辣

📃<2>知识讲解:C++ 继承

☂️<3>开发环境:Visual Studio 2022

💬<4>前言:面向对象三大特性的,封装,继承,多态,今天我们研究研究C++的多态

目录

一.多态的概念

二.多态的定义及实现

1.多态的构成条件

2. 虚函数

3.虚函数的重写

4. C++11 override 和 final

5. 重载、覆盖(重写)、隐藏(重定义)的对比

三. 抽象类

1.概念

2.接口继承和实现继承

四.多态的原理

1.虚函数表

2.多态的原理

3. 动态绑定与静态绑定

5.单继承和多继承关系的虚函数表

1. 单继承中的虚函数表

2. 多继承中的虚函数表


image.gif编辑

一.多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会

产生出不同的状态。

举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人
买票时是优先买票。

再举个栗子: 几年前为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的

活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5

毛....。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如

你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 =random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。ps:支付宝红包问题纯属瞎编,大家仅供娱乐。

C++中多态演示:

class Person
{
public:
  virtual void  buy_ticket()
  {
    cout << "这是一个成年人---->全价票" << endl;
  }
};
class Student :public Person
{
public:
  virtual void buy_ticket()
  {
    cout << "这是一个学生---->八折票" << endl;
  }
};
int main()
{
  //定义好成人对象和学生对象
  Person p;
  Student s;
  //在去买票之前他们是没有区别的
  Person& person1 = p;
  Person& person2 = s;
  //买票的学生和成人价格不一样
  person1.buy_ticket();
  person2.buy_ticket();
    return 0;
}

image.gif

image.gif编辑

二.多态的定义及实现

1.多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了

Person。Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态还有两个条件:💊💊💊💊💊

    1. 必须通过基类的指针或者引用调用虚函数
    2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

    image.gif编辑

    没有重写虚函数:

    class Person{
    public:
       void  buy_ticket(){cout << "全价票" << endl;}
    };
    class Student :public Person{
    public:
       void buy_ticket(){cout << "八折票" << endl;}
    };
    int main()
    {
      Person p;
      Student s;
      Person& person1 = p;
      Person& person2 = s;
      person1.buy_ticket();
      person2.buy_ticket();
      return 0;
    }

    image.gif

    image.gif编辑

    有虚函数重写,不是通过基类的指针或者引用调用:

    class Person{
    public:
       virtual void  buy_ticket(){cout << "全价票" << endl;}
    };
    class Student :public Person{
    public:
       virtual void buy_ticket(){cout << "八折票" << endl;}
    };
    int main()
    {
      Person p;
      Student s;
      Person person1 = p;
      Person person2 = s;
      person1.buy_ticket();
      person2.buy_ticket();
      return 0;
    }

    image.gif

    image.gif编辑

    2. 虚函数

    虚函数:即被virtual修饰的类成员函数称为虚函数,虚函数就是为多态而出现的。

    //虚函数
        virtual void  buy_ticket()
      {
        cout << "这是一个成年人---->全价票" << endl;
      }

    image.gif

    3.虚函数的重写

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

    注意:派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同,这里要求非常严格,因为重写或者是覆盖,是函数体的重写或者覆盖。

    注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因

    为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。

    class Person {
    public:
      virtual void BuyTicket() { cout << "买票-全价" << endl; }
    };
    class Student : public Person {
    public:
      //virtual void BuyTicket() { cout << "买票-半价" << endl; }
      void BuyTicket() { cout << "买票-半价" << endl; }
    };
    void Func(Person& p)
    {
      p.BuyTicket();
    }
    int main()
    {
      Person p1;
      Student p2;
      Func(p1);
      Func(p2);
      return 0;
    }

    image.gif

    image.gif编辑

    虚函数重写的两个例外:

    1. 协变(基类与派生类虚函数返回值类型不同)

    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指

    针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)

    class Person {
    public:
      virtual Person* BuyTicket() { cout << "买票-全价" << endl; return nullptr; }
    };
    class Student : public Person {
    public:
      virtual Student* BuyTicket() { cout << "买票-半价" << endl; return nullptr; }
    };
    void Func(Person& p)
    {
      p.BuyTicket();
    }
    int main()
    {
      Person p1;
      Student p2;
      Func(p1);
      Func(p2);
      return 0;
    }

    image.gif

    image.gif编辑

    2. 析构函数的重写(基类与派生类析构函数的名字不同)

    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,

    都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,

    看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处

    理,编译后析构函数的名称统一处理成destructor。

    class Person {
    public:
      virtual Person* BuyTicket() { cout << "买票-全价" << endl; return nullptr; }
      virtual ~Person() { cout << "~Person()" << endl; }
    };
    class Student : public Person {
    public:
      //virtual void BuyTicket() { cout << "买票-半价" << endl; }
      virtual Student* BuyTicket() { cout << "买票-半价" << endl; return nullptr; }
      ~Student() { cout << "~Student()" << endl; }
    };
    int main()
    {
      Person* p = new Person;
      Person* s = new Student;
      delete p;
      delete s;
      return 0;
    }

    image.gif

    4. C++11 override 和 final

    1. final:修饰虚函数,表示该虚函数不能再被重写

    class Car
    {
    public:
      virtual void Drive() final {}
    };
    class Benz :public Car
    {
    public:
      virtual void Drive() { cout << "Benz-舒适" << endl; }
    };

    image.gif

    image.gif编辑

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

    class Car {
    public:
       void Drive() {}
    };
    class Benz :public Car {
    public:
      virtual void Drive() override { cout << "Benz-舒适" << endl; }
    };

    image.gif

    此时基类的函数并不是虚函数,所以派生类中函数没有构成重写,所以此处直接报错。

    image.gif编辑

    5. 重载、覆盖(重写)、隐藏(重定义)的对比💊💊💊

    image.gif编辑

    三. 抽象类

    1.概念

    在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口

    类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生

    类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

    抽象类就像我们我们生活一些泛型的事物,例如车,车也有油车,电车,他们都是车,但是他们的动力来源不一样。如果只是针对车这个泛型事物没有具体到哪一种车的时候,我们也不清楚他的动力来源是什么。

    class Car
    {
    public:
      virtual void Drive() = 0;
    };
    class Benz :public Car
    {
    public:
      virtual void Drive()
      {
        cout << "Benz-柴油机" << endl;
      }
    };
    class BMW :public Car
    {
    public:
      virtual void Drive()
      {
        cout << "BMW-高能锂电池" << endl;
      }
    };
    void Test()
    {
      Car* pBenz = new Benz;
      pBenz->Drive();
      Car* pBMW = new BMW;
      pBMW->Drive();
    }
    int main()
    {
      Test();
        return 0;
    }

    image.gif

    2.接口继承和实现继承

    普通函数的继承是一种实现继承派生类继承了基类函数,可以使用函数,继承的是函数的实
    现。
    虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成

    多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

    四.多态的原理

    1.虚函数表

    这里常考一道笔试题:sizeof(Base)是多少?

    // 这里常考一道笔试题:sizeof(Base)是多少?
    class Base
    {
    public:
        virtual void Func1()
        {
        cout << "Func1()" << endl;
        }
    private:
        int _b = 1;
    };

    image.gif

    32位下结果:

    image.gif编辑

    通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些

    平台可能会放到对象的最后面,这个跟平台有关,我们此处使用的是VS2022),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析:

    image.gif编辑

    针对上面的代码我们做出以下改造:

      • 我们增加一个派生类Derive去继承Base
      • Base再增加一个虚函数Func2和一个普通函数Func3
      • Derive中重写Func1,Func2,定义一个虚函数Func3
      class Base
      {
      public:
        virtual void Func1(){cout << "Base::Func1()" << endl;}
        virtual void Func2(){cout << "Base::Func2()" << endl;}
        virtual void Func3(){cout << "Base::Func3()" << endl;}
      private:
        int _b = 1;
      };
      class Derive : public Base
      {
      public:
        virtual void Func1(){cout << "Derive::Func1()" << endl;}
        virtual void Func2(){cout << "Derive::Func2()" << endl;}
      private:
        int _d = 2;
      };

      image.gif

      image.gif编辑

      1.派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚

      表指针也就是存在这部分的,另一部分是自己的成员。

      2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1,Func2完成了重写,所以d的虚表中存的是重写的Derive::Func1和Derive::Func2,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

      3.另外Func3继承下来后是虚函数,所以放进了虚表,如果Derive::Func3没有重写Derive::Func3不会被放进虚表,或者Base::Func3不是虚函数,不会Derive::Func3被重写也不会放进虚表。

      4.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr(仅仅针对VS系类编译器)。

      5.总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生

      类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己

      新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

      这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的?答:虚函数存在
      虚表,虚表存在对象中。注意上面的回答的错的。
      但是很多童鞋都是这样深以为然的。注意
      虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是
      他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
      那么虚表存在哪的

      呢?实际我们去验证一下会发现vs下是存在代码段的。

      image.gif编辑

      虚表的地址和代码区(常量区)很接近。

      2.多态的原理

      上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的

      Person::BuyTicket,传Student调用的是Student::BuyTicket。

      image.gif编辑

      1.观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚

      函数是Person::BuyTicket。

      2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中

      找到虚函数是Student::BuyTicket。

      3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

      4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调

      用虚函数。反思一下为什么?因为单纯的使用对象直接接受,不会拷贝虚表。

      5. 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行

      起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

      image.gif编辑

      void Func(Person* p)
      {
        p->BuyTicket();
      }
      int main()
      {
        Person mike;
        Func(&mike);
        mike.BuyTicket();
        return 0;
      }
      // 以下汇编代码中跟你这个问题不相关的都被去掉了
      void Func(Person* p)
      {
            ...
          p->BuyTicket();
            // p中存的是mike对象的指针,将p移动到eax中
            001940DE  mov     eax, dword ptr[p]
          // [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
          001940E1  mov     edx, dword ptr[eax]
          // [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
          00B823EE  mov     eax, dword ptr[edx]
          // call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来
          以后到对象的中取找的。
          001940EA  call     eax
          00头1940EC  cmp     esi, esp
      }
      int main()
      {
            ...
          // 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调
          用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址
          mike.BuyTicket();
            00195182  lea     ecx, [mike]
          00195185  call     Person::BuyTicket(01914F6h)
          ...
      }

      image.gif

      3. 动态绑定与静态绑定

        1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
        2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
        3. 本小节之前(5.2小节)买票的汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑定。

        5.单继承和多继承关系的虚函数表

        1. 单继承中的虚函数表

        需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类

        的虚表模型前面我们已经看过了,没什么需要特别研究的。我们看下面一个问题:

        class Base
        {
        public:
          virtual void Func1(){cout << "Base::Func1()" << endl;}
          virtual void Func2(){cout << "Base::Func2()" << endl;}
          virtual void Func3(){cout << "Base::Func3()" << endl;}
        private:
          int _b = 1;
        };
        class Derive : public Base
        {
        public:
          virtual void Func1(){cout << "Derive::Func1()" << endl;}
          virtual void Func2() { cout << "Derive::Func2()" << endl; }
          virtual void Func4(){cout << "Derive::Func4()" << endl;}
        private:
          int _d = 2;
        };
        int main()
        {
          Base b;
          Derive d;
          return 0;
        }

        image.gif

        image.gif编辑

        这里大家仔细就会发现在派生类的虚表怎么没有Func4呢?这里是编译器的监视窗口故意隐藏了这

        两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印

        出虚表中的函数。

        typedef void (*VFunc)();
        void Print_VFTable(VFunc table[])
        {
          int i = 0;
          //虚函数表本质是一个存虚函数指针的指针数组,
          //一般情况这个数组最后面放了一个nullptr(仅仅针对VS系类编译器)。
          while (table[i])
          {
            printf("[%d]:%p--->", i+1, table[i]);
            table[i]();
            i++;
          }
        }
        int main()
        {
          Base b;
          Derive d;
            //取出Derive对象的前四个字节,强转成VFunc*,即函数二级指针类型
          Print_VFTable((VFunc*)(*((int*)(&d))));
          return 0;
        }

        image.gif

        image.gif编辑

        不难看出这里应证了我们的猜想,Derive::Func4()只是被监视窗口隐藏了。

        image.gif编辑

        2. 多继承中的虚函数表

        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;
        };
        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;
          VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
          PrintVTable(vTableb1);
          VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
          PrintVTable(vTableb2);
          return 0;
        }

        image.gif

        image.gif编辑

        注意一个现象:

        image.gif编辑

        image.gif编辑

        image.gif编辑

        b2->func1();

        image.gif

        程序在执行这句的时候,汇编多次call,和jmp,说明编译器堆底层进行了了多次封装,其中封装的原因是:

        image.gif编辑

        ecx 寄存器存储的是this指针,所以这次封装的并且sub - 8,是为了修正this指针。

        相关文章
        |
        2月前
        |
        编译器 程序员 定位技术
        C++ 20新特性之Concepts
        在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
        128 59
        |
        28天前
        |
        安全 编译器 C++
        【C++11】新特性
        `C++11`是2011年发布的`C++`重要版本,引入了约140个新特性和600个缺陷修复。其中,列表初始化(List Initialization)提供了一种更统一、更灵活和更安全的初始化方式,支持内置类型和满足特定条件的自定义类型。此外,`C++11`还引入了`auto`关键字用于自动类型推导,简化了复杂类型的声明,提高了代码的可读性和可维护性。`decltype`则用于根据表达式推导类型,增强了编译时类型检查的能力,特别适用于模板和泛型编程。
        22 2
        |
        28天前
        |
        存储 编译器 数据安全/隐私保护
        【C++】多态
        多态是面向对象编程中的重要特性,允许通过基类引用调用派生类的具体方法,实现代码的灵活性和扩展性。其核心机制包括虚函数、动态绑定及继承。通过声明虚函数并让派生类重写这些函数,可以在运行时决定具体调用哪个版本的方法。此外,多态还涉及虚函数表(vtable)的使用,其中存储了虚函数的指针,确保调用正确的实现。为了防止资源泄露,基类的析构函数应声明为虚函数。多态的底层实现涉及对象内部的虚函数表指针,指向特定于类的虚函数表,支持动态方法解析。
        32 1
        |
        2月前
        |
        编译器 C++
        C++入门12——详解多态1
        C++入门12——详解多态1
        47 2
        C++入门12——详解多态1
        |
        2月前
        |
        安全 程序员 编译器
        【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
        【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
        90 11
        |
        2月前
        |
        C++
        C++入门13——详解多态2
        C++入门13——详解多态2
        88 1
        |
        2月前
        |
        存储 编译器 C++
        【C++】面向对象编程的三大特性:深入解析多态机制(三)
        【C++】面向对象编程的三大特性:深入解析多态机制
        |
        2月前
        |
        存储 编译器 C++
        【C++】面向对象编程的三大特性:深入解析多态机制(二)
        【C++】面向对象编程的三大特性:深入解析多态机制
        |
        2月前
        |
        C++
        C++ 20新特性之结构化绑定
        在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
        43 0
        |
        26天前
        |
        存储 编译器 C语言
        【c++丨STL】string类的使用
        本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
        42 2