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
中是虚函数,于是乎填写了该函数相对于虚表指针的偏移量。所以在编一阶段编译器仅知道这个某个方法距离虚表指针的偏移量,具体是谁的虚表指针会在运行时得到,并通过偏移量找到对应函数