深度探索C++对象模型【第三章2】

来源:互联网 发布:ubuntu 14.04 安装 编辑:程序博客网 时间:2024/05/16 00:55

1:通过对象指针还是对象来存取数据成员之间的差异:

  • 当该类是一个派生类,且其继承链中有一个虚基类存在,并且存取的member成员是一个从该虚基类中继承而来的成员时,就会有重大的差异。此时通过指针来存取的话,由于多态机制的存在,指针所绑定的对象类型要到执行期才能确定,所以存取的操作也必须延长到执行期;但是如果使用对象来存取,就不会有这样的问题存在,成员的offset在编译器就能够确定(多态的执行期确定机制)。
个人理解:其实差异不大,这种情况出现的相对很少~

2:大部分编译器对于继承体系中,派生类中的数据排列方式,是让基类的成员先排列的(虚基类除外)

3:一般而言,具体继承(concrete inheritance)并不会带来空间或者存取时间上的额外负担。(相对于虚拟继承来说)

4:在继承关系下,容易犯一下的错误:
  • 重复设计一些相同操作的函数,且选择某些函数称为inline函数,是设计class的一个重要课题
  • 为了表现class的体系抽象,将class分为很多层,从而这样就会浪费非常多的内存空间(这样造成的复制功能也会产生差异)
5:如果没有多态的机制,那么进行具体继承(无虚函数机制)的对象进行复制时,编译器会将基类对象原本的填补空间拿出来给派生类对象覆盖,这并不是我们复制想要看到的结果~

6:加上多态之后的时间和空间成本:
  • 导入一个与该类相关的vbtl(这个table的元素个数一般是虚函数的个数再加上一两个slot用来保存RTTI)
  • 在该类的对象中导入一个vptr,使得每一个对象都能找到vbtl
  • 加强构造函数,使其能够为vptr设定初值
  • 加强析构函数,使其能够抹消vptr
这样的时间与空间成本,给我们带来的是多态的弹性:指针或引用绑定的类型不确定

7:将vptr放在对象的哪一个位置一直随着编译器的发展在变化。
  • 一开始是放置在类对象的末端,这样可以保留base class C struct的对象布局,因而兼容C。
  • 随着C++2.0出现,C++开始支持虚拟继承和抽象基类,且随着OO的兴起,将vptr放在对象的前端有了更大的好处,“在多重继承下,通过类对象的指针调用虚函数”会有一定的帮助

8:在单一继承(提供了“自然多态”)的情况下,关于继承体系中的基类与派生类转换。
  • 基类对象和派生类对象都是从相同的地址开始的,中间的差异只是派生类对象比较大(算是一个部分重合的状态)
  • 在上面的情况下,类对象从派生类到基类的转换(通过指针或者引用),此时并不需要编译器去改变起始地址,达到了最佳效率
  • 出现多态之后,将vptr放在了类对象的起始处,如果基类没有虚函数而派生类有,那么这样的“自然多态natural ploymorphism”就会被打破。将类对象从派生类到基类的转换(通过指针或者引用),就需要通过编译器来调整地址(调整vptr)
9:随着多重继承的出现,更需要编译器的介入
  • 对一个多重派生的对象,将其地址指定为“最左端(第一个基类)base class的指针”,这样的情况和单一继承时相同,二者指向相同的起始地址。(额外的成本:地址的指定操作)
  • 第二个或是后续base class的地址指定操作,只需要通过修改地址:加上对应base class对象大小即可
  • 多重继承的存取操作并不需要付出额外的成本,因为成员的位置在编译时已经确定,存取members只是一个简单的offset运算,就像单一继承一样~
10:随着虚拟继承的出现,编译器的介入也是必不可少
  • istream 和 ostream都内含有一个ios subobject。所以我们在iostream中,我们只需要一份ios subobject即可,这就需要导入我们所谓的虚拟继承(只存在单一实例)。
  • 我们将这两者各自维护的ios subobject折叠成一个由iostream维护的单一io subobject。
  • 一般的实现方法:一个class,如果其有一个或者多个虚基类,那么该类将会被分割为两部分,一个不变区域和一个共享区域。
  • 不变区域不管后续如何继承,总是拥有固定的offset,所以这一部分是可以直接存取的。共享区域,所表现的是虚基类类对象,这一部分数据将会随着派生的进行而改变,所以他们只能被间接存取
11:一般的策略是先建立其不变区域,再建立其共享区域。关于共享区域中数据的存取:
  • cfront编译器的做法是:在每一个派生类对象中安插一些指针,每个指针指向一个虚基类,要存取这些继承而来的虚基类对象中的成员,可以通过这些指针来完成
  • cfront编译器这样做法的缺点有二:每个对象都必须背负对每一个虚基类所设置的一个额外指针;由于继承串链的增长,导致间接存取的层数增大,影响了存取的时间效率
  • 针对第二个缺点,现有编译器的做法是将所有的内置虚基类指针都拷贝到派生类对象中,虽然付出了一定的空间代价,以空间换取时间
  • 针对第一个缺点,微软编译器提出的解决方法是生成一个虚基类表,每一个类对象如果都有一个或者多个的虚基类,就在编译器中安插一个指针,指向虚基类表,真正的虚基类指针会被放在虚基类表中。
  • 针对第一个缺点,另一种方法是在虚函数表中放置虚基类的offset偏移量,在这种策略下,虚函数表可以经由正值或是负值来索引,正值即索引到虚函数指针,负值即所引导虚基类偏移量。
12:对于虚基类来说,有两点值得关注的地方:
  • 虚基类带来的时间和空间负担以及复杂性也就说明了它的机制肯定会随着编译器的进化而改变
  • 一般而言,虚基类最好的用法就是不包含任何的数据成员
13:编译器的优化是非常多的,对于数组、struct/class、inline函数来说,编译器的优化使得他们的执行速率相同然而对于单一继承、虚拟继承、虚拟多重继承来说,编译器即使优化,也会有时间上的额外负担

14:class Point3d{};   Point3d origin;  
  • & Point3d::x;//取一个非静态成员变量的地址,将会得到其在class 中的偏移量offset
  • & origin::x;//取一个绑定在真正的对象上的成员的地址,将会得到其在内存中的真正地址
15:指向成员的指针来存取成员,在编译器优化条件下,效率相同。但是由于虚拟继承需要加上虚基类的偏移值,也就多了一层的间接性(妨碍了优化的有效性)