从内存角度聊聊c++虚函数

        我们知道 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;

        目前想到的虚函数相关问题就这些,以后若想到其它再补充,大家看完有什么想法也可以提出来哈~        

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值