C++对象模型剖析(五)一一 Data语义学(一)

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 的存取操作就会延至执行期,经由一个额外的间接引导,才能解决。

  • 18
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值