• 继承指针《深度探索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 很不错是因为它可以运行在所有的操作系统上,那么就可以说 肛交很不错,因为其可以使用于所有的性别上。

  • 相关阅读:
    JavaScript 中document.write() 详细用法介绍
    教你怎么用JavaScript检测当前浏览器是无头浏览器
    JavaScript网页截屏方法,你get到了嘛?
    新手小白该怎么学习前端?附学习路线和资料
    实现微前端需要了解的 Vue Genesis 渲染器
    前端新人关注的Web前端饱和性分析?前端面试必知必会的十点!
    这个前端竟然用动态规划写瀑布流布局?给我打死他!
    Kubernetes之Ingress+Traefik
    MySQL语法大全
    PPTP服务器
  • 原文地址:https://www.cnblogs.com/jiangu66/p/3074114.html
Copyright © 2020-2023  润新知