我们知道 c++ 引入虚函数是为了实现多态,即根据对象的类型来调用相应的成员函数,前提是有一个基类和至少一个派生类。
此处先看看只有一个类时的虚函数情况,假设定义一个 Base 类:
- class Base
- {
- public:
- int base_a;
- Base(int m) { base_a = m; }
- virtual ~Base() { cout << "I'm a base class destructor" << endl; }
- virtual void func() { cout << "I'm a base class func()" << endl; }
- };
再定义如下主方法:
- int main()
- {
- Base baseObj(3);
-
- cout << "hello world" << endl;
-
- return 0;
- }
开始调试,在主方法 main() 处打断点:
- Thread 3 hit Breakpoint 1, main () at test.cpp:63
- 63 Base baseObj(3);
- (gdb) s
- Base::Base (this=0x7ffeefbff9f8, m=3) at test.cpp:17
- 17 {
- (gdb) p *this
- $1 = {_vptr$Base = 0x0, base_a = -272631264}
可以看到在程序运行到 constructor 时,参数并不仅仅是只有定义的 m,还有个 *this 指针,且 *this 指针的地址有了,也就是 0x7ffeefbff9f8,并且除了定义的数据成员 base_a,还有个隐藏成员 _vptr$Base。但此时这两个成员都还没有赋值。继续往下:
- (gdb) s
- Base::Base (this=0x7ffeefbff9f8, m=3) at test.cpp:17
- 17 {
- (gdb) n
- 18 this->base_a = m;
- (gdb) p *this
- $2 = {_vptr$Base = 0x100004038
for Base+16>, base_a = -272631264} - (gdb) n
- 19 };
- (gdb) p *this
- $3 = {_vptr$Base = 0x100004038
for Base+16>, base_a = 3}
当程序运行到 constructor 函数体内部时,可以看到 _vptr$Base 就有值了,是个指向虚函数表的指针,稍后我们可以看虚函数表的内容。待将形参 m 赋值给 base_a 回到主方法后,*this 指针指向的数据就全部复制给对象 baseObj 了,这点可以从 baseObj 的地址看到与刚才的 *this 指向的地址是一致的:
- (gdb) p &baseObj
- $5 = (Base *) 0x7ffeefbff9f8
- (gdb) p baseObj
- $6 = {_vptr$Base = 0x100004038
for Base+16>, base_a = 3}
虚函数表实际上是一个顺序表,表中每项元素都记录着指向虚函数的指针,可以根据地址打印出全部内容:
- (gdb) p (void *)*((long *)0x100004038)
- $7 = (void *) 0x100002f80
- (gdb) p (void *)*((long *)0x100004038 + 1)
- $8 = (void *) 0x100002fa0
- (gdb) p (void *)*((long *)0x100004038 + 2)
- $9 = (void *) 0x100003000
- (gdb) p (void *)*((long *)0x100004038 + 3)
- $10 = (void *) 0x7fff87542d38
或者通过 info 命令:
- (gdb) info vtbl baseObj
- vtable for 'Base' @ 0x100004038 (subobject @ 0x7ffeefbff9f8):
- [0]: 0x100002f80
- [1]: 0x100002fa0
- [2]: 0x100003000
可以看到虚函数表中有两个 destructor,而程序只定义了一个,此处可以参考帖子:Why does it generate multiple dtors,再后面就是自定义的虚拟成员函数 func()。
根据上述可以了解到对于类中有定义虚函数的,定义对象时,对象会有个隐藏的成员即虚表指针,它指向虚函数表,虚函数表记录每个虚函数的地址。由此也可以看到虚函数并没有直接存在对象中,那它是存哪的呢?根据地址来分析:
- (gdb) info symbol 0x100004038
- vtable for Base + 16 in section __DATA_CONST.__const of /Users/lucas/study/testCpp/test
或者利用 objdump 命令:
- lucas@lucasdeMacBook-Pro testCpp % ll
- total 136
- -rwxr-xr-x 1 root staff 64048 11 29 22:16 test
- -rw-r--r-- 1 lucas staff 1082 11 29 22:16 test.cpp
- drwxr-xr-x 3 lucas staff 96 11 2 16:50 test.dSYM
- lucas@lucasdeMacBook-Pro testCpp % objdump -h test
-
- test: file format mach-o 64-bit x86-64
-
- Sections:
- Idx Name Size VMA Type
- 0 __text 00000f09 0000000100002e00 TEXT
- 1 __stubs 0000008a 0000000100003d0a TEXT
- 2 __stub_helper 000000ba 0000000100003d94 TEXT
- 3 __gcc_except_tab 000000b8 0000000100003e50 DATA
- 4 __cstring 00000034 0000000100003f08 DATA
- 5 __const 00000006 0000000100003f3c DATA
- 6 __unwind_info 000000bc 0000000100003f44 DATA
- 7 __got 00000028 0000000100004000 DATA
- 8 __const 00000038 0000000100004028 DATA
- 9 __la_symbol_ptr 000000b8 0000000100008000 DATA
- 10 __data 00000008 00000001000080b8 DATA
地址由低向高扩展,可以看到虚函数表是存在数据区的常量部分。
至此就可以理清虚函数与类、对象之间的关系了,同时也能够解答一些相关问题。
1. 虚函数存储在数据区常量部分,属于类。不管定义多少对象,虚函数以及虚函数表都只此一份,但每个对象都包含一个虚表指针,通过虚表指针来找到虚函数。
- (gdb) p obj1
- $1 = {_vptr$Base = 0x100004038
for Base+16>, base_a = 3} - (gdb) p obj2
- $2 = {_vptr$Base = 0x100004038
for Base+16>, base_a = 5} - (gdb) p &obj1
- $3 = (Base *) 0x7ffeefbff9f8
- (gdb) p &obj2
- $4 = (Base *) 0x7ffeefbff9e8
2. 虚表指针依赖于对象,所以当调用虚函数时,依赖于对象的类型。如果是基类对象,就会调用基类定义的虚函数;如果是派生类对象,就调用派生类定义的虚函数。如果基类与派生类虚函数同名,也就实现了多态。
3. 构造函数不能为虚函数。构造函数的目的是创建类对象,如果它为虚函数,那么当调用它创建对象时,需要先拿到虚表指针,通过虚表指针去找到该构造函数。但是虚表指针是依赖于对象而存在的,此时对象还没创建,就不存在虚表指针,也就拿不到对应虚函数,所以就形成一个悖论。
4. 析构函数需定义为虚函数。当基类指针指向派生类对象时,如果基类析构函数为普通函数,那么当执行 delete 删除基类指针时,基类指针就只能调用基类的析构函数,而找不到派生类析构函数,继而导致内存泄漏。但如果定义为虚函数,在派生类对象的虚表中就会包含派生类析构函数。
- (gdb) p *pObj
- $2 = {_vptr$Base = 0x100004068
for Derived+16>, base_a = 3} - (gdb) info vtbl *pObj
- vtable for 'Base' @ 0x100004068 (subobject @ 0x100304100):
- [0]: 0x100002e00
- [1]: 0x100002e20
- [2]: 0x100002e80
这时如果再调用 delete 删除,就会先调用派生类析构函数,再调用基类析构函数了。
- (gdb) n
- main () at test.cpp:71
- 71 delete pObj;
- (gdb) s
- Derived::~Derived (this=0x100304100) at test.cpp:47
- 47 {
- (gdb)
- 48 cout << "I'm a derived class destructor" << endl;
- (gdb) n
- Derived::~Derived (this=0x100304100) at test.cpp:49
- 49 }
- (gdb) s
- Base::~Base (this=0x100304100) at test.cpp:23
- 23 {
- (gdb) s
- 24 cout << "I'm a base class destructor" << endl;
- (gdb) n
- 25 }
- (gdb)
- main () at test.cpp:73
- 73 return 0;
目前想到的虚函数相关问题就这些,以后若想到其它再补充,大家看完有什么想法也可以提出来哈~