简介:深入理解virtual关键字背后的机制,了解它存在的理由以及使用它需要注意的问题。本文将为你揭开virtual的神秘面纱。
引言
为什么会写这篇文章?主要是因为项目中的代码大量使用了带virtual关键字的类,让我觉得不吐不快。virtual并没有什么超能力可以化腐朽为神奇,它有其存在的理由,但滥用它是一种非常不可取的错误行为。本文将带你一步一步了解virtual机制,为你揭开virtual的神秘面纱。
为什么需要virtual
假设我们正在进行一个公共图形化库的设计实现,其中涉及2d和3d坐标点的打印,设计出Point2d和Point3d的实现如下:
完美,一切都符合预期。既然如此,我们为什么需要virtual?让我们提个新需求:封装一个坐标点打印接口,输入是坐标点实例,输出是坐标点的值。很快,我们实现了代码:
哦噢,问题来了,当我们传入3d坐标点实例时,我们的期望是打印3d坐标点的值,而实际只能打印2d坐标点的值。现在的程序傻傻分不清坐标点是2d还是3d,为了让程序变得更聪明,需要对症下药,而virtual正是该症的药方。只需要更新Point2d接口print的声明即可:
干得漂亮,一切又恢复完美如初。在c++继承关系中实现多态的威力,正是需要virtual的地方。那么它的神奇魔力究竟从何而来呢?一切要从类数据成员内存布局说起。
类的内存布局
在c++对象模型中,非静态数据成员被配置于每一个类对象之内,静态数据成员则被存放在类对象之外。静态和非静态函数成员也被存放在类对象之外。大多数编译器对类的内存布局方式是按成员的声明顺序依次排列,本文的所有例子都是在mac环境下,使用x86_64-apple-darwin21.6.0/clang-1300.0.29.3编译,非virtual版本的Point2d内存布局:
内存布局需要我们注意的是编译器对内存的对齐方式,内存对齐一般分两步:其一是类成员先按自身大小对齐,其二是类按最大成员大小对齐。我们在安排类成员的时候,应该遵循成员从大到小的顺序声明,这样可以避免不必要的内存填充,节省内存占用。
派生类的内存布局
在c++的继承模型中,一个子类的内存大小,是其基类的数据成员加上其自己的数据成员大小的总和。大多数编译器对子类的内存布局是先安排基类的数据成员,然后是本身的数据成员。非virtual版本的Point3d的内存布局:
virtual类的内存布局
当Point2d声明了virtual函数后,对类对象产生了两点重大影响:一是类将产生一系列指向virtual functions的指针,放在表格之中,这个表格被称之为virtual table(vtbl)。二是类实例都被安插一个指针指向相关的virtual table,通常这个指针被称为vptr。为了示例需要,我们重新设计Point2d和Point3d实现:
大多数编译器把vptr安插在类实例的开始处,现在我们来看看virtual版本的Point2d和Point3d的内存布局:
真实内存布局是否如上图所示,很简单,我们一验便知:
关键核心virtual table的获取在第5行,其实可以看成两步操作:intptr_t vptr2d = *(intptr_t*)&point2d;intptr_t *vtbl2d = (intptr_t*)vptr2d;第一步使vptr2d指向virtual table,第二步将指针转换为数组首地址。然后就可以用vtbl2d逐个调用虚函数。从输出结果看,程序确实逐个调用到对应的虚函数,virtual类的内存布局和先前我们所画结构图一致。
......
点击链接查看原文,获取更多福利!
https://developer.aliyun.com/article/1052792?utm_content=g_1000361334
版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。