zoukankan      html  css  js  c++  java
  • 《深度探索C++对象模型》读书笔记(二)

    第三章:Data语意学

    这一章主要讲了类中的数据在内存中是如何分配的,包括(多重)继承和多态。

    让我们首先从一段代码开始:

    class X{};
    class Y :virtual public X{};
    class Z :virtual public X{};
    class A :public Y, public Z{};
    std::cout << sizeof(X) << std::endl;
    std::cout << sizeof(Y) << std::endl;
    std::cout << sizeof(Z) << std::endl;
    std::cout << sizeof(A) << std::endl;

        在VS2013上输出的结果为1,4,4,8。为什么会这样呢?这就涉及到编译器针对C++语法而采取的对象模型。X虽然是一个空类,但为了使它的对象具有地址,编译器实际上是会为它的对象分配1个字节大小的空间,因此它的大小为1。Y和Z是对X的虚拟继承,编译器会为它们各分配一个指针,指向X的对象,因此它们的大小为4.A同理,它是Y和Z的多重继承,因此编译器会为它分配两个指针,所以它的大小为8.内存分布如图所示:

       

       书中还提到了一种模型,其输出的结果为1,8,8,12。这种模型的思路是给每一个空类分配1 byte的空间,再考虑到4 byte的alignment,因此可以推导出书中的结果,在这里不再赘述。

       关于类中Member的布局,C++ Standard是这样规定的:static data members不会被放到对象的布局之中;在同一个access section(public、private、protected等区段),member的排列是按照声明顺序分布的,但不一定是连续排列(因为存在alignment);同时,对于不同的access section,它们的data members自由排列,不必在乎声明顺序(也就是说access section之内是按照声明顺序排列,而access section之间自由排列)

       之后,书中讨论了单一继承、多态、多重继承、虚拟继承下类中内存分布的表现,这也是本章的重中之重。

       这是一个很简单的程序:

    Point3d origin;
    origin.x=0.0;
    

     执行这段程序所需要的时间和空间代价,随x的性质而不同。让我们分情况讨论:

    1.x是static data members

       类的每个static成员都只有一个实例,存放在程序的data segment之中,和对象无关;因此对于这种情况,对x的存取并不会招致任何空间和时间上的额外负担。

    2.x是 nonstatic data members

       访问类的nonstatic data members时,实际上编译器做了如下工作:

    //源代码
    origin.x=0.0;
    //编译后的代码
    *(&origin+(&Point3d::x))=0.0;
    

      也就是说,为了得到x的地址,编译器需要将origin的起始地址加上x的地址偏移量。(实际上,起码在VS里用&Point3d::x来表示偏移量是有问题的,但思想可以先这样理解)

     3.x是基类的变量,没有多态与多重继承

        在有继承的情况下,可能会导致空间上的浪费。我们来看这样一个例子:

       

        这个类中存有一个int和三个char,如果我们把这些变量都放到一个类中声明,那么算上alignment,它的对象大小为8字节。

        假设我们要继承:

        

        那么Concrete3的对象的大小将达到16字节,比原先的设计多了100%!

        这是因为alignment导致的,因为C++的对象模型中,在一个继承而来的类的内存分布里,各个基类需要分别遵循alignment,从而导致了空间的浪费。具体地对象布局可见下图:

        

    4.加上多态

       在这种情况下,无论是时间还是空间上,访问类的成员都会带来一定额外的负担,主要体现在以下几个方面:

       1.virtual table,用来存放它所声明的每一个virtual functions的地址。

       2.每一个对象中会有一个vptr,提供执行期的链接。

       3.编译器会重写constructor和destructor,使其能够创建和删除vptr。

    5.多重继承

       在多重继承的条件下,对于指针之间的赋值需要运行时计算。举个例子,以下的继承结构:

       

      我们声明几个对象和指针并赋值:

    Vertex3d v3d;
    Vertex *pv;
    Point2d *p2d;
    Point3d *p3d;
    pv=&v3d;
    p2d=&v3d;'
    p3d-&v3d;
    

      对于p3d和p2d的赋值,只需要直接将v3d的地址赋过去就好。但对于pv的赋值,编译器需要计算一个Vertex在Vertex3d中的偏移量,从这个偏移量起始来得到pv的地址。因为类之间的内存分布如下所示:

       

       Vertex3d中Vertex部分的起始地址并不是Vertex3d对象的起始地址,因此对pv赋值需要一个运行时计算的开销。

    6.虚拟继承

        在虚拟继承中,C++对象模型将Class分为两个区域,一个是不变区域,直接存储在对象中;一个是共享区域,存储的是virtual base class subobjects,它在内存中单独存储在某处,derived class object持有指向它的指针。在cfront编译器中,每一个derived class object中安插一些指针,每个指针指向一个virtual base class,为此需要付出相应的时间和空间成本。如下所示:

    //具体的类同上一节多重继承,不同的是Vertex和Point3d虚拟继承了Point2d
    void Point3d::operator+=(const Point3d&rhs)
    {
        x+=rhs.x;
        y+=rhs.y;
        z+=rhs.z;
    }
    
    //编译器翻译后的版本
    _vbcPoint2d->x+=rhs._vbcPoint2d->x;
    _vbcPoint2d->y+=rhs._vbcPoint2d->y;
    z+=rhs.z;
    

      这只是最基本的解决方案,书中还提出了一些编译器优化时间和空间的方法,感兴趣可以深入阅读一下。

    最后,书中探讨了如何获得类中某个成员的地址偏移。我在这里就总结两种方法:

    1.((int)&((structure*)0)->member)

    2.先通过 int Test::* pOffset = &Test::x;获取偏移变量,再利用reinterpret_cast<int>(*(void**)(&pOffset))将其转化为整形量。

  • 相关阅读:
    SubVerSion 快速入门教程
    利用JQuery的$.ajax()可以很方便的调用asp.net的后台方法
    函数式编程初探(functional programming)
    用Openfire架设自己的即时聊天服务器
    把DataTable中的身份证号导出excel的解决方案
    jQuery Ajax 全解析
    总结使用Cookies的一些问题
    VS2008 AJAX控件介绍
    支付宝服务接口,实现虚拟交易和实物交易
    Qt QElapsedTimer
  • 原文地址:https://www.cnblogs.com/wickedpriest/p/6580134.html
Copyright © 2011-2022 走看看