【CPP】类与多态

目录

      • [15 类与多态](#15 类与多态)
        • [15.1 如何理解多态](#15.1 如何理解多态)
        • [15.2 动态绑定](#15.2 动态绑定)
        • [15.3 虚函数及重写的生效方式](#15.3 虚函数及重写的生效方式)
        • [15.4 动态多态的生效方式](#15.4 动态多态的生效方式)
        • [15.5 坑](#15.5 坑)
        • [15.6 协变](#15.6 协变)
        • [15.7 析构函数的重写](#15.7 析构函数的重写)
        • [15.8 CPP11新增关键字](#15.8 CPP11新增关键字)
          • [15.8.1 `override`](#15.8.1 override)
          • [15.8.2 `final`](#15.8.2 final)
        • [15.8 纯虚函数与抽象类](#15.8 纯虚函数与抽象类)
        • [15.9 虚函数表指针和虚函数表](#15.9 虚函数表指针和虚函数表)
          • [15.9.1 粗看虚函数表指针和虚函数表](#15.9.1 粗看虚函数表指针和虚函数表)
          • [15.9.2 虚函数表指针和虚函数表的一些细节](#15.9.2 虚函数表指针和虚函数表的一些细节)
        • [15.2 进一步理解多态(静态多态和动态多态)](#15.2 进一步理解多态(静态多态和动态多态))

这里是oldking呐呐,感谢阅读口牙!先赞后看,养成习惯!

个人主页:oldking呐呐

专栏主页:深入CPP语法口牙

15 类与多态

15.1 如何理解多态
  • 我们举个简单的例子,我们可以定义一个类:"人","人"里有个方法是吃,同时定义一个人是否有胃病,要知道,正常情况下,人类的吃吃喝喝都没啥问题,一旦这个人有肠胃炎啥的,那生冷辣咸的东西可能就不能吃了

  • 接着,我们再定义一个食物作为父类,冰淇淋作为食物的子类,再定义一个鸡汤,同样作为食物的子类

    class Food
    {
    public:
    Food(int calorie = 0) :_calorie(calorie) {}

      //这里是虚函数,我们在下一小节再细讲
      virtual void digest(bool stomach_trouble)
      {
      	cout << "OK" << endl;
      }
    

    protected:
    int _calorie;
    };

    class Ice_cream : public Food
    {
    public:
    Ice_cream(int calorie = 0) :Food(calorie) {}

      //覆写了父类的方法
      void digest(bool stomach_trouble)
      {
      	if (stomach_trouble)
      	{
      		cout << "not OK" << endl;
      	}
      	else
      	{
      		cout << "OK" << endl;
      	}
      }
    

    };

    class Chicken_soup : public Food
    {
    public:
    Chicken_soup(int calorie = 0) :Food(calorie) {}

      //覆写了父类的方法
      void digest(bool stomach_trouble)
      {
      	cout << "OK" << endl;
      }
    

    };

    class Person
    {
    public:
    Person(bool stomach_trouble = false) :_stomach_trouble(stomach_trouble) {}

      //食物这么多,我们不可能单独进行定义,这样太浪费时间了
      //所以我们把食物作为一个父类,我们只需要传子类具体的食物进来就可以了
      //记不记得咱之前提到过,父类类型的引用可以引用子类中父类的部分,这里就用到了
      void eat(Food& food) { food.digest(_stomach_trouble); }
    

    protected:
    bool _stomach_trouble;
    };

    int main()
    {
    Person p1(false);
    Person p2(true);

      Ice_cream ice_cream(1000);
      Chicken_soup chicken_soup(2000);
    
      p1.eat(ice_cream);
      p2.eat(ice_cream);
    
      cout << endl;
    
      p1.eat(chicken_soup);
      p2.eat(chicken_soup);
    
      return 0;
    

    }

    //输出:
    /*
    OK
    not OK

    OK
    OK
    */

  • 食物一般被吃下去,即感到饱腹,满足,所以在父类Food中我们向上面的例子一样定义函数,但如果根据情况,不是所有食物在任何情况下吃下去都会有这种感觉,所以在某些食物中,我们覆写了父类Food的方法,类似于冰淇淋

  • 覆写的概念很重要,从字面意思咱就知道,子类的情况可能会和父类不太一样,此时我们就可以覆写掉父类的方法,改成我们想让子类所呈现的模式,当然,这里的覆写在底层的角度上其实说不上是覆写,这个后面的小节会提到

  • 所以,简单说,从代码层面说,多态就是允许将不同的参数传入相同的函数而产生不同的结果(这句话是片面的),用于模拟生活中的各种情况

  • 例如典中典的火车票的例子,设计一个自助售票函数,设计一个父类为乘客,设计一个子类继承自父类为学生,再设计一个子类继承自父类为军人,每个种乘客类型可以买不同价格的票,当这类对象作为参数(传引用/指针)传进一个为父类"乘客"的自助售票函数中,根据函数判断生成符合当前乘客身份的票

  • 如果没有get到的话我们可以接着看下面的章节,感受一下多态的思想

15.2 动态绑定
  • 如上例中,函数eat实际上是调用了子类中的函数实现的不同效果
  • 事实上,函数eat通过子类的父类型的引用所调用的函数,编译时不会确定下来,因为具体是哪个函数是不确定的,只有当运行时才会被调用,我们称这种情况为动态绑定
15.3 虚函数及重写的生效方式
  • 虚函数即在父类中允许子类进行重写的函数(也可以指子类中被重写过的新的函数),还允许在函数中通过类型父类但指向子类的指针或引用直接访问

  • 我们接着看上一小节的例子

    class Food
    {
    public:
    Food(int calorie = 0) :_calorie(calorie) {}

      virtual void digest(bool stomach_trouble)
      {
      	cout << "OK" << endl;
      }
    

    protected:
    int _calorie;
    };

    class Ice_cream : public Food
    {
    public:
    Ice_cream(int calorie = 0) :Food(calorie) {}

      //注意这里的override,这个关键字在CPP11被引入
      //本身含义不大,但可以提高代码的可读性,告诉程序员这个函数是被覆写的
      //还可以检查一下函数名写错的小漏洞
      void digest(bool stomach_trouble) override
      {
      	if (stomach_trouble)
      	{
      		cout << "not OK" << endl;
      	}
      	else
      	{
      		cout << "OK" << endl;
      	}
      }
    

    };

    class Chicken_soup : public Food
    {
    public:
    Chicken_soup(int calorie = 0) :Food(calorie) {}

      void digest(bool stomach_trouble) override
      {
      	cout << "OK" << endl;
      }
    

    };

    class Person
    {
    public:
    Person(bool stomach_trouble = false) :_stomach_trouble(stomach_trouble) {}

      void eat(Food& food) { food.digest(_stomach_trouble); }
      //在这里,虽然引用的类型是父类,但实际上还是指向的子类,因此能够调用子类重写过的函数
    

    protected:
    bool _stomach_trouble;
    };

  • 我们在父类定义虚函数,考虑到子类可能会在某些地方与父类不同而并不是简单的在父类的基础上进行扩充,就可以在子类中重写这个方法

  • 不难看出,重写的生效方式非常简单

    1. 首先,被重写的函数必须是父类的虚函数(加virtual)
    2. 子类中覆写父类的函数必须在返回值,函数名,参数类型及其数量(参数名不做要求)都与父类中被重写的虚函数相同

Ps: 子类中覆写父类的虚函数可以不加virtualoverride,当然我一般选择virtualoverride二选一加上以保持一定的可读性

15.4 动态多态的生效方式
  • 动态多态我们可以暂时的理解为形如以上的例子的调用方式,需要满足两个重要条件

    1. 通过类型为父类但指向子类的指针或引用来调用子类中的虚函数
    2. 子类中的虚函数需要是覆写过父类的虚函数
15.5 坑
class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};

class B : public A
{
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};

int main()
{
	B* p = new B;
	p->test();
	return 0;
}
  • Q: 输出什么?

  • 分析

    class A
    {
    public:
    virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
    //注意注意!!!这里的test()的this指针的类型一定是A*
    virtual void test() { func(); }
    };

    //B继承自A
    class B : public A
    {
    //虽然B中没有virtual,缺省值也不一样,但依旧构成覆写,但是,覆写后的函数的函数头还是覆写前的函数头,哪怕是缺省值也是覆写前的
    //这就是为什么覆写后的函数头可以不加virtual,因为直接沿用了覆写前的函数头
    //且不管加不加,一定会沿用覆写前的函数头,这就导致了缺省值都和覆写前一样(可以理解为覆写仅仅只是覆写函数体而不包括函数头!)
    void func(int val = 0) { std::cout << "B->" << val << std::endl; }
    };

    int main()
    {
    //这里类型为B的p指向的是B对象
    B
    p = new B;
    //因为这里p的类型是B*,所以这里传进去的this指针的类型也是B*,由test中类型为A*的this指针接收,所以调用B::func()
    p->test();

      //如果是以下这种情况,就不会构成多态了,
    
      return 0;
    

    }

  • 综上,输出了一个完全不像是正确答案的答案,即B->1

15.6 协变
  • 协变允许子类虚函数的返回值不跟随父类,但要求是返回指向子类的指针/引用(可以不是当前类,可以是其他)

  • 同时也允许父类虚函数返回指向父类的指针/引用(同样可以是其他类)

    class A
    {
    public:
    virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
    virtual void test() { func(); }
    };

    class B : public A
    {
    public:
    void func(int val = 0) { std::cout << "B->" << val << std::endl; }
    };

    class C
    {
    public:
    virtual A& func1(int val) {}
    };

    class D : public C
    {
    public:
    B& func1(int val) override {}
    };

15.7 析构函数的重写
  • 这俩类的设计乍一看可能没什么问题,正常调用的话也是先调用~B()然后自动调用~A()

    class A
    {
    public:
    A(int a = 0) :_a(a) {}

      ~A()
      {
      	cout << "~A()" << endl;
      }
    

    protected:
    int _a;
    };

    class B : public A
    {
    public:
    B() :_b(new int) {}

      ~B()
      {
      	cout << "~B()" << endl;
      	delete _b;
      }
    

    protected:
    int* _b;
    };

  • 但有一种调用情况会出现极大的问题

    int main()
    {
    //我们仔细看,这里pb指向的内容开辟在堆上,且是一个子类
    //将这个在子类的指针传给父类指针,实际上是指向的子类对象的父类部分
    A* pb = new B;

      //我们假设中间要做某些操作	
      //...
    
      //最终我们想要释放pb所指向的空间的时候
      //由于子类和父类的析构函数并未构成重载
      //导致指向子类的父类类型的指针只能调用到父类的析构函数
      //这就导致子类的一个开辟在堆上的成员函数无法释放了
      //最终造成了内存泄漏
      delete pb;
    
      return 0;
    

    }

  • 所以就设计出了析构函数的重写

    class A
    {
    public:
    A(int a = 0) :_a(a) {}

      virtual ~A()
      {
      	cout << "~A()" << endl;
      }
    

    protected:
    int _a;
    };

    class B : public A
    {
    public:
    B() :_b(new int) {}

      //在调用了子类的析构之后,由于析构默认的特性,又会去调用父类的析构,以保证释放时先子后父的原则
      ~B() override
      {
      	cout << "~B()" << endl;
      	delete _b;
      }
    

    protected:
    int* _b;
    };

    int main()
    {
    A* pa = new A;
    A* pb = new B;

      delete pa;
      delete pb;
    
      return 0;
    

    }

  • 析构函数重写的独特之处在于,不要求其函数名相同(其实本质上析构函数的函数名会在编译的过程中改成destructor,所以本质上函数名是相同的)

  • 换句话说,正是存在这个特殊的场景,所以才会需要把析构的函数名改成destructor以覆写

15.8 CPP11新增关键字
15.8.1 override
  • 可以检测一些小bug,验证是否构成重写,同时增加可读性,前面的例子有演示,就不再提了
15.8.2 final
  • 如果不想让子类重写这个函数,可以加上final关键字
15.8 纯虚函数与抽象类
  • 生活中可以实例化出具体的对象的东西有很多,但往往有一些名词在某些场景不适合实例化出具体的东西,比如说"汽车"这个名词,放在二手车网站上就不太合适,因为这是一个大的概念而非具体的车型和型号

  • 所以我们就可以定义出纯虚函数和抽象类

    //这就是一个抽象类
    class Car
    {
    public:
    //这就是一个纯虚函数
    virtual void drive() = 0;
    };

    class BMW : public Car
    {
    public:
    void drive() override
    {
    cout << "操控型" << endl;
    }
    };

    class Benz : public Car
    {
    public:
    void drive() override
    {
    cout << "舒适型" << endl;
    }
    };

    int main()
    {
    //不可实例化抽象类
    //Car c;

      return 0;
    

    }

Ps:如果在子类不重写父类的虚函数,那子类也会变成抽象类

15.9 虚函数表指针和虚函数表
15.9.1 粗看虚函数表指针和虚函数表
  • 前面我们说过子类可以覆写父类的虚函数,如何证明这一点呢?

  • 我们可以将全部食物类全部实例化看一下

    int main()
    {
    Person p1(false);
    Person p2(true);

      //将所有食物类全部实例化
      Food food(500);
      Ice_cream ice_cream(1000);
      Chicken_soup chicken_soup(2000);
    
      p1.eat(ice_cream);
      p2.eat(ice_cream);
    
      cout << endl;
    
      p1.eat(chicken_soup);
      p2.eat(chicken_soup);
    
      return 0;
    

    }

  • 我们观察一下这个调试窗口,不难发现这个父类的区域中竟然有一个类型为void**名为_vfptr的指针

  • 可以看到,这里虽然子类中都包含有父类的部分,但父类的部分中,digest方法的类域竟然是子类的类域

  • 意味着虽然都是父类的区域,但不同的子类类型,这个指针指向的区域不一样

  • 我们再在这基础上看看

    // X86 !!!!

    class Food
    {
    public:
    Food(int calorie = 0) :_calorie(calorie) {}

      virtual void digest(bool stomach_trouble)
      {
      	cout << "OK" << endl;
      }
    

    protected:
    int _calorie;
    };

    class Ice_cream : public Food
    {
    public:
    Ice_cream(int calorie = 0) :Food(calorie) {}

      void digest(bool stomach_trouble)
      {
      	if (stomach_trouble)
      	{
      		cout << "not OK" << endl;
      	}
      	else
      	{
      		cout << "OK" << endl;
      	}
      }
    

    };

    class Chicken_soup : public Food
    {
    public:
    Chicken_soup(int calorie = 0) :Food(calorie) {}

      void digest(bool stomach_trouble)
      {
      	cout << "OK" << endl;
      }
    

    };

    class Person
    {
    public:
    Person(bool stomach_trouble = false) :_stomach_trouble(stomach_trouble) {}

      void eat(Food& food) { food.digest(_stomach_trouble); }
    

    protected:
    bool _stomach_trouble;
    };

    int main()
    {
    Person p1(false);
    Person p2(true);

      Ice_cream ice_cream(1000);
      Chicken_soup chicken_soup(2000);
    
      cout << sizeof(Ice_cream) << endl;
      cout << sizeof(Chicken_soup) << endl;
      cout << sizeof(Food) << endl;
    
      return 0;
    

    }

    //输出:
    //8
    //8
    //8

  • 按理讲,不管是父类还是子类,因为只包含一个bool类型的成员变量,其大小在对齐之后应该都是4才对,为什么会是8呢?

  • 不妨与这个_vfptr指针联想一下,不难猜出这多的4字节就是这个_vfptr指针

  • 这个指针我们称为虚函数表指针,简而言之就是指向一个表的指针

  • 我们对上面的例子做一下改进,方便理解虚函数表

    class Food
    {
    public:
    Food(int calorie = 0) :_calorie(calorie) {}

      virtual void digest(bool stomach_trouble)
      {
      	cout << "OK" << endl;
      }
    
      virtual void func() { cout << "Food::func()" << endl; }
    

    protected:
    int _calorie;
    };

    class Ice_cream : public Food
    {
    public:
    Ice_cream(int calorie = 0) :Food(calorie) {}

      void digest(bool stomach_trouble) override
      {
      	if (stomach_trouble)
      	{
      		cout << "not OK" << endl;
      	}
      	else
      	{
      		cout << "OK" << endl;
      	}
      }
    
      void func() override { cout << "Ice_cream::func()" << endl; }
    

    };

    class Chicken_soup : public Food
    {
    public:
    Chicken_soup(int calorie = 0) :Food(calorie) {}

      void digest(bool stomach_trouble) override
      {
      	cout << "OK" << endl;
      }
    
      void func() override { cout << "Chicken_soup::func()" << endl; }
    

    };

  • 我们在每个类中都加了一个函数,从监视中可以看到,每个类都有一个自己的虚函数表

  • 这意味着,父类指针依旧指向子类中父类的部分,只不过编译器用某种方法把__vfptr的指针改到对象自己的虚函数表上了

  • 虚函数表和虚函数表指针是在对象被创建时就生成的,编译器会把重写过的函数的指针通通塞到表里,等到要用的时候只需要在此对象的虚函数表里找具体的函数

  • 我们知道,函数实际并不会存放到类的区域中,而是会存放到代码段中,一旦想要类调用此函数,在汇编层面就是直接call到相应的地址去
15.9.2 虚函数表指针和虚函数表的一些细节
  • 编译器编译时,根据父类生成虚函数表,子类会直接拷贝(继承)父类的虚函数表给子类用来放虚函数表的空间

  • 然后编译器会检查子类有没有重写父类的虚函数,如果有,就用重写的函数指针替换掉虚函数表中对应的父类的被重写的函数的函数指针

  • 最后检查子类中有没有单独定义虚函数,如果有,也一并塞到虚函数表里

  • 所以我们可以说,一种类型对象的虚函数表可以包含有从父类继承下来的虚函数,也有重写了父类的虚函数,也有父类没有,但在自己这边定义的虚函数

  • Ps1:同种类型的对象指向的虚函数表是一样的,不会额外再开空间放虚函数表,不同类型的对象的虚函数表不同(子类和父类就不会是同一个虚函数表)

  • Ps2:VS下的虚函数表的末尾会存一个空指针,g++就没有

  • Ps3(重要):虚函数存在在代码段,但虚函数表存在哪里没有规定,VS下也是存在代码段

15.2 进一步理解多态(静态多态和动态多态)
  • 多态多态,即同一类型事物的多种形态,官方一点的说法是,为不同的数据类型提供统一的接口

  • 静态多态的体现,可以是函数的重载,也可以是函数模板的实例化

    • 关于函数的重载,我定义多个同名的函数,其中构成函数重载,虽然调用的函数名相同,但我可以传入不同的参数达到不同的结果,即为多数据类型的传入提供统一的接口
    • 函数模板也是同理,我根据函数模板生成的函数,函数名(接口名)相同,但数据类型可以多种多样
    • 而是否静态的体现,则在于所调用的函数在编译之后是否是确定的,在函数重载和函数模板的例子中,编译生成了机器码之后,实际所需要调用的函数是被确定的,可以是重载中已经被程序员显式定义出来了,也可以是编译器根据函数模板生成的函数,这些都在编译完之后直接被确定下来了
  • 动态多态的体现,一般体现在函数通过统一的父类接口,以达成不同对象传入导致不同结果的设计

  • 这种设计由于虚函数表的存在,导致编译器不会在设定call的函数地址的时候直接找到具体的函数地址,而是程序运行之后再在虚函数表中查找所需函数

相关推荐
学会沉淀。3 分钟前
Docker学习
java·开发语言·学习
ragnwang20 分钟前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习
西猫雷婶33 分钟前
python学opencv|读取图像(二十一)使用cv2.circle()绘制圆形进阶
开发语言·python·opencv
kiiila34 分钟前
【Qt】对象树(生命周期管理)和字符集(cout打印乱码问题)
开发语言·qt
小_太_阳1 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾1 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
唐 城1 小时前
curl 放弃对 Hyper Rust HTTP 后端的支持
开发语言·http·rust
码银3 小时前
【python】银行客户流失预测预处理部分,独热编码·标签编码·数据离散化处理·数据筛选·数据分割
开发语言·python
从善若水3 小时前
【2024】Merry Christmas!一起用Rust绘制一颗圣诞树吧
开发语言·后端·rust
lqqjuly3 小时前
特殊的“Undefined Reference xxx“编译错误
c语言·c++