《深入探索C++对象模型》笔记 Chapter2 构造函数
《深入探索C++对象模型》笔记 Chapter3 成员变量
《深入探索C++对象模型》笔记 Chapter4 成员函数
第3章 成员变量
3.1 成员变量的绑定
所谓成员变量的绑定(binding),就是确定成员变量的类型。试想这样一个场景,有一个 extern int 类型的全局变量 x ,某个类声明了一个 float类型的 x,并且成员函数返回一个 x ,那么这个 x 应该是 float 而不是 int 是吧?但是如果成员函数的声明放在 float x 的声明之前呢?
这就是C++标准里 member scope resolution rule 的必要性了,对成员函数的分析,会直到整个类的声明都出现了才开始。 但是对于函数参数,还是会先 resolve 为全局声明的类型,然后碰到 nested type 的声明,再将之前的绑定标记为非法。为避免这种情况,我们在写代码时,尽量要把 nested type 声明放在类的起始处。
3.2 成员变量的布局
在类声明中,public/private/protect 分隔开的一段声明称之为一个 access section 。C++标准要求在一个 access section 里声明的变量要按照从低地址到高地址的顺序,不同 access section 的变量自由排列,不过主流编译器都是把各个 access session 连锁在一起,依照声明顺序,成为一个连续区块。
3.3 成员变量的存取
通过指针和通过对象对成员变量存取是有区别的,不过静态成员变量除外。
对于非静态成员变量的存取,origin._y
会被编译器翻译为 &origin + (&A::_y -1)
,其中 A::_y
表示成员变量 _y 在对象布局中的偏移值,至于为什么要减一,在3.6节有详细解释。
如果。。。。虚继承,通过指针对成员变量存取就必须要延迟到运行期,但通过对象对成员变量存取就不会出现这个问题。
3.4 继承与成员变量
只考虑继承
非 virtual 的继承不会增加空间或存取时间上的额外负担。
设想一个 Concrete3 继承 Concrete2,Concrete2 继承 Concrete1 的场景。如果 Concrete1 有两个成员 int 和 char,Concrete2 Concrete3分别额外持有一个 char,那么内存布局应该是这样:
显然,这种布局会把很多空间浪费在 padding 上。那么为什么不采用看上去更好的办法,将 padding 都去除呢?这会带来一个严重的问题,那就是将基类对象拷贝到继承类对象时会覆盖原先的值。下图清晰地说明了这个过程。
继承时去除padding.png
再加上多态
多态会带来额外的时间空间负担:
- 类导入一个虚函数表存放虚函数地址
- 每个对象导入一个 vptr ,指向虚函数表
- 构造函数中初始化 vptr
- 析构函数中去除 vptr
不同编译器把 vptr 放置在对象的不同位置,比如 cfront 放在尾端,g++ 放在头部。
如果 vptr 放在头部,试想一种场景,基类没有虚函数,而派生类有,这时候我用一个基类指针指向派生类对象,那么这个指针应该向后偏移4字节(或8字节)的长度,才能正确指向一个对象。
为了验证以上内容,我写了如下代码,然而打印出来的两个地址相同:
#include <iostream>
using namespace std;
class Base{
};
class Derive:public Base{
public:
virtual void func(){}
};
int main(){
Derive d;
cout<<&d<<endl;
Base *b=&d;
cout<<b<<endl;
}
so,where is wrong?
多继承
派生类指针指向基类对象在遇到多继承时,不仅要考虑成员变量的偏移,还要考虑基类对象的偏移。
以 Vertex3d 继承 Vertex 和 Point3d 为例:
多继承内存布局.png
Vertex3d v3d;
Vertex *pv;
pv = &v3d;
编译器会将以上代码转换为
pv = (Vertex*)((char*)&v3d + sizeof( Point3d ));
虚继承
虚继承的一种实现方式,就是让每个对象持有一个指针,指向虚基类对象,如果要对虚基类成员存取,可以通过该指针间接完成。这称之为 Pointer Strategy。
PointerStrategy.PNG
但是如果有多个虚基类呢?这样做就会加重对象的负担。于是第二种方式就是引入 virtual base class table,每个对象拥有指向虚基类表的指针就可以了。
除此之外,还有一种解决方式就是,将虚基类偏移值放在虚函数表中。这称之为 Virtual Table Offset Strategy。
VirtualTableOffsetStrategy.PNG
以上可以看出,虚基类如果有成员变量,其存取是一件很麻烦,可能会影响效率的事情, 所以最好在虚基类中不声明任何成员变量。
3.6 指向成员变量的指针
指向成员变量的指针会是什么值?
A::* pa = &A::member_name
,其中 member_name 是类A一个成员变量的名字,试想这么一条语句,打印 pa 会显示某个地址吗?显然不能,因为我都没创建一个对象呢,怎么能告诉我这个对象的成员变量地址在哪?那么 pa 到底是什么呢?答案是成员相对对象起始位置的偏移值。
这个偏移值是从1开始的,而不是从0开始。因为要区分指向第一个成员的指针和 指向成员的空指针(A::* pb = 0
) 这两种情况。
网友评论