《深度探索C++对象模型》随笔


这大概是第三遍看这本书,第一遍是看pdf模模糊糊的。第二遍是买了书之后,笔记写在OneNote上,没法看。第三遍就是现在了,还是看懂了不少以前没看懂的东西,不过还是有些东西被跳过了,比如六七章的很多东西。

第一章 关于对象

  1. C++中,有两种数据成员:
    ①静态数据成员②非静态数据成员
  三种成员函数:
    ①静态成员函数②非静态成员函数③虚函数
  

  2. 静态数据成员放在类之外,非静态数据成员被配置与每一个类对象之类
  静态和非静态成员函数也放在类之外,虚函数则是特殊情况:
    ①每个类产生一堆指向其虚函数的指针,集合在一个表格中,该表成为虚函数表(vtbl)
    ②每个类对象被安插一个指针,指向虚函数表。虚函数表的初始化和重设由构造、析构、赋值运算符自动完成。
  

  3. 基类子对象的数据成员直接放置于派生类对象中。
  

  4. C++凡属于同一个控制层级的数据,必定保证以声明顺序出现在内存布局当中。而不同层级之间的数据顺序则不确定。
  基类和派生类的数据成员顺序也未有规定。

  5. C Struct在C++中的一个合理用途,一个复杂的类对象全部或部分到某个C函数,Struct可以封装起来,保证其拥有兼容C的空间布局,然后只在组合情况下存在,继承情况下是不可行的。

  6. 一个对象的大小:非静态数据成员大小总和 + 边界对齐 + 支持虚函数产生的额外负担(vtbl)

  7. 各个类型的指针本质上相同,指针本身所需内存都一样大,区别在于编译器对不同类型的指针时,根据指针类型确定需要读取内存的长度。

  8. 派生类对象直接赋值给基类对象引起对象切割。

                                      2020年10月19日13:44:41

第二章 构造函数语义学

  1. 有4种情况,造成编译器为 未声明构造的类 合成一个默认构造。他们被合成出来只是为了满足编译器的需要。只有这四种情况合成出来的隐式的 默认的 构造是有效的。编译器不会初始化每一个成员。对于以下四种情况,该干嘛干嘛,但是对于任何一个内置类型(整形、整形指针等等),编译器不会去初始化,需要程序员自己来解决,因为这不是编译器需要的。
    ①类含有一个成员类对象,编译器需要在构造中调用哪个成员类对象的构造函数。
    ②派生类。编译器需要为派生类构造调用基类的构造函数。
    ③虚函数。为对象初始化虚函数表。填写虚表指针。
    ④虚基类。完成虚基类机制。调用基类构造,安插那些"允许每一个虚基类的执行期存取操作的代码。"(没怎么看明白)

  2. 复制构造把每一个内置的或者继承而来的成员,从一个拷贝到另一个上,不过对于成员对象,不是直接复制,而是递归执行,拷贝其成员。复制构造也分为有效和无效,只有有效的会被合成与程序之中。
  决定一个复制构造是否有效:是否bitwise。有bitwise语义即为有效构造。
  以下四种情况不展现bitwise copy 语义
    ①有一个成员对象且成员对象的类有一个复制构造。
    ②是派生类,基类有复制构造。
    ③有虚函数。重设虚表指针,尤其是在派生类给基类赋值时。
    ④有虚基类。

  3. 成员初值列队伍:①引用成员②常量成员③调用基类构造④调用成员对象的构造
    初值列顺序要和其声明顺序相同

                                      2020年10月19日22:11:13

第三章 data语义学

  1. 一个空类有一个隐藏的1byte,用来在声明对象时取得一个独一无二的地址。

  2. 成员函数在看到整个类声明之后才去寻找绑定数据成员。因为如果成员函数未看到全部的类声明时,又出现了所需要的数据成员还没看到,同时又有一个同名的全局对象,那么成员函数就会取到同名的全局对象,而不是成员变量。

  3.
  ①非静态数据成员在对象中排列顺序和其声明顺序一样
  ②成员排列符合:(同一控制层级)较晚出现的成员在类对象中拥有较高的地址。中间可能出现其他东西,比如内存对齐。
  ③不同控制层级的数据成员可以自由排列。
  ④静态成员视为全局变量。

  4. 普通继承没有虚函数。派生类对象包含基类数据成员以及自身的数据成员,且基类成员总是先出现。但是一旦碰上虚基类就例外了。只是普通继承没有虚函数,就不会带来额外的时间和空间的开支。

  5. 普通继承加虚函数。为此需要增加一个虚表指针,虚表指针有的放在整个对象末尾,可以兼容C Struct,因为前面的都是数据,取地址操作即可。有的放在对象开头,但会失去C Struct的兼容性。同时需要在构造析构时完成对虚表指针的处理。

  6. 派生类对象保持基类对象完整性。class A{char c1;} class B: public A{char c2;}首先A被对齐后4字节,然后B继承A后保持这4字节的完整性,接下来B对齐,一个B对象8字节。

  7. 多重继承。一般是按继承的顺序在内存中组织对象。取第一个被继承的基类地址,和派生类地址相同,取后续基类的地址,则需要修改指针。

  8. 虚继承。虚继承将组织分为两个区域:不变区域和共享区域。不变区域中,不论后续如何演化,总拥有固定的偏移。共享区域村粗虚基类子对象,因其位置随每次派生操作变化,所以只能间接存取。

  9. 虚基类最有效的情况:抽象虚基类,没有任何数据成员。

  10. 指向类成员数据的指针:主要内容是对数据成员取地址,直接对类成员取地址得到的是偏移,对某个具现对象数据成员取地址得到的是实际地址。同时,取出来的地址会被+1,这么做的原因是为了区别空值。当声明了一个数据成员指针,如果他的偏移是0,那么到底是解读为空指针还是为0的偏移呢?当然我觉得没什么人会去声明一个指向数据成员的指针。代码测试也不写了。

  11. 单一继承下,开启优化后,和正常类效率一样。一旦涉及虚拟继承,代价大概是3倍左右。

                                      2020年10月20日20:43:48

第四章 functoion语义学

  1. 非静态成员函数。成员函数会被转化为非成员函数,其代价也和非成员函数相同。安插一个额外参数到成员函数,该参数即为this指针。对非静态数据成员的存取也通过this指针。

  2. 静态成员函数。被转换为一般非成员函数调用。

  3. 虚函数实现。一个复杂继承结构的类中,可能有多个虚表指针。其中为了找到虚表,在每个对象中添加虚表指针成员,为了找到虚表中对应的函数,每个虚函数被指派一个在表中的索引值,其中vtbl[0]大概存储的是类型信息。一个类可能未修改继承而来的虚函数,那么其父类对应虚函数的地址被复制到该类的虚函数表中。换一种说法,我不知道通过虚函数表找到的虚函数到底是谁的,但是我知道这个函数的在虚表的索引,只要调用这个函数,我就按索引来找函数。

  4. 多继承下的虚函数。多重继承下,复杂度主要在第二个以及随后的基类上,以及在执行期期间调整this指针。通过对this指针进行偏移。一个派生类内含n-1个额外虚表,n表示其基类个数。

  5. 成员函数指针还好吧。用bind绑定过一次,效率上也不会差太多。至于成员虚函数指针,应该不会有这种阴间操作吧。

  6. 总:非成员函数、静态成员函数、非静态成员函数都会被转化为完全相同的形式。增加继承甚多,就会增加执行成本。涉及虚函数时,每多继承一层,就多增加一个虚表指针。

                                      2020年10月21日21:19:55

第五章 构造、拷贝、析构语义学

  1. 不要把虚析构声明为纯虚。

  2. 自定义的构造被编译器扩展,以便将虚表指针初始化,这些代码放在基类构造调用之后,程序员提供的代码之前。

  3. 继承体系下的构造。
    ①所有虚基类构造被调用,从左到右。如果类位于成员初值列,有任何显示指定的参数,都应该传递过去,否则若有一个默认构造调用之。
    ②调用所有基类构造,调用顺序为其声明顺序(从左到右)。同上,在成员初值列就调用,否则若有默认构造,就调用。
    ③接着类的虚表指针被设置。
    ④执行成员初值列的操作。
    ⑤若成员对象有默认构造,调用之。
    ⑥执行程序员提供的代码。
    在构造函数中调用虚函数,语法上是安全的,但是语义上可能不安全,因为虚表指针虽被设定好,但是其中的虚函数却可能涉及到那些 暂时未被初始化的数据成员。

  4. 是否提供一个复制或赋值构造,应该取决于其默认行为是否足够。默认行为是对于内置类型都只是简单拷贝,对于成员对象调用其复制构造。
  复制或赋值构造在虚拟情况下效率低下。甚至因为虚拟继承的原因,会调用多次其复制赋值构造。
所以最好不要再虚基类中声明数据。

  5. 析构。如果类没有定义析构,只有在内含成员对象或基类时会自己合成。
  析构函数执行顺序与构造相反。
    ①首先析构函数本体,即程序员提供的代码先被执行。
    ②类成员对象有析构,调用之。
    ③虚表指针被重设,指向适当的基类虚表。
    ④若上一层的非虚基类有析构,从右到左依次调用之。
    ⑤调用虚基类的析构。

                                      2020年10月22日12:59:16

第六章 执行期语义学

  1. 全局对象。
    ①为每一个需要静态初始化的文件产生一个_sti()函数,内含全局对象的掉哦那个操作或内联扩展。
    ②和①相反,为每个全局对象清理行为生成_std()函数,内含析构调用。
    ③一个_main()函数,用以调用所有的_sti()函数,一个exit()函数,用以调用所有_std()函数。

  2. 局部静态对象。其构造析构只被调用一次。编译器的行为是在程序起始时构造对象。

  3. 对象数组。
    使用vec_new()的函数,产生以类对象构造的数组。在vec_new中为每个对象调用其构造。
    结束时,使用vec_delete或者vec_vdelete()来为其调用析构。
    对于设定了初值的元素,调用vec_new()时会跳过他们。

  4. new和delete。
    如果以new配置对象,而构造抛出异常,内存会被释放掉,但是异常会被继续抛出。

第七章

  第七章是模板、异常、执行期类型识别三个内容。简单的太简单,难得一点也看不懂,懒了懒了。

                                      2020年10月22日23:40:39

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值