从内存角度聊聊虚函数
创始人
2024-03-12 04:08:39
0

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

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

class Base
{public:int base_a;Base(int m);virtual ~Base();virtual void func();
};Base::Base(int m)
{this->base_a = m;
};Base::~Base()
{cout << "I'm a base class destructor" << endl;
}void Base::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 , base_a = -272631264}
(gdb) n
19      };
(gdb) p *this
$3 = {_vptr$Base = 0x100004038 , 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 , 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 testtest:   file format mach-o 64-bit x86-64Sections:
Idx Name             Size     VMA              Type0 __text           00000f09 0000000100002e00 TEXT1 __stubs          0000008a 0000000100003d0a TEXT2 __stub_helper    000000ba 0000000100003d94 TEXT3 __gcc_except_tab 000000b8 0000000100003e50 DATA4 __cstring        00000034 0000000100003f08 DATA5 __const          00000006 0000000100003f3c DATA6 __unwind_info    000000bc 0000000100003f44 DATA7 __got            00000028 0000000100004000 DATA8 __const          00000038 0000000100004028 DATA9 __la_symbol_ptr  000000b8 0000000100008000 DATA10 __data           00000008 00000001000080b8 DATA

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

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

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

(gdb) p obj1
$1 = {_vptr$Base = 0x100004038 , base_a = 3}
(gdb) p obj2
$2 = {_vptr$Base = 0x100004038 , 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 , 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;

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

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...