C++ 虚函数 虚析构 多态性原理

C++ 动态绑定:

C++ 动态绑定触发时机:

  • 基类中有虚函数
  • 使用基类类型指针引用子类对象
  • 基类指针调用虚函数
#include <cstdio>

class Base {
 public:
  virtual ~Base() {}

  virtual void foo() { ::printf("%s", __PRETTY_FUNCTION__); }

  virtual void foo1() { ::printf("%s", __PRETTY_FUNCTION__); }

  void foo2() { ::printf("%s", __PRETTY_FUNCTION__); }
};

class Derived : public Base {
 public:
  ~Derived() override {}

  void foo() override { ::printf("%s", __PRETTY_FUNCTION__); }

  void foo1() override { ::printf("%s", __PRETTY_FUNCTION__); }

  void foo2() { ::printf("%s", __PRETTY_FUNCTION__); }
};

通常我们有如下实现

int main() {
  Base *p1 = new Derived;
  p1->foo2();  // Base::foo2()
  p1->foo();   // virtual void Derived::foo()
  p1->foo1();  // virtual void Derived::foo1()

  delete p1;
}

很显然,在p1->foo(); p1->foo1();这个两个表达式中,由于子类重写了父类中的2个虚函数,这里理所当然会调用子类的方法。

而在表达式p1->foo2();中,本想调用子类方法,奈何父类的该方法不是虚函数,则调用了父类的方法。

那么这是为什么呢?

查看主函数中反汇编的调用堆栈:

# g++ main.cpp -o test
# objdump -d test >1.txt
0000000000001189 <main>:
    1189:	f3 0f 1e fa          	endbr64 
    118d:	55                   	push   %rbp
    118e:	48 89 e5             	mov    %rsp,%rbp
    1191:	53                   	push   %rbx
    1192:	48 83 ec 18          	sub    $0x18,%rsp
    1196:	bf 08 00 00 00       	mov    $0x8,%edi
    119b:	e8 e0 fe ff ff       	callq  1080 <_Znwm@plt>
    11a0:	48 89 c3             	mov    %rax,%rbx
    11a3:	48 89 df             	mov    %rbx,%rdi
    11a6:	e8 09 02 00 00       	callq  13b4 <_ZN7DerivedC1Ev>
    11ab:	48 89 5d e8          	mov    %rbx,-0x18(%rbp)
    11af:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
    11b3:	48 89 c7             	mov    %rax,%rdi
    11b6:	e8 f9 00 00 00       	callq  12b4 <_ZN4Base4foo2Ev>	// 明确调用Base::foo2
    11bb:	48 8b 45 e8          	mov    -0x18(%rbp),%rax	// this的值(对象地址)放入rax
    11bf:	48 8b 00             	mov    (%rax),%rax	// 将对象的前8字节(虚表指针的值)放入rax,即存入虚表的首地址
    11c2:	48 83 c0 10          	add    $0x10,%rax	// 地址偏移16位,即要调用虚表中第3个函数
    11c6:	48 8b 10             	mov    (%rax),%rdx
    11c9:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
    11cd:	48 89 c7             	mov    %rax,%rdi
    11d0:	ff d2                	callq  *%rdx	// 调用虚表中第3个函数
    11d2:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
    11d6:	48 8b 00             	mov    (%rax),%rax
    11d9:	48 83 c0 18          	add    $0x18,%rax	// 地址偏移24位,即要调用虚表中第4个函数
    11dd:	48 8b 10             	mov    (%rax),%rdx
    11e0:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
    11e4:	48 89 c7             	mov    %rax,%rdi
    11e7:	ff d2                	callq  *%rdx	// 调用虚表中第4个函数
    11e9:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
    11ed:	48 85 c0             	test   %rax,%rax
    11f0:	74 0f                	je     1201 <main+0x78>
    11f2:	48 8b 10             	mov    (%rax),%rdx
    11f5:	48 83 c2 08          	add    $0x8,%rdx	// 地址偏移8位,即要调用虚表中第2个函数
    11f9:	48 8b 12             	mov    (%rdx),%rdx
    11fc:	48 89 c7             	mov    %rax,%rdi
    11ff:	ff d2                	callq  *%rdx	// 调用虚表中第2个函数
    1201:	b8 00 00 00 00       	mov    $0x0,%eax
    1206:	48 83 c4 18          	add    $0x18,%rsp
    120a:	5b                   	pop    %rbx
    120b:	5d                   	pop    %rbp
    120c:	c3                   	retq   
    120d:	90                   	nop

查看对象内存布局:

g++ -fdump-class-hierarchy main.cpp
# 或者高版本g++使用
g++ -fdump-lang-class main.cpp
Vtable for Base
Base::_ZTV4Base: 6 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI4Base)
16    (int (*)(...))Base::~Base
24    (int (*)(...))Base::~Base
32    (int (*)(...))Base::foo
40    (int (*)(...))Base::foo1

Class Base
   size=8 align=8
   base size=8 base align=8
Base (0x0x7f1cebb0ec60) 0 nearly-empty
    vptr=((& Base::_ZTV4Base) + 16)

Vtable for Derived
Derived::_ZTV7Derived: 6 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI7Derived)
16    (int (*)(...))Derived::~Derived
24    (int (*)(...))Derived::~Derived
32    (int (*)(...))Derived::foo
40    (int (*)(...))Derived::foo1

Class Derived
   size=8 align=8
   base size=8 base align=8
Derived (0x0x7f1ceb9ba1a0) 0 nearly-empty
    vptr=((& Derived::_ZTV7Derived) + 16)
  Base (0x0x7f1cebb7d000) 0 nearly-empty
      primary-for Derived (0x0x7f1ceb9ba1a0)

从中可以得知:

  • 父类和子类都生成了各自的虚表,字节都是按照8字节对齐
  • vptr=((& Base::_ZTV4Base) + 16)表示父类中的虚表指针所指位子在相对虚表头部16个字节的地方,子类同理。
  • 子类的虚表函数顺序和父类保持一致
  • 子类重写了父类方法后,在其虚表中表示的作用域从父类改为了子类

结合反汇编,可以看到:

  • p1->foo2(),编译器能够找到确切的函数地址,于是直接在汇编代码中填入了该地址
  • p1->foo ,编译器填了一个相对虚表指针所指位置偏移16位的偏移量,结合内存布局可以知道这个位子是xxx::foo()的地址
  • p1->foo1 ,编译器填了一个相对虚表指针所指位置偏移24位的偏移量,结合内存布局可以知道这个位子是xxx::foo1()的地址
  • delete p1 ,编译器填了一个相对虚表指针所指位置偏移8位的偏移量,结合内存布局可以知道这个位子是xxx::~xxx()的地址

于是可以知道:

  • 编译器在编译当前cpp文件的时候,通过静态分析p1就是一个纯粹的Base类指针,跟其指向的Derived类对象没有任何联系
  • 编译p1->foo2()时发现该函数在Base中不是虚函数,于是根据p1原本的类型,直接找到了Base::foo2()
  • 编译p1->foo()时发现该函数在Base中是虚函数,于是乎填写了该函数相对于虚表指针的偏移量。所以在编一阶段编译器仅知道这个某个方法距离虚表指针的偏移量,具体是谁的虚表指针会在运行时得到,并通过偏移量找到对应函数
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

歪锅锅

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

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

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

打赏作者

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

抵扣说明:

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

余额充值