多态是C++中的重要内容,也是设计模式的基础。
形成多态的几个基本条件为:
- 继承和虚函数
- 父类对象指向子类对象
多态形成的原理就是vptr
指针和vtable虚函数表,当一个类中有虚函数时,编译器就会自动生成虚函数表,并生成一个vptr
指针指向这个虚函数表。调用虚函数的时候,会通过这个vptr指针找到相应的虚函数表,然后再定位到对应的函数,以此来调用形成多态。
一、证明vptr指针存在
没有虚函数存在时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include<iostream> using namespace std; class A { public: A() {}; ~A() {}; void print() { cout << "AAAAAAAAAA" << endl; }; }; class B : public A { public: B() {}; ~B() {}; void print() { cout << "BBBBBBBBBB" << endl; }; }; int main() { cout << "sizeof(A): " << sizeof(A) << endl; cout << "sizeof(B): " << sizeof(B) << endl; return 0; } |
A和B的大小都是1(C++对空类会分配一个字节的内存),但是将A中的print()
函数设置为虚函数后:
1 |
void print() { cout << "AAAAAAAAAA" << endl; }; |
程序会输出:
1 2 |
sizeof(A): 4 sizeof(B): 4 |
因为添加虚函数后,系统会默认添加vptr
指针,32位系统,指针是四个字节,所以A和B的大小就变成了4
。
二、vptr指针的工作原理
在创建一个含有虚函数的类时,系统会自动生成一个虚函数表,存放了当前对象所有的虚函数,而vptr指针就指向这个表的地址。子类继承父类后,也会生成一个属于自己的虚函数表和vptr指针。对象调用虚函数,则会通过vptr指针在虚函数表中查找函数的地址进行调用。以此来达到多态的效果。
2.1 静态联编和动态联编
说到多态肯定免不了要提到动态联编,所谓动态联编就是指程序在运行时才能决定的运行片段,例如if
和switch
代码段,它们只有在运行时再能知道下一步是什么,走哪个代码代码段。多态也是如此,运行到了虚函数的时候才能决定执行哪个函数。
而静态联编就是指程序在编译阶段就能决定的事情,就像我们的main
函数,编译后就固定走这一条路线。
2.2 vptr绑定的顺序
子类中vptr指针的值的绑定顺序:
- 运行父类构造函数时,先指向父类的虚函数表。
- 运行子类构造函数时,再把指针指向子类的虚函数表。
我们可以使用gdb来验证这一个观点,代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#include<iostream> using namespace std; class A { public: A() { }; ~A() {}; virtual void print() { cout << "AAAAAAAAAA" << endl; }; }; class B : public A { public: B() { }; ~B() {}; void print() { cout << "BBBBBBBBBB" << endl; }; }; void f(A& p) { p.print(); } int main() { cout << "sizeof(A): " << sizeof(A) << endl; cout << "sizeof(B): " << sizeof(B) << endl; A a; B b; f(a); f(b); return 0; } |
带调试模式编译:
1 |
g++ main.cpp -g -o app -std=c++11 |
启动gdb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
(gdb) l 1 1 #include<iostream> 2 3 using namespace std; 4 5 class A { 6 public: 7 A() { 8 }; 9 ~A() {}; 10 virtual void print() { cout << "AAAAAAAAAA" << endl; }; (gdb) b 8 // 在父类构造函数中加断点 Breakpoint 1 at 0x400c3c: file main.cpp, line 8. (gdb) l // 查看后续代码 11 }; 12 13 class B : public A { 14 public: 15 B() { 16 }; 17 ~B() {}; 18 void print() { cout << "BBBBBBBBBB" << endl; }; 19 }; 20 (gdb) b 16 // 在子类代码中加断点 Breakpoint 2 at 0x400ca8: file main.cpp, line 16. (gdb) l 21 void f(A& p) { 22 p.print(); 23 } 24 25 int main() { 26 cout << "sizeof(A): " << sizeof(A) << endl; 27 cout << "sizeof(B): " << sizeof(B) << endl; 28 29 A a; 30 B b; (gdb) b 30 // 在对象b创建前加断点 Breakpoint 3 at 0x400b4e: file main.cpp, line 30. |
然后运行程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
(gdb) r Starting program: /data/code/cpp/7_class/0x02-多态/2-vptr/app sizeof(A): 8 sizeof(B): 8 Breakpoint 1, A::A (this=0x7fffffffe1c0) at main.cpp:8 8 }; // 此时运行到对象a创建时的构造函数,我们不关心a,直接跳到下一个断点 (gdb) c Continuing. Breakpoint 3, main () at main.cpp:30 30 B b; (gdb) p a // 断点位于B b这行,a已经定义好了,打印a的地址。 $2 = {_vptr.A = 0x400de0 <vtable for A+16>} (gdb) c // 继续运行到下一个断点 Continuing. Breakpoint 1, A::A (this=0x7fffffffe1d0) at main.cpp:8 8 }; // 此时在构造b,正在运行父类A的构造函数A() (gdb) p *this // 打印出此时b的vptr指针,可以看到和对象a中的地址一致 $3 = {_vptr.A = 0x400de0 <vtable for A+16>} (gdb) c // 运行到B的构造函数 Continuing. Breakpoint 2, B::B (this=0x7fffffffe1d0) at main.cpp:16 16 }; (gdb) p *this // 打印vptr,地址已经不同于之前的地址 $4 = {<A> = {_vptr.A = 0x400dc8 <vtable for B+16>}, <No data fields>} (gdb) n // 单步运行,执行到b创建完毕 main () at main.cpp:32 32 f(a); (gdb) p b // b的指针 $5 = {<A> = {_vptr.A = 0x400dc8 <vtable for B+16>}, <No data fields>} |
上面很明显就能看到,b在执行父类构造的时候时指向父类的虚函数表,在执行自己的构造函数时才转向B的
实际上子类对象vptr指针的赋值可以分为以下两步:
- 执行父类构造函数时,指向父类的虚函数表
- 执行子类的构造函数时,再指向子类的虚函数表。
三、关于虚函数的常见问题
// todo
评论