程序员

《深入探索C++对象模型》笔记 Chapter3 成员变量

2018-09-09  本文已影响12人  朱明代月

《深入探索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 放置在对象的不同位置,比如 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 ) 这两种情况。

上一篇下一篇

猜你喜欢

热点阅读