• 从内存角度聊聊虚函数


            我们知道 c++ 引入虚函数是为了实现多态,即根据对象的类型来调用相应的成员函数,前提是有一个基类和至少一个派生类。

            此处先看看只有一个类时的虚函数情况,假设定义一个 Base 类:

    1. class Base
    2. {
    3. public:
    4. int base_a;
    5. Base(int m) { base_a = m; }
    6. virtual ~Base() { cout << "I'm a base class destructor" << endl; }
    7. virtual void func() { cout << "I'm a base class func()" << endl; }
    8. };

            再定义如下主方法:

    1. int main()
    2. {
    3. Base baseObj(3);
    4. cout << "hello world" << endl;
    5. return 0;
    6. }

            开始调试,在主方法 main() 处打断点:

    1. Thread 3 hit Breakpoint 1, main () at test.cpp:63
    2. 63 Base baseObj(3);
    3. (gdb) s
    4. Base::Base (this=0x7ffeefbff9f8, m=3) at test.cpp:17
    5. 17 {
    6. (gdb) p *this
    7. $1 = {_vptr$Base = 0x0, base_a = -272631264}

            可以看到在程序运行到 constructor 时,参数并不仅仅是只有定义的 m,还有个 *this 指针,且 *this 指针的地址有了,也就是 0x7ffeefbff9f8,并且除了定义的数据成员 base_a,还有个隐藏成员 _vptr$Base。但此时这两个成员都还没有赋值。继续往下:

    1. (gdb) s
    2. Base::Base (this=0x7ffeefbff9f8, m=3) at test.cpp:17
    3. 17 {
    4. (gdb) n
    5. 18 this->base_a = m;
    6. (gdb) p *this
    7. $2 = {_vptr$Base = 0x100004038 for Base+16>, base_a = -272631264}
    8. (gdb) n
    9. 19 };
    10. (gdb) p *this
    11. $3 = {_vptr$Base = 0x100004038 for Base+16>, base_a = 3}

            当程序运行到 constructor 函数体内部时,可以看到 _vptr$Base 就有值了,是个指向虚函数表的指针,稍后我们可以看虚函数表的内容。待将形参 m  赋值给 base_a 回到主方法后,*this 指针指向的数据就全部复制给对象 baseObj 了,这点可以从 baseObj 的地址看到与刚才的 *this 指向的地址是一致的:

    1. (gdb) p &baseObj
    2. $5 = (Base *) 0x7ffeefbff9f8
    3. (gdb) p baseObj
    4. $6 = {_vptr$Base = 0x100004038 for Base+16>, base_a = 3}

            虚函数表实际上是一个顺序表,表中每项元素都记录着指向虚函数的指针,可以根据地址打印出全部内容:

    1. (gdb) p (void *)*((long *)0x100004038)
    2. $7 = (void *) 0x100002f80
    3. (gdb) p (void *)*((long *)0x100004038 + 1)
    4. $8 = (void *) 0x100002fa0
    5. (gdb) p (void *)*((long *)0x100004038 + 2)
    6. $9 = (void *) 0x100003000
    7. (gdb) p (void *)*((long *)0x100004038 + 3)
    8. $10 = (void *) 0x7fff87542d38

    或者通过 info 命令:

    1. (gdb) info vtbl baseObj
    2. vtable for 'Base' @ 0x100004038 (subobject @ 0x7ffeefbff9f8):
    3. [0]: 0x100002f80
    4. [1]: 0x100002fa0
    5. [2]: 0x100003000

            可以看到虚函数表中有两个 destructor,而程序只定义了一个,此处可以参考帖子:Why does it generate multiple dtors,再后面就是自定义的虚拟成员函数 func()。

            根据上述可以了解到对于类中有定义虚函数的,定义对象时,对象会有个隐藏的成员即虚表指针,它指向虚函数表,虚函数表记录每个虚函数的地址。由此也可以看到虚函数并没有直接存在对象中,那它是存哪的呢?根据地址来分析:

    1. (gdb) info symbol 0x100004038
    2. vtable for Base + 16 in section __DATA_CONST.__const of /Users/lucas/study/testCpp/test

            或者利用 objdump 命令:

    1. lucas@lucasdeMacBook-Pro testCpp % ll
    2. total 136
    3. -rwxr-xr-x 1 root staff 64048 11 29 22:16 test
    4. -rw-r--r-- 1 lucas staff 1082 11 29 22:16 test.cpp
    5. drwxr-xr-x 3 lucas staff 96 11 2 16:50 test.dSYM
    6. lucas@lucasdeMacBook-Pro testCpp % objdump -h test
    7. test: file format mach-o 64-bit x86-64
    8. Sections:
    9. Idx Name Size VMA Type
    10. 0 __text 00000f09 0000000100002e00 TEXT
    11. 1 __stubs 0000008a 0000000100003d0a TEXT
    12. 2 __stub_helper 000000ba 0000000100003d94 TEXT
    13. 3 __gcc_except_tab 000000b8 0000000100003e50 DATA
    14. 4 __cstring 00000034 0000000100003f08 DATA
    15. 5 __const 00000006 0000000100003f3c DATA
    16. 6 __unwind_info 000000bc 0000000100003f44 DATA
    17. 7 __got 00000028 0000000100004000 DATA
    18. 8 __const 00000038 0000000100004028 DATA
    19. 9 __la_symbol_ptr 000000b8 0000000100008000 DATA
    20. 10 __data 00000008 00000001000080b8 DATA

            地址由低向高扩展,可以看到虚函数表是存在数据区的常量部分。

            至此就可以理清虚函数与类、对象之间的关系了,同时也能够解答一些相关问题。

            1. 虚函数存储在数据区常量部分,属于类。不管定义多少对象,虚函数以及虚函数表都只此一份,但每个对象都包含一个虚表指针,通过虚表指针来找到虚函数。

    1. (gdb) p obj1
    2. $1 = {_vptr$Base = 0x100004038 for Base+16>, base_a = 3}
    3. (gdb) p obj2
    4. $2 = {_vptr$Base = 0x100004038 for Base+16>, base_a = 5}
    5. (gdb) p &obj1
    6. $3 = (Base *) 0x7ffeefbff9f8
    7. (gdb) p &obj2
    8. $4 = (Base *) 0x7ffeefbff9e8

            2. 虚表指针依赖于对象,所以当调用虚函数时,依赖于对象的类型。如果是基类对象,就会调用基类定义的虚函数;如果是派生类对象,就调用派生类定义的虚函数。如果基类与派生类虚函数同名,也就实现了多态。

            3. 构造函数不能为虚函数。构造函数的目的是创建类对象,如果它为虚函数,那么当调用它创建对象时,需要先拿到虚表指针,通过虚表指针去找到该构造函数。但是虚表指针是依赖于对象而存在的,此时对象还没创建,就不存在虚表指针,也就拿不到对应虚函数,所以就形成一个悖论。

            4. 析构函数需定义为虚函数。当基类指针指向派生类对象时,如果基类析构函数为普通函数,那么当执行 delete 删除基类指针时,基类指针就只能调用基类的析构函数,而找不到派生类析构函数,继而导致内存泄漏。但如果定义为虚函数,在派生类对象的虚表中就会包含派生类析构函数。

    1. (gdb) p *pObj
    2. $2 = {_vptr$Base = 0x100004068 for Derived+16>, base_a = 3}
    3. (gdb) info vtbl *pObj
    4. vtable for 'Base' @ 0x100004068 (subobject @ 0x100304100):
    5. [0]: 0x100002e00
    6. [1]: 0x100002e20
    7. [2]: 0x100002e80

            这时如果再调用 delete  删除,就会先调用派生类析构函数,再调用基类析构函数了。

    1. (gdb) n
    2. main () at test.cpp:71
    3. 71 delete pObj;
    4. (gdb) s
    5. Derived::~Derived (this=0x100304100) at test.cpp:47
    6. 47 {
    7. (gdb)
    8. 48 cout << "I'm a derived class destructor" << endl;
    9. (gdb) n
    10. Derived::~Derived (this=0x100304100) at test.cpp:49
    11. 49 }
    12. (gdb) s
    13. Base::~Base (this=0x100304100) at test.cpp:23
    14. 23 {
    15. (gdb) s
    16. 24 cout << "I'm a base class destructor" << endl;
    17. (gdb) n
    18. 25 }
    19. (gdb)
    20. main () at test.cpp:73
    21. 73 return 0;

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

  • 相关阅读:
    python之while循环介绍
    【微服务】微服务架构
    豪车大洗牌,小米成搅动市场“鲶鱼”
    HTML静态网页成品作业(HTML+CSS)——电影肖申克的救赎介绍设计制作(1个页面)
    大数据开发写sql写烦了,要不要转?
    视频剪辑方法:为视频剪辑添加亮点,如何制作精美的滚动字幕
    股票程序化交易系统是如何计算胜率的?
    「UG/NX」Block UI 指定方位SpecifyOrientation
    关于 分布式事务 你知道多少
    VS CODE中的筛选器如何打开?
  • 原文地址:https://blog.csdn.net/yang1018679/article/details/128106264