【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++下测试也是在代码段。


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


动态绑定和静态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  • 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
相关文章
|
25天前
|
存储 编译器 数据安全/隐私保护
【C++】多态
多态是面向对象编程中的重要特性,允许通过基类引用调用派生类的具体方法,实现代码的灵活性和扩展性。其核心机制包括虚函数、动态绑定及继承。通过声明虚函数并让派生类重写这些函数,可以在运行时决定具体调用哪个版本的方法。此外,多态还涉及虚函数表(vtable)的使用,其中存储了虚函数的指针,确保调用正确的实现。为了防止资源泄露,基类的析构函数应声明为虚函数。多态的底层实现涉及对象内部的虚函数表指针,指向特定于类的虚函数表,支持动态方法解析。
31 1
|
2月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
47 2
C++入门12——详解多态1
|
7月前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
169 1
|
2月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
87 1
|
4月前
|
存储 编译器 C++
|
5月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
57 1
【C++】深度解剖多态(下)
|
5月前
|
存储 编译器 C++
|
5月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
5月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
58 2
|
5月前
|
程序员 C++
【C++】揭开C++多态的神秘面纱
【C++】揭开C++多态的神秘面纱