彻底搞懂C++多态

彻底搞懂C++多态

基础

多态的条件

  • 有继承关系;
  • 有虚函数;
  • 父类指针或引用指向子类对象;

哪些函数不能是虚函数?

  • 构造函数:

    • 原因:虚函数表指针在构造函数中初始化。
  • 静态成员函数:

    • 原因:静态成员函数属于类,不属于类对象。而多态的机制和类对象息息相关。
  • 内联函数:

    • 原因:在编译期间,内联函数就会展开。

关于虚函数表指针和虚函数表

  • 虚函数表在编译期间就确定好了。
  • 虚函数表指针在构造函数中初始化。(运行时动态绑定)

面试问题

1. 纯虚函数在虚函数表中的形式

  • https://blog.csdn.net/qq_31788759/article/details/106225916
  • 有虚函数表项,但是可能为nullptr,只是起到占位作用,等待子类去重写它。

2. 从汇编角度解释虚函数机制

成员函数的地址是编译期间就确定了。所以我们在调用普通成员函数的时候,可以直接call 函数地址,实现函数调用。

而虚函数的话,有一套虚函数的调用机制。虚函数表的地址在编译期间就确认了,但是虚函数表指针是在类的构造函数中初始化的,和类对象息息相关,存在于类对象的开始8字节处。

在汇编的表示形式就是先取出对象的开始8个字节(也就是虚函数表指针),然后根据用户调用的是哪个虚函数,就加上偏移取到对应的虚函数然后调用。

#include <iostream>
#include <vector>
#include <string>
using namespace std;

class Animal
{
public:
  Animal(int age)
    : m_age(age)
  {
  }

  virtual void speak()
  {
  }

  virtual void eatFood()
  {
  }

  virtual void sleep()
  {
  }

  void printAge()
  {
    cout << m_age << endl;
  }

public:
  int m_age;
};

class Dog : public Animal
{
public:
  Dog(int age)
    : Animal(age)
  {
  }

  void speak() override
  {
    cout << "汪汪汪\n";
  }

  void eatFood() override
  {
    cout << "吃骨头\n";
  }

  void sleep() override
  {
    cout << "睡20分钟\n";
  }
};

int main()
{
  Animal *dogPtr = new Dog(10);
  dogPtr->printAge();
  dogPtr->speak();  // 0x0
  dogPtr->eatFood();  // 0x8
  dogPtr->sleep();  // 0x10
}
  • 普通函数:
 ► 0x555555555214 <main+43>    mov    rax, qword ptr [rbp - 0x18]
   0x555555555218 <main+47>    mov    rdi, rax
   0x55555555521b <main+50>    call   Animal::printAge()                <Animal::printAge()>


In file: /home/ubuntu/GithubFile/Code/CPP/objectmodel/virtual_function_test5.cpp
   65 
   66 int main()
   67 {
   68   Animal *dogPtr = new Dog(10);
 ► 69   dogPtr->printAge();
   70   dogPtr->speak();  // 0x0
   71   dogPtr->eatFood();  // 0x8
   72   dogPtr->sleep();  // 0x10
   73 }
  • 虚函数表的样子:
pwndbg> info vtbl dogPtr 
vtable for 'Animal' @ 0x555555557d18 (subobject @ 0x55555556aeb0):
[0]: 0x5555555553a2 <Dog::speak()>
[1]: 0x5555555553c8 <Dog::eatFood()>
[2]: 0x5555555553ee <Dog::sleep()>
  • 虚函数speak:
 ► 0x555555555220 <main+55>    mov    rax, qword ptr [rbp - 0x18]   <std::cout@@GLIBCXX_3.4>
   0x555555555224 <main+59>    mov    rax, qword ptr [rax]  # 取到虚函数表指针的地址
   0x555555555227 <main+62>    mov    rdx, qword ptr [rax]  # 取到需要调用的虚函数speak的地址放到rdx中
   0x55555555522a <main+65>    mov    rax, qword ptr [rbp - 0x18]
   0x55555555522e <main+69>    mov    rdi, rax
   0x555555555231 <main+72>    call   rdx


In file: /home/ubuntu/GithubFile/Code/CPP/objectmodel/virtual_function_test5.cpp
   65 
   66 int main()
   67 {
   68   Animal *dogPtr = new Dog(10);
   69   dogPtr->printAge();
 ► 70   dogPtr->speak();  // 0x0
   71   dogPtr->eatFood();  // 0x8
   72   dogPtr->sleep();  // 0x10
   73 }
pwndbg> p &dogPtr
$1 = (Animal **) 0x7fffffffe4d8
pwndbg> p dogPtr
$2 = (Dog *) 0x55555556aeb0

pwndbg> p $rbp -0x18
$3 = (void *) 0x7fffffffe4d8
pwndbg> info line *0x5555555553a2
Line 50 of "virtual_function_test5.cpp" starts at address 0x5555555553a2 <Dog::speak()> and ends at 0x5555555553b2 <Dog::speak()+16>.

dogPtr的地址是0x7fffffffe4d8(栈上的局部变量)、保存的内容是0x55555556aeb0(堆上申请的Dog地址最开始位置)。

rbp - 0x18就是dogPtr的地址,[rbp - 0x18]就是0x55555556aeb0(堆上申请的Dog地址最开始位置)。

  • 虚函数eatFood:
   0x555555555233 <main+74>    mov    rax, qword ptr [rbp - 0x18]
 ► 0x555555555237 <main+78>    mov    rax, qword ptr [rax] # 取到虚函数表指针的地址
   0x55555555523a <main+81>    add    rax, 8 # eatFood在虚函数表中的位置偏移是8 
   0x55555555523e <main+85>    mov    rdx, qword ptr [rax] # 取eatFood的地址到rdx中
   0x555555555241 <main+88>    mov    rax, qword ptr [rbp - 0x18]
   0x555555555245 <main+92>    mov    rdi, rax
   0x555555555248 <main+95>    call   rdx


In file: /home/ubuntu/GithubFile/Code/CPP/objectmodel/virtual_function_test5.cpp
   66 int main()
   67 {
   68   Animal *dogPtr = new Dog(10);
   69   dogPtr->printAge();
   70   dogPtr->speak();  // 0x0
 ► 71   dogPtr->eatFood();  // 0x8
   72   dogPtr->sleep();  // 0x10
   73 }
  • 虚函数sleep:
   0x55555555524a <main+97>     mov    rax, qword ptr [rbp - 0x18]
 ► 0x55555555524e <main+101>    mov    rax, qword ptr [rax] # 取到虚函数表指针的地址
   0x555555555251 <main+104>    add    rax, 0x10 # sleep在虚函数表中的位置偏移是8 
   0x555555555255 <main+108>    mov    rdx, qword ptr [rax] # 取sleep的地址到rdx中
   0x555555555258 <main+111>    mov    rax, qword ptr [rbp - 0x18]
   0x55555555525c <main+115>    mov    rdi, rax
   0x55555555525f <main+118>    call   rdx


In file: /home/ubuntu/GithubFile/Code/CPP/objectmodel/virtual_function_test5.cpp
   67 {
   68   Animal *dogPtr = new Dog(10);
   69   dogPtr->printAge();
   70   dogPtr->speak();  // 0x0
   71   dogPtr->eatFood();  // 0x8
 ► 72   dogPtr->sleep();  // 0x10
   73 }

3. 函数重载和多态的区别

  • 函数重载主要是函数签名不同(编译期间确定);
  • 多态主要是虚函数机制;

C++的函数签名

  • 函数签名组成部分:

    • 函数名;
    • 参数类型;
    • 参数个数;
    • 参数顺序;
    • 它所在的类和命名空间;
  • 举例:

    // g++ -c function_signature.cpp -o function_signature.o
    // nm function_signature.o
    
    #include <iostream>
    using namespace std;
    
    // _Z8sayHellov
    //   _Z 前缀:表示这是一个修饰后的名称,通常是为了标识这个名称是 C++ 编译所生成的。
    //   8sayHello:数字 8 表示后面跟随的名称的长度(8 个字符),即 sayHello
    //   v 后缀:表示这个函数没有参数(Void),在 C++ 中,v 是表示没有参数的标识。
    void sayHello()
    {
      cout << "hello\n";
    }
    
    // _Z8sayHelloi
    void sayHello(int a)
    {
      cout << "hello " << a << endl;
    }
    
    class Books
    {
    public:
      // _ZN5BooksC1Ev 构造函数
      // _ZN5BooksC2Ev 默认构造函数
      Books()
      {
      }
      // _ZN5BooksD1Ev 析构函数
      // _ZN5BooksD2Ev 默认析构函数
      ~Books()
      {
      }
    
      // _ZN5Books11printNumberEv
      void printNumber()
      {
        cout << m_cnt << endl;
      }
    
      // _ZN5Books12printAddressEv
      virtual void printAddress()
      {
        cout << "Chengdu\n";
      }
    
    public:
      int m_cnt;
    };
    
    int main()
    {
      sayHello();
      sayHello(2);
      Books books;
      books.printNumber();
    }
    
  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

落后的炫幕あ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值