初阶后的C++ 第七节 —— 多态

简介: 注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

目录


一、多态的概念


二、虚函数的重写(覆盖):


三、C++11中的override 和 final


关键字override:


关键字:final


对比:重载、覆盖(重写)、隐藏(重定义)


四、抽象类


概念


接口继承和实现继承


五、多态的原理


虚函数表:


具体实现多态的原理:


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


声明:


接下来所有举的例子,都是在x86 32位机器平台下的程序中实现的。如果是在64位平台下,需要考虑指针是8个字节等的问题。


一、多态的概念

通俗的来说,多态,就是多种形态。


具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。


我们一般情况下,可以这样来分类:


1、静态的多态。


2、动态的多态。


静态的多态实际上很简单,就是函数重载。我们在这里不再做过多的赘述。


对于动态的多态,也叫做运行时的多态。


对于动态的多态,必须或者说至少需要满足两个条件:


①子类继承父类,完成虚函数的重写。


②父类的指针或者引用去调用这个重写的虚函数。


得到的效果就是:


如果父类的指针或引用指向父类对象,调用的就是父类的虚函数。


如果父类的指针或引用指向子类对象,其调用的就是子类的虚函数。


换句话来说,调用函数跟对象有关,指向哪个对象,就调用哪个虚函数。


就好比,在抽红包的时候,每个人都是不同的个体,每个人抽到的数额都是不相同的


关于虚函数是个什么东西、还有上面一坨话又是什么意思,我们接下来会详细介绍,等介绍完各位自然就明白了。


首先来说说,什么叫做虚函数。


很简单,被virtual修饰的类成员函数称为虚函数。


注意,其修饰的必须并且只能是成员函数!不能随随便便对一个全局的函数都可以加virtual。


二、虚函数的重写(覆盖):

派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),


称子类的虚函数重写了基类的虚函数。


并且都要在函数的前面加上关键字virtual


举个例子:

#include<iostream>
using namespace std;
class Person
{
public:
  virtual void func()
  {
  cout << "i am a person" << endl;
  }
};
class Student : public Person
{
public:
  virtual void func()                         
  {
  cout << "i am a Student" << endl;
  }
};
class Slodier : public Person
{
public:
  virtual void func()         //必须要是用virtual修饰的虚函数 ,并且还要重写
  {
  cout << "i am a Slodier" << endl;
  }
};
void func(Person& a)     
{
  a.func();
}
int main()
{
  Person pp;
  Student ss;
  pp.func();
  ss.func();
  return 0;
}




同理,如果我实例化Slodier的类,然后去调用func函数,得到的就是“i am a Slodier”.


需要注意的是,上面所说的在派生类中和基类的函数“完全一样”,实际上是有两种特殊情况的。也就是说,其有两个例外:


①一个是协变:


就是基类与派生类虚函数返回值类型不同


派生类重写基类虚函数时,与基类虚函数返回值类型不同。


即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,


称为协变。


同样,我们来举一个例子:

class A {};
class B : public A {};
class Person {
public:
  virtual A* f() { return new A; }  //返回值为A*,其返回类型为基类对象
};
class Student : public Person {
public:
  virtual B* f() { return new B; }  //返回值为B*,其返回类型为派生类对象
};


②另外一个,是析构函数的重写(就是说基类与派生类析构函数的名字不同)


再举一个例子:

#include<iostream>
using namespace std;
class Person
{
public:
  virtual void func()
  {
  cout << "i am a person" << endl;
  }
  virtual ~Person()
  {
  cout << "~Person()" << endl;
  }
};
class Student : public Person
{
public:
  virtual void func()                         
  {
  cout << "i am a Student" << endl;
  }
  virtual ~Student()
  {
  cout << "~Student()" << endl;
  }
};
class Slodier : public Person
{
public:
  virtual void func()         //必须要是用virtual修饰的虚函数 ,并且还要重写
  {
  cout << "i am a Slodier" << endl;
  }
  virtual ~Slodier()
  {
  cout << "~Slodier()" << endl;
  }
};
void func(Person& a)     
{
  a.func();
}
int main()
{
  Person* pp = new Person;
  Student* ss = new Student;
  Slodier* s = new Slodier;
  delete pp;
  delete ss;
  delete s;
  return 0;
  }


如上述代码所示,这里的所有的析构函数构成了重写。


为什么可以这样呢?我们可以这样来理解:为了更好地使类在析构的时候,满足先进后出的栈的出入原则,编译器在编译之后,会将所有的析构函数统一变成destructor函数。


在这里,我们想来强调一下:


在析构函数中,如果有像上述的继承关系,那么最好在前面加上virtual,使所有的析构函数构成多态。


因为如果不这么做,在进行切片之后,派生类中的析构函数可能无法完成调用。就可能会造成内存泄漏。


还有一点,就是virtual可以只在基类的析构函数前面加一次就可以了,编译器默认后面的是继承下来的,即使派生类的析构函数不加virtual,仍然可以与基类的析构函数构成重写。


三、C++11中的override 和 final

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序

写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。


关键字override:

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


举一个简单的例子:

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


如上所示,这里在函数的末尾加上了一个override。


这里如果没有用virtual构成重写,那么编译器就会抱错。各位可以试一下。


关键字:final

其修饰虚函数,表示该虚函数不能再被继承


现在,我们来:


对比:重载、覆盖(重写)、隐藏(重定义)

重载:两个函数在同一个作用域中。它们的函数名、参数相同。(参数类型不完全相同)


覆盖(重写):两个函数分别在基类和派生类的作用域;函数名、返回值、参数等必须相同(协变除外);两个函数必须都要是虚函数。


重定义(隐藏):只要函数名相同,并且两个函数分别在基类和派生类中,它们不构成重写,那么就是重定义(隐藏)。


四、抽象类

概念

在虚函数的后面写上 =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;
}



接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现


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


下面,重点正式开始,如果对上面所说的还是糊里糊涂的,那么接下来的知识应该会让你豁然开朗。


五、多态的原理

虚函数表:

我们先来看这样一个题:



就想问:sizeof(Car)是多少?

class Car
{
public:
  virtual void Drive() {}
private:
  int _a;
};
//求siezeof(Car)?


我们还是先来看一下答案吧:



(再次强调一下,我们这里的环境是x86,不是x64,64位平台下可能数据和这里不一样,因为要考虑int 和指针都是8个字节的问题,但是原理是一样的)


为什么?


为什么答案是8?


我们打开我们的监视窗口来看一下:

我们可以看到,这c里面有两个东西。


其中,叫_vfptr的是一个指针,二级指针。


更准确的来说,其是一个指针数组的首元素地址。而虚函数表就是一个指针数组,其末尾以nullptr收尾。


这里的_vfptr指向的正是虚函数的虚表。(v可以理解为virtual,f理解为function)


在虚表中,存储着虚函数的地址。


画图理解成:


image.png



我们可以再来看看下面的代码:

微信图片_20221210115109.png


我们会发现,b中的虚函数表里面有两个函数的指针,即加上virtual的函数。


在d 中的虚函数表中仅有一个虚函数的指针。


在继承的时候, 过程是这个样子的:


将虚表先继承下来,如果对于虚函数进行了重写,那么就将虚表中的地址进行修改,修改成新的函数的地址。


而这里的派生类我们只看到了一个函数的地址可能是由于我们的编译器进行了优化。如果我们想将其打印出来,也是可以做到的:

我们可以看到,只要是用virtual修饰了,那么在虚表中就会存在属于自己的位置。


而我们将Func1进行了重写,而Func2用virtual修饰,但是并未重写,在继承下来的之后,在派生类的虚表中该位置的地址是不改变的(经编译器优化我们看不见)。只有当重写时,派生类所对应的地址才会发生相应的改变。


结合上面的例子,我们现在来稍微总结一下吧,关于虚函数表:


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


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


重写是语法的叫法,覆盖是原理层的叫法。


3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。


4. 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。


5. 总结一下派生类的虚表生成:


a.先将基类中的虚表内容拷贝一份到派生类虚表中


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


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


6. 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的?


答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。


注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。


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


具体实现多态的原理:

我们在之前,强调过这样句话:


必须要用父类的指针或者引用去调用这个重写的虚函数。 不能直接用对象去调用。


这是为什么?


我们来看:



image.png

Person和Student都有属于自己的虚表。


当我们用引用或者指针去调用,那么其经过切片切割之后,剩下所得到的虚函数表是自己的虚表。


而当我们用对象去调用的话,所传得到的对象的虚表就是父类的虚表了。


就像这样(如下图:)

image.png



如果还要解释,我们可以用画图的方式来理解:


(传谁用谁)

image.png



从汇编的角度来分析:


完成重写的虚函数调用(多态调用):

image.png



下面是普通函数的调用:

image.png



所以,可以看到,虚函数是在程序运行起来之后,找到了p的地址,再去运行的。


而普通函数是在编译的时候 就已经确定了函数的地址,然后直接去call调用。


这也证明了用虚函数是动态的多态。


动态绑定与静态绑定


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


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


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

关于单继承,我们在上面已经给出详细地讲解了,一句话,就是先复制过来,重写就覆盖。


我们现在重点来看多继承,尤其是上节我们说的菱形继承中的虚基表中留下了的一个关于多态的问题。


首先说一下,普通的多继承,如果继承了多个类,那么其就会存在多个虚表:(如下图)



我们上图的完整代码如下:

#include<bits/stdc++.h>  //这是一个万能头文件,需要配置一下才能用
using namespace std;
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(*Fun)();
void pr(Fun* ptr)
{
  for (int i = 0; ptr[i] != nullptr; i++)
  {
  printf("%d point:[%p]\n", i, ptr[i]);
  }
}
int main()
{
  Derive d;
  return 0;
}



那么对于上次所说的菱形继承中的虚表中的问题,如下:



这张图,是我们上节内容所说的。


代码:

#include<bits/stdc++.h>
using namespace std;
class A
{
public:
  virtual void func()
  {
  cout << "A func()" << endl;
  }
  int _a;
};
class B : virtual public A
{
public:
  virtual void func()
  {
  cout << "B func()" << endl;
  }
  int _b;
};
class C :virtual public A
{
public:
  virtual void func()
  {
  cout << "C func()" << endl;
  }
  int _c;
};
class D :public B, public C
{
public:
  virtual void func()
  {
  cout << "D func()" << endl;
  }
  int _d;
};
int main()
{
  D d;
  d._a = 1;
  d._b = 2;
  d._c = 3;
  d._d = 4;
  return 0;
}



可是,我们在这里重写的虚函数是父类中也有的,也就是说,A的虚表和B、C可以是同一张。


那如果B和C有自己的虚函数呢?


稍微改动一下,变成下面的:

#include<bits/stdc++.h>
using namespace std;
class A
{
public:
  virtual void func()
  {
  cout << "A func()" << endl;
  }
  int _a;
};
class B : virtual public A
{
public:
  virtual void func()
  {
  cout << "B func()" << endl;
  }
  virtual void func1()   //新加入
  {
  }
  int _b;
};
class C :virtual public A
{
public:
  virtual void func()
  {
  cout << "C func()" << endl;
  }
  int _c;
};
class D :public B, public C
{
public:
  virtual void func()
  {
  cout << "D func()" << endl;
  }
  virtual void func1()   //新加入
  {
  cout << endl;
  }
  int _d;
};
int main()
{
  D d;
  d._a = 1;
  d._b = 2;
  d._c = 3;
  d._d = 4;
  return 0;
}



(如上图所示)


B中所存储的内容又多了一项——即为自己的虚表指针。而在其虚基表中,fc ff ff ff 就是其偏移量的值。(即该位置距离 虚表指针 的值)


我们关于多态的有关知识,再总结一下:(实际上属于再重复一遍了)

image.png





目录
相关文章
|
25天前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
31 2
C++入门12——详解多态1
|
25天前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
67 1
|
3月前
|
存储 编译器 C++
|
4月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
52 1
【C++】深度解剖多态(下)
|
4月前
|
存储 编译器 C++
|
3月前
|
存储 编译器 C++
C++多态实现的原理:深入探索与实战应用
【8月更文挑战第21天】在C++的浩瀚宇宙中,多态性(Polymorphism)无疑是一颗璀璨的星辰,它赋予了程序高度的灵活性和可扩展性。多态允许我们通过基类指针或引用来调用派生类的成员函数,而具体调用哪个函数则取决于指针或引用所指向的对象的实际类型。本文将深入探讨C++多态实现的原理,并结合工作学习中的实际案例,分享其技术干货。
68 0
|
4月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
4月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
52 2
|
4月前
|
程序员 C++
【C++】揭开C++多态的神秘面纱
【C++】揭开C++多态的神秘面纱
|
4月前
|
机器学习/深度学习 PyTorch 算法框架/工具
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型