继承指针《深度探索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的定义:

    class Point {

    public:

          virtual ~Point();

          virtual Point& mult(float)=0;

          float x()const {return _x;}

          float y()const {return 0.0;}

          float z()const {return 0.0;}

    protected:

        Point(float x=0.0);

        float _x;

    };

    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 类结构如下:

    class Base1{                                      class Base2{

    public:                                                    public:

          Base1();                                                      Base2();

          virtual ~Base1();                                        virtual ~Base2();

          virtual void SpeakClearly();                     virtual void mumble();

          virtual Base1* clone() const;                   virtual Base2* clone()const;

    };                                                                };

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

    class Derived: public Base1,public Base2{

    public:

            Derived();

            virtual ~Derived();

            virtual Derived* clone()const;

    protected:

            float data_derived;

    };

    那么这几个类的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的声明

    type struct

    {

        float x,y,z;

    }Point;

    point的使用:

    Point global;

    Point foobar()

    {

        Point local;

        Point *heap = new Point;

        *heap = local;

        delete heap;

        return local;

    }

    观念上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。

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

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

    1987年 引入静态成员函数。

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

    

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

    

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

    

文章结束给大家分享下程序员的一些笑话语录: 关于编程语言
如果 C++是一把锤子的话,那么编程就会变成大手指头。
如果你找了一百万只猴子来敲打一百万个键盘,那么会有一只猴子会敲出一 段 Java 程序,而其余的只会敲出 Perl 程序。
一阵急促的敲门声,“谁啊!”,过了 5 分钟,门外传来“Java”。
如果说 Java 很不错是因为它可以运行在所有的操作系统上,那么就可以说 肛交很不错,因为其可以使用于所有的性别上。

posted @ 2013-05-12 16:13  坚固66  阅读(240)  评论(0编辑  收藏  举报