【C++】多态(中)

简介: 【C++】多态(中)

3.多态构成的条件

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,而且派生类必须对虚函数进行重写
class person
{
public:
  virtual void BuyTicket() { cout << "person::买票-全价" << endl; }
};
class student : public person
{
public:
  virtual void BuyTicket() { cout << "student::买票-半价" << endl; }
};
class soldier : public person
{
public:
  virtual void BuyTicket() { cout << "soldier::买票-优先" << endl; }
};
void func(person& p)
{
  p.BuyTicket();
}
void Test2()
{
  person pn;
  student st;
  soldier sr;
  func(pn);
  func(st);
  func(sr);
}

19de1c86b2986755a57b0e786b136541.png

可以看到,三个不同类对象调用同一个函数,最终执行的结果不同,这就是多态

当没有通过基类的指针或者引用调用时:不构成多态

57986e6f5734ed5427cdc94c73fb78a2.png

当没有虚函数重写的时候:不构成多态

666317e5dc85fe752fb321fbc648e1d0.png


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

重载:两个在同一作用域的函数,其函数名相同且参数不同,构成函数重载

重定义(隐藏):两个分别在基类和派生类中的同名函数,构成隐藏

重写(覆盖):两个分别在基类和派生类中的函数名、返回值、参数列表都相同虚函数构成重写(协变例外)


3. 抽象类


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

class person
{
public:
  virtual void BuyTicket() = 0 { cout << "person::买票-全价" << endl; }
};
class student : public person
{
public:
  virtual void BuyTicket(int a) { cout << "student::买票-半价" << endl; }
};
void Test3()
{
  person pn;
}

7f8ac7f95334c02df9e33a567fb5a339.png


接口继承与实现继承

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


4. 多态的原理


首先,我们来看一道题

//(在x86环境下)这里sizeof(Base)是多少
class Base
{
public:
  virtual void func() { cout << "func" << endl; }
private:
  int _b = 1;
};


7c3df7082cd2959227426b5b55cb243f.png

可以看到,结果是8个字节,这是为什么?一个Base对象中不是只有一个_b成员吗?我们来看一下监视窗口

7e417073292504010ac4a156a96abbc6.png

可以发现,在一个Base对象中,除了_b成员之外,还有一个_vfptr(在VS2022平台下是放在前面的),看类型可知这是一个指针。这个指针我们叫他虚函数表指针(其中v表示virtual,f表示function)。一个含有虚函数的类至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表(也叫虚表)中。


现在我们来改写一下这个代码

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


现在,我们通过监视窗口来观察一下b和d对象

586dfd7af61a9a1e4224fd6b02d1e7fb.png


有以下几点发现:


  1. 可以看到d对象继承了Base类中的成员,所以也就理所当然的继承了一个虚表指针
  2. 在代码中,我们让Func1完成了重写的过程,所以看到d对象中的虚表中的Func1是Drive中的Func1,也可以理解成覆盖,就是指虚表中的虚函数的覆盖。这里重写是语法层上的叫法,覆盖是原理层上的叫法。
  3. 由于在基类中,Func2也是虚函数,所以也放在虚表里面,但是在派生类中没有被重写,所以在b对象和d对象中虚表的Func2地址是相同的。

对上述现象的疑问与分析


✅虚函数表指针vfptr本质上是一个函数指针数组指针,这个指针指向了一个函数指针数组,这个数组的名字叫做虚函数表(虚表),表里面存放的是该类中的所有虚函数 ==> 类中的所有虚函数都会进入虚表


❓派生类中的虚表是怎么生成的?


✅首先,将基类中的虚表内容拷贝一份到派生类虚表中;然后,将派生类中所有的虚函数放进虚表中,如果发现其中有虚函数是重写基类中的,那就覆盖掉;最后,对于新增虚函数的顺序问题:按照在派生类中的声明顺序排列


✅在VS下测试,虚表以一个nullptr结尾。

❓虚表是存放在什么位置的?

✅我们不太清楚虚表存放的位置,那么现在用一个方法来测试一下,我们看下面一段代码:

void print()
{
  Base b;
  cout << (void*)(*(int*)&b) << endl;//拿到b对象的地址,强转成int*,拿到前四个字节的地址,解引用就是虚表的地址
}
void Test5()
{
  int a = 10;
  int* b = new int(20);
  static int c = 30;
  const char* d = "aaaa";
  cout << "栈:" << &a << endl;
  cout << "堆:" << b << endl;
  cout << "静态区:" << &c <<endl;
  cout << "代码段:" << (void*)d << endl;
  cout << "虚表地址";
  print();
}

85325e741dbb59774425fb07bb7e237d.png

可以看到虚表地址和代码段是最接近的,所以虚表是存放在代码段的

695f8802b12ddbf1cf72c338e90109d1.png

在g++下测试也是在代码段。


所以多态的原理就是在在类中定义了虚函数,然后在运行时虚函数会进入虚表中,在构造对象的时候,会构造一个函数指针数组指针,用于存放虚表的地址,这里的虚表本质上是一个函数指针数组,数组内存放的就是虚函数的地址。如果在派生类中对虚函数进行了重写,那么派生类对象中的基类成员的虚表中对应的地址会被重写之后的虚函数地址覆盖。此时通过切片得到的派生类对象指针或引用与基类对象指针或引用调用虚函数时,就会通过虚表去找到需要调用的虚函数,从而就实现了调用同一个函数名,却产生了不同的结果的情况,即多态行为。


动态绑定和静态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  • 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
相关文章
|
1月前
|
C++
C++多态实现计算器
C++多态实现计算器
|
2月前
|
C++
9. C++虚函数与多态
9. C++虚函数与多态
33 0
|
2月前
|
存储 编译器 C++
C++:多态究竟是什么?为何能成为面向对象的重要手段之一?
C++:多态究竟是什么?为何能成为面向对象的重要手段之一?
52 0
|
2月前
|
存储 安全 算法
【C/C++ 数据发送结构设计】C++中的高效数据发送:多态、类型擦除与更多解决方案
【C/C++ 数据发送结构设计】C++中的高效数据发送:多态、类型擦除与更多解决方案
79 0
|
19天前
|
编译器 C++
c++的学习之路:22、多态(1)
c++的学习之路:22、多态(1)
21 0
c++的学习之路:22、多态(1)
|
2月前
|
存储 编译器 C++
【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)
【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)
|
1天前
|
存储 C++
C++中的多态
C++中的多态
6 0
|
2天前
|
C++
【C++】从零开始认识多态(二)
面向对象技术(oop)的核心思想就是封装,继承和多态。通过之前的学习,我们了解了什么是封装,什么是继承。 封装就是对将一些属性装载到一个类对象中,不受外界的影响,比如:洗衣机就是对洗衣服功能,甩干功能,漂洗功能等的封装,其功能不会受到外界的微波炉影响。 继承就是可以将类对象进行继承,派生类会继承基类的功能与属性,类似父与子的关系。比如水果和苹果,苹果就有水果的特性。
20 1
|
2天前
|
C++
【C++】从零开始认识多态(一)
面向对象技术(oop)的核心思想就是封装,继承和多态。通过之前的学习,我们了解了什么是封装,什么是继承。 封装就是对将一些属性装载到一个类对象中,不受外界的影响,比如:洗衣机就是对洗衣服功能,甩干功能,漂洗功能等的封装,其功能不会受到外界的微波炉影响。 继承就是可以将类对象进行继承,派生类会继承基类的功能与属性,类似父与子的关系。比如水果和苹果,苹果就有水果的特性。
18 4
存储 编译器 C++
10 2