Data 语义学
Data Member 的绑定
在早期的编译器上会存在一个很奇怪的现象,我们先看个例子吧,
extern float x;
class Point3d {
public:
Point3d( float, float, float );
float X() const { return x; }
void X( float new_x ) const { x = new_x; };
private:
float x, y, z;
};
对于上述的代码中的 float X() const { return x; }
返回的值是 X::x
还是 extrern float x
,我想很多人都会答对-- X::X
,但是在早期的编译器上却是 extern float x
。因为对于这些编译器来说,它们对一个 class 进行分析是在这个 class 被全部读取完成之后,也就是将这个 class 的右括号读完了,而在 float X() const { return x; }
中,此时 X::x
还并未被编译器进行分析,所以对编译器来说,x 就是 extern float x
所以,早期的防御性编程一般会这样做
-
将所有的 data member 都放在 class 的最开始
class Point3d { float x, y, z; public: float X() const { return x; } };
-
把所有的 inline function, 不管大小都放在 class 声明之外
class Point3d { public: float X(); Point3d(); void X( float ) const; }; inline float Point3d::X() const { return x; }
Data Member 的布局(Data Member layout)
Nonstatic Data Members 在 class object 中的排列顺序将和其被声明的顺序一样,任何中间介入的 static data members 都不会被放入对象布局中。
C++ Standard 要求,在同一个 access section 中,members 的排列只需要符合“较晚出现的 members 在class object 中具有较高的地址”即可。**需要注意:这并不意味着每一个 data member 的排列一定是连续的。**因为 members 之间的边界调整(alignment, 内存对齐)可能就需要填补一些字节。
编译器还可能会合成出一些内部使用的 data members,以支持整个对象模型。比如 vptr,但是不同的编译器会将 vptr 放在不同的位置上,有的会将 vptr 放在 class object 的头部,有的则会将 vptr 放在 class object 的尾部。
Data Member 的存取
在开始之前,作者提出了这样一个问题:
// 如果有一下两个定义, origin 和 pt,通过 origin 来存取和通过 pt 来存取有什么重大的差异吗?
// 提示:可以从 x 被确定时期来看,也可以从多态的角度来看,但本质上两个角度并没有区别
Point3d origin, *pt = &origin;
origin.x = 0.0;
pt->x = 0.0;
-
Static Data Member
**每一个 static data member 只有一个实例,存在程序的 data segment 之中。每一次程序访问 static member 时,就会被内部转化为对该唯一 extern 实例的直接操作。**每一个 member 的存取许可,以及与 class 关联,并不会招致任何空间上或执行时间上的额外负担(无论是在个别的 class object 还是在 static data member 本身。)
所以访问一个 class object 的 static member 时可以直接通过该 class object 进行访问
class test { public: static int _int; } int test::_int = 1; auto _test_func() -> int { std::cout << test::_int << std::endl; }
需要注意:每一个 class object 的 static member 的 初始化都需要在 class segment 之外进行(就像上面一样)。
如果取一个 static data member 的地址,会得到一个指向其数据类型的指针
auto _int_pointer = &test::_int; // 此时,这个 _int_pointer 的类型就是 const int*
-
Nonstatic Data Member
**Nonstatic Data Member 直接存放在每一个 class object 之中,除非经由显式的(explicit)或隐式的(implicit)class object,否则没有办法直接存取它们。**只要程序员在一个 member function 中直接处理一个 nonstatic data member,这时候 “implicit class object” 就会发生。
class test { public: int _int; } void test::_test() { _int = 1; } // 这时候这个函数在内部就会转化为 void test::_test() { this->_int = 1; }
-
对一个 nonstatic data member 进行存取
当对一个 nonstatic data member 进行存取时,编译器需要把 class object 的起始地址加上 data member 的 偏移位置(offset)。
比如:
origin._y = 0.0; // 在编译器内部就会转换为 &origin + (&Point3d::_y - 1);
这里需要注意了:通常的 指针 是不能直接进行
&Point3d::_y
这种操作的,但是 c++ 有一个 对象成员指针 (就是指向一个对象的 成员变量 指针)。声明的方法也很简单,比如上面的 就是float Point3d::* _name
。但是可能有人要问了,为什么要减一呢?为了使编译系统可以区分出 “一个指向 data member 指针,用以指出 class 的第一个 member” 和 “一个指向 data member 指针,没有指向任何 member” 两种情况。(不清楚的兄弟们,后面还会讲到这个 data member pointer。
每一个 nonstatic data member 的偏移位置在编译时期即可获知,甚至如果 member 属于一个 base class subobject(派生自单一或多重继承链)也是一样的。**所以,存取一个 nonstatic data member 的效率和存取一个 c struct member 或一个 nonderived class 的效率是一样的。
如果是虚拟继承(virtual Inheritance)的话,编译器会为 virtual base class 增加一层间接(在derived class 中添加一个指向这个 virtual base class 的指针,但是不同的编译器可能会有不同的实现方案,但总体的实现思想就是增加一层间接)。所以,就算是virtual Inheritance(需要多消耗一些时间,因为增加了一层间接,需要进行多次运算),一个 class object 的布局或者说其 data member 的偏移位置(offset)都可以在编译器确定下来。
-
回到开始的问题吧。
// 如果有一下两个定义, origin 和 pt,通过 origin 来存取和通过 pt 来存取有什么重大的差异吗?
Point3d origin, *pt = &origin;
origin.x = 0.0;pt->x = 0.0;
标准答案:当 Point3d
是一个 derived class,而其继承结构中有一个 virtual base class,并且被存取的 member 是一个从该 virtual base class 继承下来的 member 时,就会有重大的差异。这时候,我们不能够说 pt 必然指向哪一种 class type(因此,在编译期,编译器无法确定这个 member 的真正的偏移位置 offset,所以 pt 的存取操作就会延至执行期,经由一个额外的间接引导,才能解决。