C++ 对象模型 — Data 语义学
文章目录
对象大小
- 语言本身所造成的额外负担,如虚基类
- 编译器对于特殊情况的优化处理,如某些编译器对空虚基类的特殊支持,一般放在头部 就省去 1 byte 大小的空间
- 字节对齐
数据成员的绑定
早期的两种防御性编程:
- 把所有的 data members 放在 class 声明头处确保正确的绑定
- 把所有的 inline functions, 不管大小定义都放在 class 声明之外
C++ 2.0 之后这种风格消失了,因为对其成员函数的分析会到整个 class 的声明都出现才 开始。
数据成员的布局
C++ 标准要求同一个 access section 中, members 的排列顺序只需符合 较晚出现的 members 在 class object 中有较高的地址, 也就是说不一定连续。 members 之间可能会 存在一些 padding 或着合成的 data members, 如 vptr.
当前编译器都是把多个 access sections 按照声明的顺序连锁在一起成为连续区。
数据成员的存取
静态数据成员
只有一个实体,存放在数据段。存取时会内部转化为唯一的 extern 实体。
非静态数据成员
存在类对象中,只能通过显式或隐式(如this)的类对象进行存取。
存取非静态数据,编译器需要把类对象起始地址加上数据成员的偏移。
继承与数据成员
派生类的数据表现出的东西是自己和继承的成员的总和,但是排列次序,C++标准并未规定; 大部分编译器中,基类先出现。但 虚基类 除外。
只要继承不要多态
容易犯的错误:
- 重复设计一些相同操作的函数
- 把 class 分解多蹭导致所需空间膨胀
加上多态
空间和存取时间的额外负担:
- 类的虚函数表 virtual table
- 类对象的虚函数指针 vptr
- 加强构造函数,能够设置 vptr 初始值指向类的虚函数表
- 加强析构函数,抹消 vptr
vptr 应放在什么位置,因不同的编译器而不同:
- 前端:多重继承时,通过指向类成员的指针调用虚函数会有优势;但丧失可 C 语言的兼 容性
- 后端:保留 C struct 的对象布局
多重继承
派生类对象将其地址指定最左边(第一个)基类的指针,情况和单一继承一样,因为有相同 的起始地址。
第二个及后继基类的地址指定操作则需要将地址修改,加上或减去介于中间的基类的大小。
虚继承
副作用:必须支持某种形式的 shared suobject 继承 语意。所表现的是基类子对象的数 据的位置会因每次的派生操作而变化;所以只能间接存取,存取策略因不同的编译器而不同。
指向数据成员的指针
需要详细调查成员低层布局时比较有用,可以决定 vptr 是在起始处还是尾端。
- 静态数据成员取地址获得内存地址
- 非静态数据成员取地址获得的是相对于对象起始地址的偏移。
要输出成员的地址要用 %p 格式化输出,不能用 std::cout 。