我们知道 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 <vtable for Base+16>, base_a = -272631264}
(gdb) n
19 };
(gdb) p *this
$3 = {_vptr$Base = 0x100004038 <vtable 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 <vtable for Base+16>, base_a = 3}
虚函数表实际上是一个顺序表,表中每项元素都记录着指向虚函数的指针,可以根据地址打印出全部内容:
(gdb) p (void *)*((long *)0x100004038)
$7 = (void *) 0x100002f80 <Base::~Base()>
(gdb) p (void *)*((long *)0x100004038 + 1)
$8 = (void *) 0x100002fa0 <Base::~Base()>
(gdb) p (void *)*((long *)0x100004038 + 2)
$9 = (void *) 0x100003000 <Base::func()>
(gdb) p (void *)*((long *)0x100004038 + 3)
$10 = (void *) 0x7fff87542d38
或者通过 info 命令:
(gdb) info vtbl baseObj
vtable for 'Base' @ 0x100004038 (subobject @ 0x7ffeefbff9f8):
[0]: 0x100002f80 <Base::~Base()>
[1]: 0x100002fa0 <Base::~Base()>
[2]: 0x100003000 <Base::func()>
可以看到虚函数表中有两个 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 <vtable for Base+16>, base_a = 3}
(gdb) p obj2
$2 = {_vptr$Base = 0x100004038 <vtable 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 <vtable for Derived+16>, base_a = 3}
(gdb) info vtbl *pObj
vtable for 'Base' @ 0x100004068 (subobject @ 0x100304100):
[0]: 0x100002e00 <Derived::~Derived()>
[1]: 0x100002e20 <Derived::~Derived()>
[2]: 0x100002e80 <Derived::func()>
这时如果再调用 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;
目前想到的虚函数相关问题就这些,以后若想到其它再补充,大家看完有什么想法也可以提出来哈~