继承指针《深度探索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)
第二个、假设两个基类BaseA和BaseB都有virutal函数,BaseC继承自BaseA和BaseB,那么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就好。所以引入虚拟继承。然而编译器要实现虚拟继承,实在是困难度颇高。虚拟继承的原则就是:让Vertex和Point3d各自维护的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 很不错是因为它可以运行在所有的操作系统上,那么就可以说 肛交很不错,因为其可以使用于所有的性别上。