封装隐藏了类内部细节,经过继承加虚函数的方式,咱们还能够作到隐藏类之间的差别,这就是多态(运行时多态)。多态意味一个接口有多种行为,今天就来讲说C++的多态是怎么实现的。ios
编译时多态感受没什么好说的,编译时直接绑定了函数地址。数组
有下面这么一段代码:A有两个虚函数(virtual
关键字修饰的函数),B继承了A,还有一个参数为A*
的函数foo()
。数据结构
#include <iostream> class A { public: A(); virtual void foo(); virtual void bar(); private: int a; }; A::A() : a( 1 ) { } void A::foo() { std::cout << "A::foo()\n"; return; } void A::bar() { std::cout << "A::bar()\n"; return; } class B : public A { public: B(); virtual void foo(); virtual void bar(); private: int b ; }; B::B() : b( 2 ) { } void B::foo() { std::cout << "B::foo()\n"; return; } void B::bar() { std::cout << "B::bar()\n"; return; } void foo( A* x ) { x->foo(); x->bar(); return; }
咱们要先知道,对于虚函数的重写,规则要求编译器必须根据实际类型调用对应的函数,而不是像重写普通成员函数那样,直接调用当前类型的函数。函数
假设
bar()
是一个非虚函数,B重写了bar()
,那么即便x
指向B的对象,在foo()
调用x->bar()
时也仍是输出"A::bar()"指针
这段代码编译成动态库的话,编译器就没法肯定foo()
的入参x
指向的对象是什么类型了(父类指针能够指向自身类型的对象或任意子类的对象),所以编译器就没法直接得出foo()
和bar()
实际的函数地址,没法完成函数调用。这中间确定发生了什么!调试
题外话:一旦函数重写,
A::foo()
和B::foo()
就是两个函数,两个地址。若是只是单纯继承的话,以前介绍继承的时候说过,子类是不存在B:;foo()
这个函数,而只是让编译器容许经过B类型的对象调用A::foo()
。code
一旦没法天然地想通一个流程,以为中间缺了什么东西时,那确定是编译器干了什么。所以仍是要祭出gdb
这件大杀器。对象
// 省略前面那段代码 int main() { B* x = new B; foo( x ); return 0; }
当咱们打印x
的内容时,会发现其多了一个位于对象的首地址的_vptr.A
,它其实指向了虚函数表。继承
(gdb) p *x $2 = {<A> = {_vptr.A = 0x400a70 <vtable for B+16>, a = 1}, b = 2}
foo()
中的x->foo()
和x->bar()
对应着以下汇编接口
# x->foo() 0x0000000000400815 <+8>: mov %rdi,-0x8(%rbp) # 将rdi中的对象地址保存到-0x8(%rbp) 中 => 0x0000000000400819 <+12>: mov -0x8(%rbp),%rax 0x000000000040081d <+16>: mov (%rax),%rax # 取对象首地址的8个字节也就是_vptr.A 0x400a70保存到rax中 0x0000000000400820 <+19>: mov (%rax),%rax # 再取出0x400a70这个地址存放的4个字节数据保存到rax中,其实就是B::foo()函数地址 0x0000000000400823 <+22>: mov -0x8(%rbp),%rdx # 将对象地址保存到rdx中 0x0000000000400827 <+26>: mov %rdx,%rdi # 将对象地址保存到rdi中,做为虚函数foo()的参数 0x000000000040082a <+29>: callq *%rax # 调用B::foo() # x->bar() 0x000000000040082c <+31>: mov -0x8(%rbp),%rax 0x0000000000400830 <+35>: mov (%rax),%rax # 取对象首地址的8个字节也就是_vptr.A 0x400a70保存到rax中 0x0000000000400833 <+38>: add $0x8,%rax # 跳过8字节,即0x400a70+8 0x0000000000400837 <+42>: mov (%rax),%rax # 取出B::bar()的地址 0x000000000040083a <+45>: mov -0x8(%rbp),%rdx 0x000000000040083e <+49>: mov %rdx,%rdi 0x0000000000400841 <+52>: callq *%rax # 调用B::bar()
看一下0x400a70
这个地址的内容,更容易理解上面的汇编。
(gdb) x /4x 0x400a70 0x400a70 <_ZTV1B+16>: 0x0040095e 0x00000000 0x0040097c 0x00000000 (gdb) x 0x0040095e 0x40095e <B::foo()>: 0xe5894855 # 0x0040095e就是B::foo()的首地址 (gdb) x 0x0040097c 0x40097c <B::bar()>: 0xe5894855 # 0x0040097c就是B::bar()的首地址
从上面能够看出,虚函数表相似于一个数组,其中每一个元素是该类实现的虚函数地址,利用虚函数表,就执行正确的函数了。
既然虚函数表是类数据结构里的一部分,那它的初始化确定就是在类的构造函数里了,让咱们去找找。
下面是B::B()
的一部分汇编,A::A()
也相似只不过是将A的虚函数表地址赋值给_vptr.A
。
0x0000000000400941 <+19>: callq 0x4008d2 <A::A()> # 先构造父类 0x0000000000400946 <+24>: mov -0x8(%rbp),%rax 0x000000000040094a <+28>: movq $0x400a70,(%rax) # 将B的虚函数表地址0x400a70保存到对象的首地址中,即给_vptr.A赋值 0x0000000000400951 <+35>: mov -0x8(%rbp),%rax 0x0000000000400955 <+39>: movl $0x2,0xc(%rax) # 初始化列表
题外话:在更新虚函数表和初始化列表以后,才执行咱们显式写在
B::B()
中的代码。
每一个类都有一个本身的虚函数表,这在编译时就肯定了。若是子类没有实现虚函数,虚函数表里对应位置的函数地址就仍是父类的函数地址。
从上面咱们知道
class A { public: A(); virtual void bar(); virtual void foo(); private: int a; }; class B : public A { public: B(); virtual void bar(); virtual void foo(); int b; }; void bar( A* x ) { x->foo(); x->bar(); return; } int main() { B* b = new B; bar( b ); return 0; }
上面代码的输出是
B::bar() B::foo()
与预期结果恰好相反
B::foo() B::bar()
出现这样错误的缘由就是在编译main.cpp时,编译器认为B::foo()
是虚函数表的第二个元素,但实际在liba.so中B::foo()
是虚函数表中的第一个元素。
强烈建议虚函数的声明顺序必须保持一致,并且增长虚函数时,只在尾部增长
了解C++的多态实现后,对于理解其余语言的多态实现也是有益处的,本质都应当是在经过一个中间结构肯定实际函数的地址。
除了以上内容外,还有
gcc version 4.8.5