当前位置:   article > 正文

《深度探索C++对象模型》(一)对象模型、存储形式;默认构造函数一定会构造么?_c++ 对象模型中不存储函数

c++ 对象模型中不存储函数

欢迎查看系列博客:

《深度探索C++对象模型》(一)对象模型、存储形式;默认构造函数一定会构造么(本篇)
《深度探索C++对象模型》(二)C++,new,delete,构造/析构,临时对象
《深度探索C++对象模型》(三)构造函数、拷贝构造和初始化列表

--------------------------------------------------------------------------------------------------------------

一)、读后感

    在我参加工作两年多的时候,工作不算很忙了,《深入理解C++对象模型》开始进入我的视野;或许是因为我要从Symbian.C++ 转向iOS Objective-C,并开始思考语言本身的一些东西的缘故。

    其实在一年前,出于对C++的迷惑,我已经买了这本书。当时翻了几页竟然没懂,就搁那儿了!可是现在,它让我随身携带、流连忘返、是个旅途好伴侣;看到它我精神抖擞,它给了我继续做程序员的信心

    这段时间经常会在晚上11点后,关闭电脑,然后捧着书本儿汲取知识。这种感觉觉很不错!如果你在北京上班,那么不要在地铁上捧着手机看新闻、看微博和QQ,可以看点儿书。

    以上是我感慨,也许你觉得我说的太罗嗦、夸张。我只能再引用李宗盛《鬼迷心窍》中的歌词:

        “有人问我你究竟是哪里好, 这么多年我还忘不了。“

         “春风再美也比不上你的好,没见过你的人不会明了。”

    我看的是左边“蓝绿色”的老版的,右边是2012版的。我看过新版的目录,跟老的基本一样。买新的吧

                           

    阅读者要求。需要具备C++的基础知识。这本说就像译者评论的那样,不是婴幼儿奶粉,它是成人专用的低脂高钙特殊奶粉。假如把C++比喻成一辆汽车,这本书不是教你怎么开车,而是将汽车大卸八块,逐个部件剖析。

    这本说的作者也有一些地方是相互矛盾的,很难理解,难道是C++太复杂了么。

    专业术语介绍:

derived class派生类
base class基类
member function成员函数
nonvirtual function非虚函数



二)、回答几个小问题

    这本书的作者就是C++第一个编译器(cfront)的负责人,所以作者主要从编译器的角度来剖析C++的对象模型。

    第一个、一般来说在学习C++的时候,如果没有指明一个构造函数,那么系统会默认创建构造函数。非也,编译器会决定是否有必要生成一个构造函数和析构函数。也就默认构造函数可能不存在哦!特别是没有继承的情况下,编译器认为构造函数和析构函数是无用的。(参考p231)

    第二个、假设两个基类BaseABaseB都有virutal函数,BaseC继承自BaseABaseB,那么BaseC会有几个虚函数表?答案是:根据编译器不同而不同,有些是两个虚函数表。有些是一个表,比如sun的编译器。注意:这种情况属于多重继承,BaseC肯定会有两个虚函数表指针

    第三个、局部变量和全局变量重名了,在局部变量的生命周期的大括号之内使用这个变量,那个起作用。当然是局部变量,但是C++并不是从一开始就是这么设计的。

    一定要重复阅读第三章:Data语意学第四章:function语意学,和五章:构造、析构、拷贝语意学,这时平时开发中最常见的。

    侯捷翻译的很不错,很多地方比如“虚函数”,基类,派生类,直接用virtual function 、base class derived class代替,很符合程序员的习惯。

下面开始笔记本分

三)、类属性(Data语意学p83-p143)

---》一个空的类,大小不是0而是1,因为编译器会生成一个隐晦的1bytes,用于区分,当该类多个对象时,各个对象都能在内存分配唯一地址。(p84)

---》成员变量的内存对齐,例如一个类只有char a一个属性; 但是它的大小是4.虽然char的大小是1。(p85)

---》为了保持跟C的兼容性,C++不要求基类属性跟派生类属性的排列顺序,这个完全有编译器决定。(p88)

---》局部变量和全局变量重名情况,在局部变量的生命周期的大括号之内,使用该变量,哪个起作用?在1990年 随着The Annotated C++ Reference Manual修订,局部变量开始隐藏全局同名变量。而之前则是不隐藏。(p89)

---》属性的内存顺序和声明顺序是一致的。不同级别(public、protected和private)属性的排列顺序是相对一致的,就是说可能不连续,但是必须符合较晚出现的属性存在较高的地址。(p92)

---》虚函数表指针Vptr,可能存在类的开始,也有可能存在类的末尾。通常都是类的末尾。(p92,p111,p112)

首先介绍vptr存在末端模式。下图演示单一继承并含有虚函数情况下的数据布局(自然多态)。Point2d 和Point3d是继承关系,注意:Vptr放在类的末尾。



初学者不要以为派生类的虚函数表指针Vptr(类结构中存的是虚函数表指针,并非虚函数表)存在派生类的那个部位,它依然是在父类的完整对象结构中

只不过,在派生类构造的时候,会将vptr所指向的virtual table修改。

vptr在前端模式,这么做丧失了与C的兼容性。


    如果是前端存放,还存在一个问题:如果基类没有虚函数,派生类有虚函数,那么单一继承的自然多态就会被打破。如果要将派生类转换成基类,必须编译器的介入。(p112)


    编译器似乎开始发挥它的作用了。多重继承下又是虚拟继承,编译器必须做出必要的偏移和调整,才能保证正确的调用虚函数。

---》对一个类对象取地址,那么并不是第一个属性的地址,第一个属性的地址还需要+1,这么做是为了区分指向第一个属性和指向所有属性的指针两种情况。(p98)

---》一般而言,基类属性在派生类的开始部分,但是C++任何一条规则,只要碰上虚继承就没辙儿了。(p99)

---》C++语言保证“出现在派生类中的基类对象,有其完整性”,这么做是为了在位拷贝的时候,能够拷贝正确。(p106)

假如ClassA 和ClassB都有一个char的属性,假设ClassB 继承自ClassA,假设,C++为了节省内存,将自己的char类型和基类的char类型绑定一起,那么经过下面表达式后可能出现问题:

ClassA* a = new ClassB;

ClassB b = *a;

下图描述的是“紧凑类型”,这样会导致严重后果,派生类的属性可能被“抹掉”,如图中的char b


不要以为ClassB中的char b和ClassA中的char 会放在一起,由于内存对齐的规则,ClassA大小是4B,ClassB大小是8B。这样即使拷贝就不会出问题。

下图描述的是父类在子类中有完整的对象结构:


(同样就像刚才我说的那样:虚拟继承将破坏这种父类结构的完整型)

---》单一继承下,父类通常在派生类前端。所以不管继承有多深,把一个derived class指定给class,该操作不需要编译器的介入。多重继承既不像单一继承,也不容易模拟出其模型,多重继承的复杂度在于derived class和其上一个base class 乃至于上上一个base class......之间的“非自然”关系,(p112)

多重继承的问题主要发生于derived class和其第二或后继的base class 之间的转换。

对于一个多重派生对象,将其地址指定给“最左端(也就是第一个)基类的指针”,情况和单一继承时相同,因为两者都指向相同的起始地址。需要付出的成本只是地址的指定操作而已,至于第二个或后继的base class的地址指定操作,则需要进行地址修改:加上或者减去介于中间base class大小。

下图展示了多继承的关系。涉及到4个类 Point2d、Point3d、Vertex和Vertex3d(p115)



下面展示了多重继承的对象模型。



多继承的情况下,drived clas可能会有两个或两个以上虚函数表指针

请看下面的表达式:

Vertex3d v3d;

Vertex *pv;

Point2d *p2d;

Point3d *p3d;

那么这个操作 pv = &v3d  需要转换内部代码pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d));

下面这两个操作,只需要拷贝地址就行了。

p2d = &v3d;

p3d = &v3d;

---》虚拟多继承情况(p117)

下图可以表现Vertex3d 的继承体系图。左为多重继承,右为虚拟多重继承。


    不论是Vertex还是Point3d都内含一个Point2d。然而在Vertex3d的对象布局中,我们只需要单一一份Point2d就好。所以引入虚拟继承。然而编译器要实现虚拟继承,实在是困难度颇高。虚拟继承的原则就是:让VertexPoint3d各自维护的Point2d 折叠成一个有Vertex3d维护的单一Point2d,并且还可以保存base class 和derived class的指针之间的多台指定操作。

    如果一个class含有virtual base class 那么,该对象将被分割为两部分:一个不变局部和一个共享局部。不变局部中的数据,不管后继如何演化,总是拥有固定的offset,所以这部分数据可以直接存取。至于共享局部(即virtual base class),这一部分的数据,其位置会因为每次的派生操作而有变化,所以他们只能被间接存取。各家编译器实现技术之间的差异就是间接存取的方法不同,目前有三种主流策略。

第一个策略:如何存取class的共享局部呢?cfront编译器会在每一个derived class中安插一个指向virtual base class的指针,这样就可以间接存取。这样的实现模型会有下面两个主要缺点:

1.每一个对象必须针对其每一个virtual base class 背负一个额外的指针。

解决方法有:第一个,Microsoft编译器引入所谓的virtual base class table。每一个class object如果有一个或多个virtual base class,就会由编译器安插一个指针,指向virtual base class table。至于真正的virtual base class 指针,当然是被放在该表格中。

请看下面的虚拟继承对象模型,如图。


红框内即所谓的“共享局部”,其位置会因每次派生操作而有所变化。虚拟破坏了base class 的对象完整型,虚拟继承会在自己类中生成一个虚函数表指针。

第二个、在virtual function table 中放置virtual base class的offset(不是地址)。


这个方法的好处是,巧妙的利用了虚函数表的结构,使得drived class 能够节省一个指针的大小。上图中国蓝色曲线是offset

2.由于虚拟继承串链的加长,导致间接存取层次的增加。例如:如果我们有三层虚拟衍化,我就需要三次间接存取(经由三个virtual base class指针)。

这个问题的解决方案有:拷贝所有的virtual base class 的指针到drived class中。这样就解决了存取时间的问题,虽然会有空间的开销。

一般而言,virtual base class 最有效的一种运行形式就是:一个抽象的virtual base class 没有任何的data members。也许正是java和Objective-c不使用多重继承,却使用接口类(OC叫协议)的原因。

---》如果对类的属性取地址(p130)

比如 &Point3d::z得到的值将是z在所有属性中偏移量。

打印该值的时候必须使用这个方法   :printf("&Point3d::z =%p\n",&Point3d::z);

四)、类方法(function语意学p139-p186)

---》 C++的成员函数有三种:static 、nonstatic和virtual。每一种类型的调用方法都不同。(p140)

---》C++的设计原则之一就是nonstatic member function至少必须和一般的nonmember function有相同的效率。而实际上成员函数也是被转化为nonmember function调用,下面是转化步骤:(p142)

1.改写函数的签名(signature,函数名称+参数数目+参数类型)安插一个this指针到函数参数中来。

例如:float Point3d::magnitude3d()const;

经过改写后的方法为:float Point3d::magnitude3d(const Point3d* const this)const

***这也就是问什么:const  可以用来区分重载函数的标示的,包括const参数或const函数,但是返回值不算,因为返回值不会作为函数的签名。

2.对nonstatic data member 的存取操作改为经由this指针来完成。

3.将member function重新写成一个外部函数。对函数进行mangling(重新命名)处理,是它在程序中成为独一无二的语汇。

---》一般而言,member function(data member也是一样)的名称前面会被加上class名称,形成独一无二的命名。(p144)

---》目前C++编译器对name mangling的做法还没有统一,但是迟早会统一。(p145)

---》虚函数(p147)

如果函数normalize()是一个虚函数,那么下面的调用 ptr->normalize()将被内部转化为:

(*ptr->vptr[1])(this); vptr是有编译器产生的指针,指向virtual table。下标为1说明是是第1个虚函数。

---》静态成员函数将被转化为一般的nonmember函数调用。它不能存取nonstatic members,不能声明为:const、volatile或virtual。

由于静态成员函数缺乏this指针,因此其差不多等同于nonmember function。它提供了一个意想不到的好处:成为callback函数。

---》虚拟成员函数(p152)

在C++中多态(polymorphism)表示”以一个public base class的指针(或者reference),寻址处一个derived class object“的意思。

在C++中virtual functions可以在编译时期获知,这一组地址是固定不变的,执行期不可能新增或者替换值。

请看下面一个类Point的定义:

  1. class Point {
  2. public:
  3. virtual ~Point();
  4. virtual Point& mult(float)=0;
  5. float x()const {return _x;}
  6. float y()const {return 0.0;}
  7. float z()const {return 0.0;}
  8. protected:
  9. Point(float x=0.0);
  10. float _x;
  11. };

Point2d继承自Point。Point3d继承自Point2d。那么内存模型如图,单一继承情况


在单一继承体系中,virtual function机制的行为十分良好,不但有效率而且很容易塑造其模型出来,但是在多重继承和虚拟继承中,对virtual function的支持就没有那么美好了

---》thunk技术(p162)

所谓的thunk是一段assembly码,用来以适当的offser值调整this指针,跳到virtual function去。Thunk技术允许virtual table slot 继续内含一个简单的指针,因此多重继承不需要任何空间上的额外负担。slots中的地址可以直接指向virtual function,也可以指向一个相关的thunk。

---》vptr将在构造函数中被设立初始值。(p164)

---》多重继承下的虚函数

    多重继承下,通常派生类会有多个virtual table ,最左边基类的称之为:“主要表格”,第二或更过多基类的表格称为:“次要表格”(参考上图),派生类的主要表格和次要表格可以连在一起,比如Sun的编译器的策略就是这样的。(p164,p165)

class drived 继承自 class Base1 class Base2 类结构如下:

  1. class Base1{ class Base2{
  2. public: public:
  3. Base1(); Base2();
  4. virtual ~Base1(); virtual ~Base2();
  5. virtual void SpeakClearly(); virtual void mumble();
  6. virtual Base1* clone() const; virtual Base2* clone()const;
  7. }; };

这两个类我故意并列在一起,Base1和Base2的区别就是两个不同的虚函数void SpeakClearly()和void mumble();

  1. class Derived: public Base1,public Base2{
  2. public:
  3. Derived();
  4. virtual ~Derived();
  5. virtual Derived* clone()const;
  6. protected:
  7. float data_derived;
  8. };

那么这几个类的virtual table的布局如下:




多重继承下:derived类会分别重写“主要表格”和“次要表格”

---》虚拟继承下的虚函数。




---》当然这本说也不是如此的深入,当一个virtual base class 从另外一个virtual base class派生而来,并且两者都支持virtual functions和nonstatic data members时,编译器对于virtual的支持简直就像进入迷宫一样。作者只是给了一句话“距离复杂的深渊悬崖不远了。”(p169)

---》获取一个nonstatic member function的地址,如果该函数是non virtual,则得到的结果是它在内存中的真实地址。然而这个地址是不全的,他也需要被绑定与某个class object的地址上(this指针),才能过通过它调用函数。(p174)

---》获取一个virtual member function的地址,只能获取一个索引值。(p176)

那么,如果使用一个函数指针float (Point::*pmf)() = &Point::z;这时pmf是一个索引值。

但是,pmf还可以指向一个nonvirtual member function的真实地址啊?cfront的做法是如果pmf大于127就是真实地址,如果小于127就是索引值。当然这种设计限定了继承体系中只能有128个virtual function,这并不是我们希望看到的。在多重继承的引入后又有了别的方法解决这个问题。然而,刚刚说的这个方法就淘汰了。(p178)

---》多重继承下,指向member functions的指针。指向member function的指针需要先指向一个结构体,该结构体中存放几个属性分别表示virtual table的索引和non virtual member function的地址。详情见(p179)

---》inline函数提供了一个强有力的工具。然后与non-inline函数比起来,他们需要更小心的处理。

五),构造、析构和拷贝语意学(p191-p236)

看第五章跟打游戏一样,看着看着不行了,看不懂了,这关没过去,还得从头儿再来。

---》每一个derived class destructor 会被编译器加以扩展,以静态调用的方式调用其“每一个virtual base class”已经“上一层base class”的destructor。所以virtual function不要声明为pure(p193)

point的声明

  1. type struct
  2. {
  3. float x,y,z;
  4. }Point;

point的使用:

Point global;

  1. Point foobar()
  2. {
  3. Point local;
  4. Point *heap = new Point;
  5. *heap = local;
  6. delete heap;
  7. return local;
  8. }

观念上Point的构造函数和析构函数会被编译器创建,事实上并非如此:Point被编译器看做是Plain Ol' Data。

---》无继承情况下的对象构造(p196)

---》不论是private、public存取层,或是member function的声明,都不会占用对象的空间。(p199)

---》constructor可能内带大量隐藏代码,因为编译器会扩充每一个constructor,大致有下面几种情况:(p206)

        1.初始化“初始化列表中的数据”

        2.如果data member没有出现在初始化列表中,将调用data member的constructor。

        3.如果有vptr进行初始化。

        4.上一层的base class constructor必须呗调用,以base class的声明顺序为准。

        5.所有virtual base class constructor必须被调用。

---》虚拟继承下的构造函数。(p210)

如下图的继承关系。


如果Vertex3d构造的时候,必然调用Point3d的构造函数,同时调用Vertex的构造函数,然而这两个类都要必须调用Point2d的构造函数,这是不合理的。取而代之的是应该在Vertex3d的构造函数中直接对Point2d初始化。这样就需要Vertex3d再条用Point3d或者Vertex的构造函数的时候传递一个bool参数__most_derived,即“是否是最后一层继承关系”,然后Point3d或者Vertex的构造函数根据这个bool变量决定是否构造Point2d。

总结为一句话:virtual base class constructor,只有当一个完整的class object被定义出来时,它才会被调用。如果object只是某个完整的object的suboject

,他就不会被调用。

---》vptr的初始化(p213)

在base class constructor调用操作之后,但是在程序员提供的代码或是“member initialization list中所列的members初始化操作”之前编译器对vptr进行初始化。这个过程就像想象的那样:一个PVertex对象会先成为一个point2d对象。一个point3d对象、一个vertex对象和一个vertex3d对象,最后才成为一个PVertex对象。

---》一个构造函数的真实步骤可能如下:(216)

        1.在derived class constructor 中,“所有virtual base classes”及“上一层base class”的constructor会被调用。

        2.上述完成后,对象vptr(可能多个vptrs)被初始化,指向相关的virtual table(可能多个表)

        3如果有member initialization list 的话,将在constructor体内扩展开来。这必须在vptr被设定之后才进行,以免有一个virtual member function被调用。

        4.最后,执行程序员所提供的代码。

---》如果不准将一个class object指定给另外一个class object,那么只要将copy assignment operator声明为private即可。(p219)

---》析构函数(p231)

如果class 没有定义destructor,那么只有在class内带的member object(或是class自己的base class)拥有destructor的情况下,编译器才会自动合成出一个来。否者destructor被视为不需要,也就不需要合成(当然更不需要调用)

---》析构函数的实际操作可能如下:

        1.destructor的函数本身首先被执行

        2.如果class拥有member class objects,而后者拥有destructor,那么它们会以其声明顺寻的反序被调用。

        3.如果object内带一个vptr,则现在被重新设定,指向适当的base class的virtual table

        4.如果有任何直接的nonvirtual base lasses 拥有destructor,它们会以其声明的反序被调用。

        5.如果有任何 virtual base class拥有destructor,而当前讨论的这个class是最末端(most-derived)的class,那么它们会以其原来的构造顺寻的相反顺寻被调用。


以上是第三章、第四章和第五章的主要内容。

 - - - - - - - - -未完待续---------- 剩余章节会新写一个blog- - - - - - - - - - - 

六、C++大记事:

    1993年引入RTTI。

    1991年引入templates 模板(在cfront 3.0中引入的)

    1990 随着The Annotated C++ Reference Manual修订,局部变量开始隐藏全局同名变量。

    1989年,发布了Release 2.0。引入了多重继承、抽象类、常数成员函数,以及成员保护。

    1987年 引入静态成员函数。

    20世纪80年代中期引入虚函数。


从某种角度上来说,C++的强大要归功与C++的编译器的强大。这时我才知道为什么用很厚一本书来介绍visual studio,可能也是Symbian不用标准C++的原因。


如有问题,欢迎大家斧正!


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/404695
推荐阅读
相关标签
  

闽ICP备14008679号